From b113c9bc86d6fb5f4c5a12e46aa92c0da5343f01 Mon Sep 17 00:00:00 2001 From: Pavel Konov Date: Mon, 7 Sep 2026 14:44:30 -0700 Subject: [PATCH] Improve native optimization and expose MiniZinc strategy controls Ten-second native comparison (one thread). Each entry is the largest confirmed sampled size / observed failed upper size; two repetitions and all family variants must pass at the reported lower size. | Problem (input unit) | Original | Auto + race | Configured | |---|---:|---:|---:| | Knapsack (items) | 33 / 34 | 2304 / 2336 | 2304 / 2336 | | Assignment (tasks) | 17 / 18 | 64 / 66 | 31 / 32 | | Facility location (sites) | 8 / 9 | 44 / 45 | 54 / 56 | | Bin packing (items) | 17 / 18 | 47 / 48 | 30 / 31 | | Production planning (periods) | 11 / 12 | 27 / 29 | 28 / 29 | | Routing (cities) | 21 / 22 | 21 / 22 | 21 / 22 | | Weighted queens | 23 / 24 | 23 / 24 | 23 / 24 | Original is the preserved pre-algorithm optimization facade, not pristine upstream Gecode. Automatic exploration and restarting share the ten-second limit. These are historical sampled brackets, not speed ratios or general capacity guarantees. Production auto size 28 had mixed confirmations. Witnesses were independently checked; optimality is backend-reported. The combined-policy study used development families and does not isolate racing's contribution. It predates MiniZinc controls and this source cleanup. Full measurements, report, methods and evidence-verification commands: https://github.com/pavelmk/gecode-optimization-benchmarks Reproduce the source regression cases with CMake 3.21+, C++17, HiGHS 1.15.1 (commit 04024d701f79feb8e2f18bc3df0dffc04ef05088), and MiniZinc 2.10.1. Replace the two dependency paths with their local installations/checkouts: cmake -S . -B build/optimize -DCMAKE_BUILD_TYPE=Release \ -DGECODE_ENABLE_OPTIMIZE=ON \ -DGECODE_OPTIMIZE_HIGHS_SOURCE=/path/to/HiGHS \ -DGECODE_OPTIMIZE_MINIZINC_REGISTRATION=ON \ -DGECODE_OPTIMIZE_MINIZINC_EXECUTABLE=/path/to/minizinc \ -DGECODE_ENABLE_QT=OFF -DGECODE_ENABLE_GIST=OFF cmake --build build/optimize --config Release --parallel 4 ctest --test-dir build/optimize -C Release --output-on-failure Focused native and frontend cases: ctest --test-dir build/optimize -C Release --output-on-failure \ -R '^optimize-(native|flatzinc|minizinc)' --- .github/workflows/optimize.yml | 344 +++ CMakeLists.txt | 155 +- Makefile.in | 1 + README.md | 9 + cmake/GecodeConfig.cmake.in | 34 +- cmake/GecodeSources.cmake | 1 + docs/optimize-io.md | 106 + docs/optimize.md | 131 + doxygen/doxygen.conf.in | 7 + doxygen/doxygen.hh.in | 4 + doxygen/optimize.hh | 435 +++ examples/optimize-file.cpp | 107 + examples/optimize-linear.cpp | 27 + gecode/flatzinc/capture-records.hh | 122 + gecode/flatzinc/capture.cpp | 323 +++ gecode/flatzinc/capture.hh | 36 + gecode/flatzinc/lexer.lxx | 57 +- gecode/flatzinc/lexer.yy.cpp | 597 ++-- gecode/flatzinc/parser.hh | 31 +- gecode/flatzinc/parser.tab.cpp | 1965 ++++++++----- gecode/flatzinc/parser.tab.hpp | 5 +- gecode/flatzinc/parser.yxx | 263 +- gecode/kernel/core.cpp | 54 +- gecode/kernel/core.hpp | 42 +- gecode/minimodel/lp-backend.hpp | 328 +++ gecode/minimodel/lp-certificate.hpp | 405 +++ gecode/minimodel/lp-cut-loop.hpp | 315 +++ gecode/minimodel/lp-cuts.hpp | 422 +++ gecode/minimodel/lp-model.hpp | 202 ++ gecode/minimodel/lp-relaxation.hpp | 321 +++ gecode/minimodel/lp-strengthening.hpp | 435 +++ gecode/optimize.hh | 29 + gecode/optimize/CMakeLists.txt | 498 ++++ gecode/optimize/GecodeOptimizeConfig.cmake.in | 44 + gecode/optimize/c_api.cpp | 1361 +++++++++ gecode/optimize/c_api.h | 788 ++++++ gecode/optimize/constraints.cpp | 407 +++ gecode/optimize/constraints.hpp | 51 + gecode/optimize/diagnostics.cpp | 306 ++ gecode/optimize/diagnostics.hpp | 76 + gecode/optimize/flatzinc.cpp | 766 +++++ gecode/optimize/flatzinc.hpp | 76 + gecode/optimize/globals.cpp | 198 ++ gecode/optimize/globals.hpp | 47 + gecode/optimize/io.cpp | 590 ++++ gecode/optimize/lp_basis.cpp | 112 + gecode/optimize/lp_basis.hpp | 72 + gecode/optimize/lp_basis_detail.hpp | 15 + gecode/optimize/lp_evidence.cpp | 366 +++ gecode/optimize/lp_evidence.hpp | 145 + gecode/optimize/lp_observations.cpp | 477 ++++ gecode/optimize/lp_observations.hpp | 147 + gecode/optimize/lp_observations_detail.hpp | 39 + gecode/optimize/lp_sensitivity.cpp | 420 +++ gecode/optimize/lp_sensitivity.hpp | 163 ++ gecode/optimize/lp_sensitivity_backend.cpp | 98 + gecode/optimize/lp_sensitivity_backend.hpp | 22 + .../optimize/lp_sensitivity_highs_detail.hpp | 13 + gecode/optimize/model.cpp | 494 ++++ gecode/optimize/model.hpp | 244 ++ gecode/optimize/native.cpp | 2520 +++++++++++++++++ gecode/optimize/native.hpp | 112 + gecode/optimize/native_components.cpp | 149 + gecode/optimize/native_lp.hpp | 97 + gecode/optimize/native_neighborhoods.hpp | 81 + gecode/optimize/native_preprocess.hpp | 20 + gecode/optimize/native_presolve.cpp | 280 ++ gecode/optimize/native_regular_limits.hpp | 29 + gecode/optimize/native_search.hpp | 101 + gecode/optimize/native_symmetry.cpp | 82 + gecode/optimize/pool.cpp | 311 ++ gecode/optimize/pool.hpp | 65 + gecode/optimize/presolve.cpp | 356 +++ gecode/optimize/presolve.hpp | 88 + gecode/optimize/quadratic.cpp | 133 + gecode/optimize/quadratic.hpp | 100 + gecode/optimize/quadratic_bound.cpp | 120 + gecode/optimize/quadratic_bound.hpp | 41 + gecode/optimize/quadratic_solve.cpp | 383 +++ gecode/optimize/relaxation.cpp | 392 +++ gecode/optimize/relaxation.hpp | 95 + gecode/optimize/result.cpp | 221 ++ gecode/optimize/result.hpp | 168 ++ gecode/optimize/scenarios.cpp | 436 +++ gecode/optimize/scenarios.hpp | 108 + gecode/optimize/session.hpp | 66 + gecode/optimize/solve.cpp | 969 +++++++ gecode/optimize/solve.hpp | 61 + gecode/optimize/types.hpp | 36 + gecode/optimize/validate.cpp | 292 ++ gecode/optimize/validate.hpp | 59 + gecode/optimize/workflow.cpp | 337 +++ gecode/optimize/workflow.hpp | 74 + gecode/search.hh | 13 + gecode/search/options.hpp | 2 +- gecode/search/seq/bab.hpp | 5 +- gecode/search/seq/dfs.hpp | 5 +- python/gecode_optimize/__init__.py | 66 + python/gecode_optimize/_runtime.py | 20 + python/gecode_optimize/binding.py | 2298 +++++++++++++++ python/tests/test_bulk.py | 176 ++ python/tests/test_conformance.py | 438 +++ python/tests/test_lp_basis.py | 141 + python/tests/test_lp_evidence.py | 196 ++ python/tests/test_lp_observations.py | 175 ++ python/tests/test_lp_sensitivity.py | 204 ++ python/tests/test_native_starts.py | 90 + python/tests/test_quadratic.py | 245 ++ python/tests/test_regular.py | 129 + python/tests/test_runtime.py | 53 + python/tests/test_scenarios.py | 231 ++ python/tests/test_workflows.py | 291 ++ test/flatzinc-capture/capture.cpp | 147 + test/optimize/brancher_lifecycle.cpp | 95 + test/optimize/bulk.cpp | 178 ++ test/optimize/c_api.c | 677 +++++ test/optimize/constraints.cpp | 257 ++ test/optimize/consumer/CMakeLists.txt | 56 + test/optimize/consumer/main.cpp | 182 ++ test/optimize/cut_loop.cpp | 336 +++ test/optimize/cuts.cpp | 299 ++ test/optimize/diagnostics.cpp | 299 ++ .../flatzinc-fixtures/boolean-channel.fzn | 5 + .../cli-v1-constant-objective.fzn | 3 + .../flatzinc-fixtures/cli-v1-empty-output.fzn | 3 + .../cli-v1-global-distinct.fzn | 9 + .../cli-v1-global-element.fzn | 6 + .../cli-v1-global-repeated-alias.fzn | 6 + .../cli-v1-infeasible-alias.fzn | 4 + .../cli-v1-malformed-arity.fzn | 4 + .../flatzinc-fixtures/cli-v1-max-linear.fzn | 8 + .../cli-v1-reified-complement.fzn | 7 + .../cli-v1-reified-signed-alias.fzn | 8 + .../cli-v1-satisfy-hidden.fzn | 6 + .../cli-v1-unbounded-domain.fzn | 4 + .../cli-v1-unknown-predicate.fzn | 4 + .../cli-v2-circuit-empty.fzn | 3 + .../cli-v2-circuit-offset.fzn | 6 + .../cli-v2-circuit-singleton.fzn | 3 + .../cli-v2-circuit-subtours.fzn | 3 + .../cli-v2-cumulative-fixed-alias.fzn | 12 + .../cli-v2-cumulative-half-open.fzn | 5 + .../cli-v2-cumulative-malformed-four.fzn | 4 + .../cli-v2-cumulative-overlap-unsat.fzn | 4 + ...-v2-cumulative-positive-duration-unsat.fzn | 4 + .../cli-v2-cumulative-unfixed-parameter.fzn | 7 + .../cli-v2-cumulative-unsupported-seven.fzn | 4 + .../cli-v2-cumulative-unsupported-six.fzn | 4 + .../cli-v2-cumulative-wrong-arity.fzn | 4 + .../cli-v2-cumulative-zero-duration.fzn | 5 + .../cli-v2-domain-contiguous.fzn | 4 + .../flatzinc-fixtures/cli-v2-holey-alias.fzn | 7 + .../cli-v2-table-alias-unsat.fzn | 4 + .../flatzinc-fixtures/cli-v2-table-empty.fzn | 4 + .../flatzinc-fixtures/cli-v2-table-holes.fzn | 5 + .../cli-v2-table-zero-arity.fzn | 3 + .../cli-v3-regular-alias-repeat.fzn | 5 + .../cli-v3-regular-bad-count.fzn | 3 + .../cli-v3-regular-bad-initial.fzn | 3 + .../cli-v3-regular-bad-target.fzn | 3 + .../cli-v3-regular-dead-transition.fzn | 3 + .../cli-v3-regular-empty-accept.fzn | 4 + .../cli-v3-regular-empty-finals.fzn | 3 + .../cli-v3-regular-empty-reject.fzn | 3 + .../cli-v3-regular-final-interval.fzn | 4 + .../cli-v3-regular-malformed-matrix.fzn | 3 + .../cli-v3-regular-nonliteral-parameter.fzn | 5 + .../cli-v3-regular-nonunit-finals.fzn | 4 + .../cli-v3-regular-rejected-word.fzn | 3 + .../cli-v3-regular-unsupported-set.fzn | 3 + .../flatzinc-fixtures/cli-v3-regular-word.fzn | 5 + .../cli-v3-regular-wrong-arity.fzn | 3 + .../cli-v3-regular-zero-symbol.fzn | 3 + .../flatzinc-fixtures/linear-alias.fzn | 4 + .../flatzinc-fixtures/unsupported.fzn | 3 + test/optimize/flatzinc.cpp | 732 +++++ test/optimize/flatzinc_driver.cpp | 218 ++ test/optimize/flatzinc_driver_cli.py | 452 +++ test/optimize/globals.cpp | 196 ++ test/optimize/io.cpp | 269 ++ test/optimize/lp_backend.cpp | 123 + test/optimize/lp_basis.cpp | 170 ++ test/optimize/lp_basis_c_api.c | 83 + test/optimize/lp_basis_failure.cpp | 62 + test/optimize/lp_certificate.cpp | 179 ++ test/optimize/lp_evidence.cpp | 115 + test/optimize/lp_evidence_binding_cleanup.cpp | 52 + test/optimize/lp_evidence_c.c | 123 + test/optimize/lp_evidence_coordinator.cpp | 122 + test/optimize/lp_integer_backend.cpp | 87 + test/optimize/lp_integer_certificate.cpp | 157 + test/optimize/lp_integer_propagator.cpp | 197 ++ test/optimize/lp_observations.cpp | 159 ++ test/optimize/lp_observations_c.c | 119 + test/optimize/lp_observations_checks.cpp | 190 ++ test/optimize/lp_propagator.cpp | 307 ++ test/optimize/lp_reduced_cost.cpp | 281 ++ test/optimize/lp_sensitivity.cpp | 210 ++ .../lp_sensitivity_binding_cleanup.cpp | 55 + test/optimize/lp_sensitivity_c.c | 121 + test/optimize/lp_sensitivity_coordinator.cpp | 120 + test/optimize/lp_sensitivity_factor.cpp | 69 + test/optimize/lp_sensitivity_fixture.hpp | 24 + test/optimize/lp_sensitivity_oracle.hpp | 63 + test/optimize/lp_sparse_certificate.cpp | 165 ++ test/optimize/lp_sparse_storage.cpp | 118 + test/optimize/lp_strengthening.cpp | 219 ++ .../minizinc-fixtures/mzn-alias-holes.mzn | 1 + .../minizinc-fixtures/mzn-all-different.mzn | 1 + .../mzn-circuit-negative.mzn | 1 + .../minizinc-fixtures/mzn-circuit-offset.mzn | 1 + .../mzn-cumulative-fixed-alias.mzn | 1 + .../mzn-cumulative-half-open.mzn | 1 + .../mzn-cumulative-unsat.mzn | 1 + .../minizinc-fixtures/mzn-cumulative-zero.mzn | 1 + .../minizinc-fixtures/mzn-element-offset.mzn | 1 + .../minizinc-fixtures/mzn-linear-max.mzn | 1 + .../minizinc-fixtures/mzn-linear-min.mzn | 1 + .../minizinc-fixtures/mzn-native-controls.mzn | 11 + .../minizinc-fixtures/mzn-native-knapsack.mzn | 9 + .../minizinc-fixtures/mzn-regular-alias.mzn | 1 + .../mzn-regular-complement.mzn | 1 + .../minizinc-fixtures/mzn-regular-dead.mzn | 1 + .../mzn-regular-decomposed-reif.mzn | 1 + .../minizinc-fixtures/mzn-regular-empty.mzn | 1 + .../minizinc-fixtures/mzn-regular.mzn | 1 + .../minizinc-fixtures/mzn-reified-le.mzn | 1 + .../minizinc-fixtures/mzn-reject-float.mzn | 1 + .../minizinc-fixtures/mzn-reject-reif-eq.mzn | 1 + .../minizinc-fixtures/mzn-reject-search.mzn | 1 + .../minizinc-fixtures/mzn-reject-set.mzn | 1 + .../minizinc-fixtures/mzn-reject-times.mzn | 1 + .../mzn-reject-variable-cumulative.mzn | 1 + .../minizinc-fixtures/mzn-satisfy.mzn | 1 + .../minizinc-fixtures/mzn-table-alias.mzn | 1 + test/optimize/minizinc-fixtures/mzn-table.mzn | 1 + test/optimize/minizinc-fixtures/mzn-unsat.mzn | 1 + test/optimize/minizinc_configure.py | 57 + test/optimize/minizinc_registration.py | 469 +++ test/optimize/model.cpp | 250 ++ test/optimize/native.cpp | 273 ++ test/optimize/native_auto.cpp | 211 ++ test/optimize/native_branching.cpp | 261 ++ test/optimize/native_components.cpp | 115 + test/optimize/native_knapsack.cpp | 399 +++ test/optimize/native_lp.cpp | 345 +++ test/optimize/native_neighborhoods.cpp | 341 +++ test/optimize/native_presolve.cpp | 266 ++ test/optimize/native_race.cpp | 115 + test/optimize/native_search.cpp | 465 +++ test/optimize/native_starts.cpp | 315 +++ test/optimize/native_symmetry.cpp | 64 + .../package_origin/consumer/CMakeLists.txt | 27 + .../package_origin/producer/CMakeLists.txt | 33 + test/optimize/package_origin/run.py | 69 + test/optimize/pool.cpp | 333 +++ test/optimize/presolve.cpp | 273 ++ test/optimize/presolve_solve.cpp | 63 + test/optimize/process_containment.py | 349 +++ test/optimize/quadratic.cpp | 251 ++ test/optimize/quadratic_bound.cpp | 100 + test/optimize/regular.cpp | 239 ++ test/optimize/regular_c_api.c | 86 + test/optimize/relaxation.cpp | 459 +++ test/optimize/result.cpp | 287 ++ test/optimize/scenarios.cpp | 201 ++ test/optimize/scenarios_c.c | 148 + test/optimize/scenarios_coordinator.cpp | 149 + test/optimize/search_checkpoint.cpp | 176 ++ test/optimize/session.cpp | 217 ++ test/optimize/session_limits.cpp | 93 + test/optimize/solve.cpp | 206 ++ test/optimize/test_process_containment.py | 467 +++ test/optimize/validate.cpp | 336 +++ test/optimize/workflow.cpp | 434 +++ tools/flatzinc/configure-optimize-msc.cmake | 44 + tools/flatzinc/fzn-gecode-optimize.cpp | 422 +++ tools/flatzinc/gecode-optimize.msc.in | 37 + .../mznlib-optimize/fzn_all_different_int.mzn | 3 + .../flatzinc/mznlib-optimize/fzn_circuit.mzn | 8 + .../mznlib-optimize/fzn_cumulative.mzn | 7 + .../flatzinc/mznlib-optimize/fzn_regular.mzn | 5 + .../mznlib-optimize/fzn_table_int.mzn | 4 + .../mznlib-optimize/redefinitions.mzn | 16 + 284 files changed, 45226 insertions(+), 1140 deletions(-) create mode 100644 .github/workflows/optimize.yml create mode 100644 docs/optimize-io.md create mode 100644 docs/optimize.md create mode 100644 doxygen/optimize.hh create mode 100644 examples/optimize-file.cpp create mode 100644 examples/optimize-linear.cpp create mode 100644 gecode/flatzinc/capture-records.hh create mode 100644 gecode/flatzinc/capture.cpp create mode 100644 gecode/flatzinc/capture.hh create mode 100644 gecode/minimodel/lp-backend.hpp create mode 100644 gecode/minimodel/lp-certificate.hpp create mode 100644 gecode/minimodel/lp-cut-loop.hpp create mode 100644 gecode/minimodel/lp-cuts.hpp create mode 100644 gecode/minimodel/lp-model.hpp create mode 100644 gecode/minimodel/lp-relaxation.hpp create mode 100644 gecode/minimodel/lp-strengthening.hpp create mode 100644 gecode/optimize.hh create mode 100644 gecode/optimize/CMakeLists.txt create mode 100644 gecode/optimize/GecodeOptimizeConfig.cmake.in create mode 100644 gecode/optimize/c_api.cpp create mode 100644 gecode/optimize/c_api.h create mode 100644 gecode/optimize/constraints.cpp create mode 100644 gecode/optimize/constraints.hpp create mode 100644 gecode/optimize/diagnostics.cpp create mode 100644 gecode/optimize/diagnostics.hpp create mode 100644 gecode/optimize/flatzinc.cpp create mode 100644 gecode/optimize/flatzinc.hpp create mode 100644 gecode/optimize/globals.cpp create mode 100644 gecode/optimize/globals.hpp create mode 100644 gecode/optimize/io.cpp create mode 100644 gecode/optimize/lp_basis.cpp create mode 100644 gecode/optimize/lp_basis.hpp create mode 100644 gecode/optimize/lp_basis_detail.hpp create mode 100644 gecode/optimize/lp_evidence.cpp create mode 100644 gecode/optimize/lp_evidence.hpp create mode 100644 gecode/optimize/lp_observations.cpp create mode 100644 gecode/optimize/lp_observations.hpp create mode 100644 gecode/optimize/lp_observations_detail.hpp create mode 100644 gecode/optimize/lp_sensitivity.cpp create mode 100644 gecode/optimize/lp_sensitivity.hpp create mode 100644 gecode/optimize/lp_sensitivity_backend.cpp create mode 100644 gecode/optimize/lp_sensitivity_backend.hpp create mode 100644 gecode/optimize/lp_sensitivity_highs_detail.hpp create mode 100644 gecode/optimize/model.cpp create mode 100644 gecode/optimize/model.hpp create mode 100644 gecode/optimize/native.cpp create mode 100644 gecode/optimize/native.hpp create mode 100644 gecode/optimize/native_components.cpp create mode 100644 gecode/optimize/native_lp.hpp create mode 100644 gecode/optimize/native_neighborhoods.hpp create mode 100644 gecode/optimize/native_preprocess.hpp create mode 100644 gecode/optimize/native_presolve.cpp create mode 100644 gecode/optimize/native_regular_limits.hpp create mode 100644 gecode/optimize/native_search.hpp create mode 100644 gecode/optimize/native_symmetry.cpp create mode 100644 gecode/optimize/pool.cpp create mode 100644 gecode/optimize/pool.hpp create mode 100644 gecode/optimize/presolve.cpp create mode 100644 gecode/optimize/presolve.hpp create mode 100644 gecode/optimize/quadratic.cpp create mode 100644 gecode/optimize/quadratic.hpp create mode 100644 gecode/optimize/quadratic_bound.cpp create mode 100644 gecode/optimize/quadratic_bound.hpp create mode 100644 gecode/optimize/quadratic_solve.cpp create mode 100644 gecode/optimize/relaxation.cpp create mode 100644 gecode/optimize/relaxation.hpp create mode 100644 gecode/optimize/result.cpp create mode 100644 gecode/optimize/result.hpp create mode 100644 gecode/optimize/scenarios.cpp create mode 100644 gecode/optimize/scenarios.hpp create mode 100644 gecode/optimize/session.hpp create mode 100644 gecode/optimize/solve.cpp create mode 100644 gecode/optimize/solve.hpp create mode 100644 gecode/optimize/types.hpp create mode 100644 gecode/optimize/validate.cpp create mode 100644 gecode/optimize/validate.hpp create mode 100644 gecode/optimize/workflow.cpp create mode 100644 gecode/optimize/workflow.hpp create mode 100644 python/gecode_optimize/__init__.py create mode 100644 python/gecode_optimize/_runtime.py create mode 100644 python/gecode_optimize/binding.py create mode 100644 python/tests/test_bulk.py create mode 100644 python/tests/test_conformance.py create mode 100644 python/tests/test_lp_basis.py create mode 100644 python/tests/test_lp_evidence.py create mode 100644 python/tests/test_lp_observations.py create mode 100644 python/tests/test_lp_sensitivity.py create mode 100644 python/tests/test_native_starts.py create mode 100644 python/tests/test_quadratic.py create mode 100644 python/tests/test_regular.py create mode 100644 python/tests/test_runtime.py create mode 100644 python/tests/test_scenarios.py create mode 100644 python/tests/test_workflows.py create mode 100644 test/flatzinc-capture/capture.cpp create mode 100644 test/optimize/brancher_lifecycle.cpp create mode 100644 test/optimize/bulk.cpp create mode 100644 test/optimize/c_api.c create mode 100644 test/optimize/constraints.cpp create mode 100644 test/optimize/consumer/CMakeLists.txt create mode 100644 test/optimize/consumer/main.cpp create mode 100644 test/optimize/cut_loop.cpp create mode 100644 test/optimize/cuts.cpp create mode 100644 test/optimize/diagnostics.cpp create mode 100644 test/optimize/flatzinc-fixtures/boolean-channel.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v1-constant-objective.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v1-empty-output.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v1-global-distinct.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v1-global-element.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v1-global-repeated-alias.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v1-infeasible-alias.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v1-malformed-arity.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v1-max-linear.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v1-reified-complement.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v1-reified-signed-alias.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v1-satisfy-hidden.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v1-unbounded-domain.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v1-unknown-predicate.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-circuit-empty.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-circuit-offset.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-circuit-singleton.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-circuit-subtours.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-cumulative-fixed-alias.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-cumulative-half-open.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-cumulative-malformed-four.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-cumulative-overlap-unsat.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-cumulative-positive-duration-unsat.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-cumulative-unfixed-parameter.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-cumulative-unsupported-seven.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-cumulative-unsupported-six.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-cumulative-wrong-arity.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-cumulative-zero-duration.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-domain-contiguous.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-holey-alias.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-table-alias-unsat.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-table-empty.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-table-holes.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v2-table-zero-arity.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-alias-repeat.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-bad-count.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-bad-initial.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-bad-target.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-dead-transition.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-empty-accept.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-empty-finals.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-empty-reject.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-final-interval.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-malformed-matrix.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-nonliteral-parameter.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-nonunit-finals.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-rejected-word.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-unsupported-set.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-word.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-wrong-arity.fzn create mode 100644 test/optimize/flatzinc-fixtures/cli-v3-regular-zero-symbol.fzn create mode 100644 test/optimize/flatzinc-fixtures/linear-alias.fzn create mode 100644 test/optimize/flatzinc-fixtures/unsupported.fzn create mode 100644 test/optimize/flatzinc.cpp create mode 100644 test/optimize/flatzinc_driver.cpp create mode 100644 test/optimize/flatzinc_driver_cli.py create mode 100644 test/optimize/globals.cpp create mode 100644 test/optimize/io.cpp create mode 100644 test/optimize/lp_backend.cpp create mode 100644 test/optimize/lp_basis.cpp create mode 100644 test/optimize/lp_basis_c_api.c create mode 100644 test/optimize/lp_basis_failure.cpp create mode 100644 test/optimize/lp_certificate.cpp create mode 100644 test/optimize/lp_evidence.cpp create mode 100644 test/optimize/lp_evidence_binding_cleanup.cpp create mode 100644 test/optimize/lp_evidence_c.c create mode 100644 test/optimize/lp_evidence_coordinator.cpp create mode 100644 test/optimize/lp_integer_backend.cpp create mode 100644 test/optimize/lp_integer_certificate.cpp create mode 100644 test/optimize/lp_integer_propagator.cpp create mode 100644 test/optimize/lp_observations.cpp create mode 100644 test/optimize/lp_observations_c.c create mode 100644 test/optimize/lp_observations_checks.cpp create mode 100644 test/optimize/lp_propagator.cpp create mode 100644 test/optimize/lp_reduced_cost.cpp create mode 100644 test/optimize/lp_sensitivity.cpp create mode 100644 test/optimize/lp_sensitivity_binding_cleanup.cpp create mode 100644 test/optimize/lp_sensitivity_c.c create mode 100644 test/optimize/lp_sensitivity_coordinator.cpp create mode 100644 test/optimize/lp_sensitivity_factor.cpp create mode 100644 test/optimize/lp_sensitivity_fixture.hpp create mode 100644 test/optimize/lp_sensitivity_oracle.hpp create mode 100644 test/optimize/lp_sparse_certificate.cpp create mode 100644 test/optimize/lp_sparse_storage.cpp create mode 100644 test/optimize/lp_strengthening.cpp create mode 100644 test/optimize/minizinc-fixtures/mzn-alias-holes.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-all-different.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-circuit-negative.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-circuit-offset.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-cumulative-fixed-alias.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-cumulative-half-open.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-cumulative-unsat.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-cumulative-zero.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-element-offset.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-linear-max.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-linear-min.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-native-controls.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-native-knapsack.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-regular-alias.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-regular-complement.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-regular-dead.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-regular-decomposed-reif.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-regular-empty.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-regular.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-reified-le.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-reject-float.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-reject-reif-eq.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-reject-search.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-reject-set.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-reject-times.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-reject-variable-cumulative.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-satisfy.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-table-alias.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-table.mzn create mode 100644 test/optimize/minizinc-fixtures/mzn-unsat.mzn create mode 100644 test/optimize/minizinc_configure.py create mode 100644 test/optimize/minizinc_registration.py create mode 100644 test/optimize/model.cpp create mode 100644 test/optimize/native.cpp create mode 100644 test/optimize/native_auto.cpp create mode 100644 test/optimize/native_branching.cpp create mode 100644 test/optimize/native_components.cpp create mode 100644 test/optimize/native_knapsack.cpp create mode 100644 test/optimize/native_lp.cpp create mode 100644 test/optimize/native_neighborhoods.cpp create mode 100644 test/optimize/native_presolve.cpp create mode 100644 test/optimize/native_race.cpp create mode 100644 test/optimize/native_search.cpp create mode 100644 test/optimize/native_starts.cpp create mode 100644 test/optimize/native_symmetry.cpp create mode 100644 test/optimize/package_origin/consumer/CMakeLists.txt create mode 100644 test/optimize/package_origin/producer/CMakeLists.txt create mode 100644 test/optimize/package_origin/run.py create mode 100644 test/optimize/pool.cpp create mode 100644 test/optimize/presolve.cpp create mode 100644 test/optimize/presolve_solve.cpp create mode 100644 test/optimize/process_containment.py create mode 100644 test/optimize/quadratic.cpp create mode 100644 test/optimize/quadratic_bound.cpp create mode 100644 test/optimize/regular.cpp create mode 100644 test/optimize/regular_c_api.c create mode 100644 test/optimize/relaxation.cpp create mode 100644 test/optimize/result.cpp create mode 100644 test/optimize/scenarios.cpp create mode 100644 test/optimize/scenarios_c.c create mode 100644 test/optimize/scenarios_coordinator.cpp create mode 100644 test/optimize/search_checkpoint.cpp create mode 100644 test/optimize/session.cpp create mode 100644 test/optimize/session_limits.cpp create mode 100644 test/optimize/solve.cpp create mode 100644 test/optimize/test_process_containment.py create mode 100644 test/optimize/validate.cpp create mode 100644 test/optimize/workflow.cpp create mode 100644 tools/flatzinc/configure-optimize-msc.cmake create mode 100644 tools/flatzinc/fzn-gecode-optimize.cpp create mode 100644 tools/flatzinc/gecode-optimize.msc.in create mode 100644 tools/flatzinc/mznlib-optimize/fzn_all_different_int.mzn create mode 100644 tools/flatzinc/mznlib-optimize/fzn_circuit.mzn create mode 100644 tools/flatzinc/mznlib-optimize/fzn_cumulative.mzn create mode 100644 tools/flatzinc/mznlib-optimize/fzn_regular.mzn create mode 100644 tools/flatzinc/mznlib-optimize/fzn_table_int.mzn create mode 100644 tools/flatzinc/mznlib-optimize/redefinitions.mzn diff --git a/.github/workflows/optimize.yml b/.github/workflows/optimize.yml new file mode 100644 index 0000000000..8430554cd0 --- /dev/null +++ b/.github/workflows/optimize.yml @@ -0,0 +1,344 @@ +name: Optimization component + +on: + pull_request: + push: + branches: + - main + - 'release/**' + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + core: + name: Core / ${{ matrix.os }} / shared=${{ matrix.shared }} + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + max-parallel: 2 + matrix: + os: [ubuntu-24.04, macos-15, windows-2022] + shared: ['OFF', 'ON'] + include: + - os: windows-2022 + generator: Visual Studio 17 2022 + - os: ubuntu-24.04 + generator: Unix Makefiles + - os: macos-15 + generator: Unix Makefiles + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + persist-credentials: false + - name: Configure standalone core + run: >- + cmake -S gecode/optimize -B build -G "${{ matrix.generator }}" + -DCMAKE_BUILD_TYPE=Release + -DBUILD_SHARED_LIBS=${{ matrix.shared }} + -DGECODE_OPTIMIZE_WITH_HIGHS=OFF + -DGECODE_OPTIMIZE_BUILD_TESTS=ON + -DBUILD_TESTING=ON + - name: Build + run: cmake --build build --config Release --parallel 2 + - name: Run core conformance tests + run: | + export PATH="$(pwd)/build/Release:$(pwd)/build/Release/bin:$(pwd)/build/bin/Release:$(pwd)/build/highs/bin/Release:$(pwd)/build/highs/bin:$(pwd)/build/bin:$(pwd)/build:$PATH" + ctest --test-dir build -C Release --output-on-failure \ + --no-tests=error --timeout 120 --parallel 2 -R '^optimize-' + - name: Install + run: cmake --install build --config Release --prefix "$(pwd)/install" + - name: Configure installed consumer + run: >- + cmake -S test/optimize/consumer -B consumer -G "${{ matrix.generator }}" + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_PREFIX_PATH="$(pwd)/install" + -DPACKAGE_MODE=standalone + -DEXPECT_BACKEND_DISABLED=ON + - name: Build installed consumer + run: cmake --build consumer --config Release --parallel 2 + - name: Run installed consumer, including Windows DLL loading + run: | + export PATH="$(pwd)/install/bin:$PATH" + ctest --test-dir consumer -C Release --output-on-failure \ + --no-tests=error --timeout 60 + + native: + name: Native combined / ${{ matrix.os }} / shared=${{ matrix.shared }} + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + strategy: + fail-fast: false + max-parallel: 2 + matrix: + include: + - os: ubuntu-24.04 + generator: Unix Makefiles + shared: 'OFF' + static: 'ON' + python: python3 + - os: windows-2022 + generator: Visual Studio 17 2022 + shared: 'ON' + static: 'OFF' + python: python + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + persist-credentials: false + - name: Check package ownership and standalone native boundary + run: >- + ${{ matrix.python }} test/optimize/package_origin/run.py + --generator "${{ matrix.generator }}" + --check-native-boundary + - name: Configure combined native bridge without HiGHS + # The bridge must compile against this checkout's native int/search + # components. Standalone native configuration is explicitly unsupported. + run: >- + cmake -S . -B build-native -G "${{ matrix.generator }}" + -DCMAKE_BUILD_TYPE=Release + -DGECODE_BUILD_SHARED=${{ matrix.shared }} + -DGECODE_BUILD_STATIC=${{ matrix.static }} + -DGECODE_ENABLE_OPTIMIZE=ON + -DGECODE_OPTIMIZE_WITH_NATIVE=ON + -DGECODE_OPTIMIZE_WITH_HIGHS=OFF + -DGECODE_OPTIMIZE_BUILD_TESTS=ON + -DBUILD_TESTING=ON + -DGECODE_ENABLE_QT=OFF + -DGECODE_ENABLE_GIST=OFF + -DGECODE_ENABLE_MPFR=OFF + -DGECODE_ENABLE_SET_VARS=ON + -DGECODE_ENABLE_FLOAT_VARS=ON + -DGECODE_ENABLE_MINIMODEL=ON + -DGECODE_ENABLE_DRIVER=ON + -DGECODE_ENABLE_FLATZINC=ON + -DGECODE_ENABLE_EXAMPLES=OFF + - name: Build native bridge and conformance tests + run: cmake --build build-native --config Release --parallel 2 + - name: Exercise enabled native bridge and core conformance + run: | + export PATH="$(pwd)/build-native/gecode/optimize/Release:$(pwd)/build-native/gecode/optimize:$(pwd)/build-native/bin/Release:$(pwd)/build-native/bin:$(pwd)/build-native/Release:$(pwd)/build-native:$PATH" + ctest --test-dir build-native -C Release --output-on-failure \ + --no-tests=error --timeout 120 --parallel 2 -R '^(optimize-|gecode-flatzinc-capture$)' + - name: Install combined native package + run: cmake --install build-native --config Release --prefix "$(pwd)/install-native" + - name: Configure installed combined bridge consumer + # CHECK_NATIVE in this consumer explicitly requests Backend::Native + # and Guarantee::Exact, independently of the absent HiGHS/Auto backend. + run: >- + cmake -S test/optimize/consumer -B consumer-native -G "${{ matrix.generator }}" + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_PREFIX_PATH="$(pwd)/install-native" + -DPACKAGE_MODE=combined + -DEXPECT_BACKEND_DISABLED=ON + - name: Build and run installed bridge consumer + run: | + cmake --build consumer-native --config Release --parallel 2 + export PATH="$(pwd)/install-native/bin:$PATH" + ctest --test-dir consumer-native -C Release --output-on-failure \ + --no-tests=error --timeout 60 + - name: Configure independent native-only consumer + run: >- + cmake -S test/optimize/consumer -B consumer-native-only -G "${{ matrix.generator }}" + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_PREFIX_PATH="$(pwd)/install-native" + -DPACKAGE_MODE=native-only + - name: Build and run native-only consumer without HiGHS discovery + run: | + cmake --build consumer-native-only --config Release --parallel 2 + export PATH="$(pwd)/install-native/bin:$PATH" + ctest --test-dir consumer-native-only -C Release --output-on-failure \ + --no-tests=error --timeout 60 + + numerical: + name: HiGHS / ${{ matrix.os }} / shared=${{ matrix.shared }} + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + strategy: + fail-fast: false + max-parallel: 2 + matrix: + include: + - os: ubuntu-24.04 + generator: Unix Makefiles + shared: 'OFF' + - os: macos-15 + generator: Unix Makefiles + shared: 'OFF' + - os: windows-2022 + generator: Visual Studio 17 2022 + shared: 'ON' + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + persist-credentials: false + - name: Check out pinned HiGHS source + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + repository: ERGO-Code/HiGHS + ref: 04024d701f79feb8e2f18bc3df0dffc04ef05088 + path: dependencies/highs + persist-credentials: false + - name: Configure standalone numerical component + # CMake receives local source explicitly; it never fetches dependencies. + run: >- + cmake -S gecode/optimize -B build -G "${{ matrix.generator }}" + -DCMAKE_BUILD_TYPE=Release + -DBUILD_SHARED_LIBS=${{ matrix.shared }} + -DGECODE_OPTIMIZE_WITH_HIGHS=ON + -DGECODE_OPTIMIZE_HIGHS_SOURCE="$(pwd)/dependencies/highs" + -DGECODE_OPTIMIZE_BUILD_TESTS=ON + -DBUILD_TESTING=ON + -DBUILD_SHARED_EXTRAS_LIB=OFF + -DBUILD_CXX_EXE=OFF + -DZLIB=OFF + - name: Build component and pinned backend + run: cmake --build build --config Release --parallel 2 + - name: Run numerical and original-model/parser conformance tests + run: | + export PATH="$(pwd)/build/Release:$(pwd)/build/Release/bin:$(pwd)/build/bin/Release:$(pwd)/build/highs/bin/Release:$(pwd)/build/highs/bin:$(pwd)/build/bin:$(pwd)/build:$PATH" + ctest --test-dir build -C Release --output-on-failure \ + --no-tests=error --timeout 120 --parallel 2 -R '^optimize-' + - name: Install component and backend + run: cmake --install build --config Release --prefix "$(pwd)/install" + - name: Configure installed numerical consumer + run: >- + cmake -S test/optimize/consumer -B consumer -G "${{ matrix.generator }}" + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_PREFIX_PATH="$(pwd)/install" + -DPACKAGE_MODE=standalone + -DEXPECT_BACKEND_DISABLED=OFF + - name: Build installed numerical consumer + run: cmake --build consumer --config Release --parallel 2 + - name: Run installed numerical consumer, including Windows DLL loading + run: | + export PATH="$(pwd)/install/bin:$PATH" + ctest --test-dir consumer -C Release --output-on-failure \ + --no-tests=error --timeout 60 + + sanitizers: + name: Full-source ASan and UBSan + runs-on: ubuntu-24.04 + timeout-minutes: 60 + env: + CC: clang + CXX: clang++ + ASAN_OPTIONS: detect_leaks=1:halt_on_error=1 + UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + persist-credentials: false + - name: Check out pinned HiGHS source + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + repository: ERGO-Code/HiGHS + ref: 04024d701f79feb8e2f18bc3df0dffc04ef05088 + path: dependencies/highs + persist-credentials: false + - name: Configure all source with one sanitizer toolchain + # Instrument C and C++ in both the adapter and HiGHS. Linking an + # uninstrumented installed backend can mix incompatible vector annotations. + run: >- + cmake -S gecode/optimize -B build -G "Unix Makefiles" + -DCMAKE_BUILD_TYPE=Debug + -DBUILD_SHARED_LIBS=OFF + -DGECODE_OPTIMIZE_WITH_HIGHS=ON + -DGECODE_OPTIMIZE_HIGHS_SOURCE="$(pwd)/dependencies/highs" + -DGECODE_OPTIMIZE_BUILD_TESTS=ON + -DBUILD_TESTING=ON + -DBUILD_SHARED_EXTRAS_LIB=OFF + -DBUILD_CXX_EXE=OFF + -DZLIB=OFF + -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" + -DGECODE_OPTIMIZE_TEST_PYTHON=OFF + -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" + -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" + -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address,undefined" + - name: Build instrumented component and backend + run: cmake --build build --config Debug --parallel 2 + - name: Run instrumented conformance tests + run: >- + ctest --test-dir build -C Debug --output-on-failure + --no-tests=error --timeout 120 --parallel 2 -R '^optimize-' + + native-sanitizers: + name: Full native and HiGHS hybrid ASan and UBSan + runs-on: ubuntu-24.04 + timeout-minutes: 60 + env: + CC: clang + CXX: clang++ + ASAN_OPTIONS: detect_leaks=1:halt_on_error=1 + UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + persist-credentials: false + - name: Check out pinned HiGHS source for the hybrid + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + repository: ERGO-Code/HiGHS + ref: 04024d701f79feb8e2f18bc3df0dffc04ef05088 + path: dependencies/highs + persist-credentials: false + - name: Configure fully instrumented native kernel, HiGHS and optimization facade + run: >- + cmake -S . -B build-native-sanitizers -G "Unix Makefiles" + -DCMAKE_BUILD_TYPE=Debug + -DGECODE_BUILD_SHARED=OFF -DGECODE_BUILD_STATIC=ON + -DGECODE_ENABLE_OPTIMIZE=ON -DGECODE_OPTIMIZE_WITH_NATIVE=ON + -DGECODE_OPTIMIZE_WITH_HIGHS=ON -DGECODE_OPTIMIZE_BUILD_TESTS=ON + -DGECODE_OPTIMIZE_HIGHS_SOURCE="$(pwd)/dependencies/highs" + -DBUILD_SHARED_EXTRAS_LIB=OFF -DBUILD_CXX_EXE=OFF -DZLIB=OFF + -DGECODE_OPTIMIZE_TEST_PYTHON=OFF -DBUILD_TESTING=ON + -DGECODE_ENABLE_QT=OFF -DGECODE_ENABLE_GIST=OFF + -DGECODE_ENABLE_MPFR=OFF -DGECODE_ENABLE_SET_VARS=ON + -DGECODE_ENABLE_FLOAT_VARS=ON -DGECODE_ENABLE_MINIMODEL=ON + -DGECODE_ENABLE_DRIVER=ON -DGECODE_ENABLE_FLATZINC=ON + -DGECODE_ENABLE_EXAMPLES=OFF + -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" + -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" + -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" + -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address,undefined" + - name: Build native lifecycle and boundary tests + run: >- + cmake --build build-native-sanitizers --parallel 2 --target + optimize-brancher-lifecycle-test optimize-native-test + optimize-search-checkpoint-test + optimize-lp-certificate-test optimize-lp-integer-certificate-test + optimize-lp-sparse-certificate-test optimize-lp-strengthening-test + optimize-lp-backend-test optimize-lp-integer-backend-test + optimize-lp-propagator-test optimize-lp-integer-propagator-test + optimize-lp-sparse-storage-test optimize-lp-reduced-cost-test + optimize-native_auto-test optimize-native_race-test + optimize-native_knapsack-test optimize-native_presolve-test + optimize-native_components-test optimize-native_symmetry-test + optimize-native_lp-test optimize-pool-test optimize-pool-coordinator-test + optimize-native_search-test optimize-native-search-coordinator-test optimize-native-lp-coordinator-test + optimize-native_branching-test optimize-native-branching-coordinator-test + optimize-native_starts-test optimize-native-start-coordinator-test + optimize-native_neighborhoods-test optimize-native-neighborhood-coordinator-test + optimize-lp_observations-test optimize-lp_observations_checks-test optimize-lp-observations-c-test + optimize-lp_basis-test optimize-lp-basis-failure-test optimize-lp-basis-c-api-test + optimize-regular-test optimize-regular-c-api-test + optimize-scenarios-test optimize-scenarios-coordinator-test + optimize-scenarios-c-test optimize-lp_evidence-test optimize-lp-evidence-coordinator-test + optimize-lp-evidence-c-test optimize-lp-evidence-binding-cleanup-test + optimize-lp_sensitivity-test optimize-lp-sensitivity-coordinator-test optimize-lp-sensitivity-factor-test + optimize-lp-sensitivity-c-test optimize-lp-sensitivity-binding-cleanup-test + optimize-quadratic-test optimize-quadratic_bound-test + optimize-quadratic-coordinator-test optimize-quadratic-arithmetic-rejection-test + gecode-flatzinc-capture-test fzn-gecode-optimize optimize-flatzinc-driver-test optimize-flatzinc-test + optimize-presolve-test optimize-presolve_solve-test optimize-cut-proofs-test optimize-cut-loop-test + optimize-globals-test optimize-c-api-test + - name: Check native engine, checked LP, pools and C ownership with sanitizers + run: >- + ctest --test-dir build-native-sanitizers --output-on-failure + --no-tests=error --timeout 120 + -R '^(gecode-flatzinc-capture|optimize-(flatzinc|flatzinc-driver|flatzinc-driver-cli|minizinc-configure|brancher-lifecycle|search-checkpoint|lp-certificate|lp-integer-certificate|lp-sparse-certificate|lp-strengthening|lp-backend|lp-integer-backend|lp-propagator|lp-integer-propagator|lp-sparse-storage|lp-reduced-cost|native|native_auto|native_race|native_knapsack|native_presolve|native_components|native_symmetry|native_lp|native_search|native_branching|native_starts|native-start-coordinator|native_neighborhoods|native-neighborhood-coordinator|lp_observations|lp_observations_checks|lp-observations-c|lp_basis|lp-basis-failure|lp-basis-c-api|lp_evidence|lp-evidence-coordinator|lp-evidence-c|lp-evidence-binding-cleanup|lp_sensitivity|lp-sensitivity-coordinator|lp-sensitivity-factor|lp-sensitivity-c|lp-sensitivity-binding-cleanup|regular|regular-c-api|scenarios|scenarios-coordinator|scenarios-c|native-search-coordinator|native-lp-coordinator|native-branching-coordinator|quadratic|quadratic_bound|quadratic-coordinator|quadratic-arithmetic-rejection|pool|pool-coordinator|presolve|presolve_solve|cut-proofs|cut-loop|globals|c-api))$' diff --git a/CMakeLists.txt b/CMakeLists.txt index e9d0600f6b..f6ecdd55a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -118,6 +118,8 @@ option(GECODE_ENABLE_INT_VARS "Build int variables module" ON) option(GECODE_ENABLE_SET_VARS "Build set variables module" ON) option(GECODE_ENABLE_FLOAT_VARS "Build float variables module" ON) option(GECODE_ENABLE_MINIMODEL "Build minimodel module" ON) +option(GECODE_ENABLE_LP_RELAXATION "Enable certified LP relaxation module (requires HiGHS)" OFF) +option(GECODE_ENABLE_OPTIMIZE "Build additive numerical optimization API" OFF) option(GECODE_ENABLE_DRIVER "Build driver module" ON) option(GECODE_ENABLE_FLATZINC "Build FlatZinc module" ON) @@ -241,6 +243,15 @@ if(GECODE_SANITIZER_NORMALIZED) endif() # Keep dependency closure behavior similar to configure. +if(GECODE_ENABLE_LP_RELAXATION) + gecode_force_option_on(GECODE_ENABLE_MINIMODEL "LP relaxation requires minimodel") + gecode_force_option_on(GECODE_ENABLE_SEARCH "LP relaxation requires search") + gecode_force_option_on(GECODE_ENABLE_INT_VARS "LP relaxation requires int variables") + find_package(highs 1.15 CONFIG REQUIRED) + if(NOT TARGET highs::highs) + message(FATAL_ERROR "LP relaxation requires the highs::highs CMake target") + endif() +endif() if(GECODE_ENABLE_SET_VARS) gecode_force_option_on(GECODE_ENABLE_INT_VARS "Set variables require int variables") endif() @@ -1068,6 +1079,7 @@ if(DOXYGEN_FOUND AND GECODE_UV_EXECUTABLE) -P ${GECODE_DOXYGEN_COMPAT_ALIASES} DEPENDS gecode-varimp-gen ${CMAKE_CURRENT_BINARY_DIR}/doxygen.hh + ${CMAKE_CURRENT_SOURCE_DIR}/doxygen/optimize.hh ${CMAKE_CURRENT_BINARY_DIR}/doxygen.conf.use ${CMAKE_CURRENT_BINARY_DIR}/header.html ${CMAKE_CURRENT_SOURCE_DIR}/misc/doxygen/footer.html @@ -1326,9 +1338,27 @@ if(GECODE_ENABLE_FLATZINC) endif() endif() +# The LP module is header-only. Keep its external dependency off the native +# minimodel target so applications can select the additional functionality. +if(GECODE_ENABLE_LP_RELAXATION) + add_library(gecodelp INTERFACE) + target_link_libraries(gecodelp INTERFACE gecodeminimodel highs::highs) + add_library(Gecode::gecodelp ALIAS gecodelp) + list(APPEND GECODE_LIBRARY_COMPONENTS lp) + list(APPEND GECODE_INSTALL_TARGETS gecodelp) + list(APPEND GECODE_EXPORT_TARGETS gecodelp) +endif() + +if(GECODE_ENABLE_OPTIMIZE) + add_subdirectory(gecode/optimize) + list(APPEND GECODE_LIBRARY_COMPONENTS optimize) + list(APPEND GECODE_INSTALL_TARGETS gecodeoptimize gecodeoptimize_c) + list(APPEND GECODE_EXPORT_TARGETS gecodeoptimize gecodeoptimize_c) +endif() + # Compatibility aggregate target for downstream projects expecting Gecode::gecode. add_library(gecode INTERFACE) -foreach(component IN ITEMS support kernel search int set float minimodel driver flatzinc gist) +foreach(component IN ITEMS support kernel search int set float minimodel driver flatzinc gist lp optimize) if(TARGET gecode${component}) target_link_libraries(gecode INTERFACE gecode${component}) endif() @@ -1351,6 +1381,64 @@ if(GECODE_ENABLE_FLATZINC) INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}") endif() list(APPEND GECODE_INSTALL_TARGETS fzn-gecode) + if(TARGET gecodeoptimize) + # Separate executable; experimental registration is explicitly opt-in below. + add_executable(fzn-gecode-optimize tools/flatzinc/fzn-gecode-optimize.cpp) + target_link_libraries(fzn-gecode-optimize PRIVATE gecodeflatzinc gecodeoptimize) + if(APPLE) + set_target_properties(fzn-gecode-optimize PROPERTIES + INSTALL_RPATH "@loader_path/../${CMAKE_INSTALL_LIBDIR}") + elseif(UNIX) + set_target_properties(fzn-gecode-optimize PROPERTIES + INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}") + endif() + list(APPEND GECODE_INSTALL_TARGETS fzn-gecode-optimize) + endif() +endif() + +option(GECODE_OPTIMIZE_MINIZINC_REGISTRATION "Build/install the explicit experimental MiniZinc solver registration" OFF) +set(GECODE_OPTIMIZE_MINIZINC_EXECUTABLE "" CACHE FILEPATH "Optional pinned MiniZinc compiler for experimental registration tests (no download)") +if(GECODE_OPTIMIZE_MINIZINC_REGISTRATION) + if(NOT TARGET fzn-gecode-optimize OR NOT GECODE_OPTIMIZE_WITH_NATIVE) + message(FATAL_ERROR "Experimental MiniZinc registration requires the optimization FlatZinc driver and native backend") + endif() + if(IS_ABSOLUTE "${CMAKE_INSTALL_BINDIR}" OR IS_ABSOLUTE "${CMAKE_INSTALL_DATADIR}") + message(FATAL_ERROR "Relocatable experimental MiniZinc registration requires relative CMAKE_INSTALL_BINDIR and CMAKE_INSTALL_DATADIR") + endif() + set(_gecode_optimize_msc_template "${PROJECT_SOURCE_DIR}/tools/flatzinc/gecode-optimize.msc.in") + set(_gecode_optimize_msc_encoder "${PROJECT_SOURCE_DIR}/tools/flatzinc/configure-optimize-msc.cmake") + # execute_process below generates the install registration at configure time. + # Keep it in sync when solver flags or JSON encoding change between builds. + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + "${_gecode_optimize_msc_template}" "${_gecode_optimize_msc_encoder}") + set(_gecode_optimize_build_msc "${PROJECT_BINARY_DIR}/minizinc/$/gecode-optimize.msc") + # Target paths are expanded before the encoder escapes them as JSON. This + # also supports multi-configuration builds and source/build paths with quotes. + add_custom_target(gecode-optimize-minizinc-config ALL + COMMAND "${CMAKE_COMMAND}" + "-DTEMPLATE=${_gecode_optimize_msc_template}" "-DVERSION=${GECODE_VERSION}" + "-DDRIVER=$" + "-DMZNLIB=${PROJECT_SOURCE_DIR}/tools/flatzinc/mznlib-optimize" + "-DOUTPUT=${_gecode_optimize_build_msc}" -P "${_gecode_optimize_msc_encoder}" + DEPENDS fzn-gecode-optimize "${_gecode_optimize_msc_template}" "${_gecode_optimize_msc_encoder}" + VERBATIM) + set(_gecode_optimize_msc_destination "${CMAKE_INSTALL_DATADIR}/minizinc/solvers") + file(RELATIVE_PATH _gecode_optimize_installed_driver "/${_gecode_optimize_msc_destination}" + "/${CMAKE_INSTALL_BINDIR}/fzn-gecode-optimize${CMAKE_EXECUTABLE_SUFFIX}") + file(RELATIVE_PATH _gecode_optimize_installed_library "/${_gecode_optimize_msc_destination}" + "/${CMAKE_INSTALL_DATADIR}/minizinc/gecode-optimize-experimental") + set(_gecode_optimize_install_msc "${PROJECT_BINARY_DIR}/minizinc/install/gecode-optimize.msc") + execute_process(COMMAND "${CMAKE_COMMAND}" + "-DTEMPLATE=${_gecode_optimize_msc_template}" "-DVERSION=${GECODE_VERSION}" + "-DDRIVER=${_gecode_optimize_installed_driver}" "-DMZNLIB=${_gecode_optimize_installed_library}" + "-DOUTPUT=${_gecode_optimize_install_msc}" -P "${_gecode_optimize_msc_encoder}" + COMMAND_ERROR_IS_FATAL ANY) + if(GECODE_INSTALL) + install(FILES "${_gecode_optimize_install_msc}" DESTINATION "${_gecode_optimize_msc_destination}") + install(DIRECTORY tools/flatzinc/mznlib-optimize/ + DESTINATION "${CMAKE_INSTALL_DATADIR}/minizinc/gecode-optimize-experimental" + FILES_MATCHING PATTERN "*.mzn") + endif() endif() if(BUILD_TESTING) @@ -1372,6 +1460,61 @@ if(BUILD_TESTING) endif() if(GECODE_ENABLE_FLATZINC) + if(NOT TARGET Threads::Threads) + find_package(Threads REQUIRED) + endif() + add_executable(gecode-flatzinc-capture-test test/flatzinc-capture/capture.cpp) + target_compile_features(gecode-flatzinc-capture-test PRIVATE cxx_std_17) + target_link_libraries(gecode-flatzinc-capture-test PRIVATE gecodeflatzinc Threads::Threads) + if(MSVC) + target_compile_options(gecode-flatzinc-capture-test PRIVATE /UNDEBUG) + else() + target_compile_options(gecode-flatzinc-capture-test PRIVATE -UNDEBUG) + endif() + add_test(NAME gecode-flatzinc-capture COMMAND gecode-flatzinc-capture-test) + set_tests_properties(gecode-flatzinc-capture PROPERTIES TIMEOUT 120) + if(TARGET gecodeoptimize AND GECODE_OPTIMIZE_BUILD_TESTS) + add_executable(optimize-flatzinc-driver-test test/optimize/flatzinc_driver.cpp) + target_link_libraries(optimize-flatzinc-driver-test PRIVATE gecodeflatzinc gecodeoptimize) + if(GECODE_OPTIMIZE_WITH_NATIVE OR GECODE_OPTIMIZE_WITH_HIGHS) + target_compile_definitions(optimize-flatzinc-driver-test PRIVATE GECODE_FLATZINC_DRIVER_EXPECT_BACKEND=1) + endif() + if(MSVC) + target_compile_options(optimize-flatzinc-driver-test PRIVATE /UNDEBUG) + else() + target_compile_options(optimize-flatzinc-driver-test PRIVATE -UNDEBUG) + endif() + add_test(NAME optimize-flatzinc-driver COMMAND optimize-flatzinc-driver-test) + set_tests_properties(optimize-flatzinc-driver PROPERTIES TIMEOUT 120) + find_package(Python3 3.9 COMPONENTS Interpreter QUIET) + if(Python3_Interpreter_FOUND AND GECODE_OPTIMIZE_WITH_NATIVE) + if(GECODE_OPTIMIZE_WITH_HIGHS) + set(_gecode_cli_highs available) + else() + set(_gecode_cli_highs unavailable) + endif() + add_test(NAME optimize-flatzinc-driver-cli COMMAND "${CMAKE_COMMAND}" -E env + "PYTHONDONTWRITEBYTECODE=1" "${Python3_EXECUTABLE}" + "${PROJECT_SOURCE_DIR}/test/optimize/flatzinc_driver_cli.py" + --binary "$" --highs "${_gecode_cli_highs}") + set_tests_properties(optimize-flatzinc-driver-cli PROPERTIES TIMEOUT 90) + add_test(NAME optimize-minizinc-configure COMMAND "${CMAKE_COMMAND}" -E env + "PYTHONDONTWRITEBYTECODE=1" "${Python3_EXECUTABLE}" -B + "${PROJECT_SOURCE_DIR}/test/optimize/minizinc_configure.py" --cmake "${CMAKE_COMMAND}") + set_tests_properties(optimize-minizinc-configure PROPERTIES TIMEOUT 60) + if(GECODE_OPTIMIZE_MINIZINC_REGISTRATION AND GECODE_OPTIMIZE_MINIZINC_EXECUTABLE) + if(NOT EXISTS "${GECODE_OPTIMIZE_MINIZINC_EXECUTABLE}") + message(FATAL_ERROR "The explicitly configured MiniZinc test compiler is missing") + endif() + add_test(NAME optimize-minizinc-registration COMMAND "${CMAKE_COMMAND}" -E env + "PYTHONDONTWRITEBYTECODE=1" "${Python3_EXECUTABLE}" -B + "${PROJECT_SOURCE_DIR}/test/optimize/minizinc_registration.py" + --minizinc "${GECODE_OPTIMIZE_MINIZINC_EXECUTABLE}" + --binary "$" --registration "${_gecode_optimize_build_msc}") + set_tests_properties(optimize-minizinc-registration PROPERTIES TIMEOUT 150) + endif() + endif() + endif() add_executable(gecode-test-blackbox-exec ${GECODE_TEST_BLACKBOX_EXEC_SOURCE}) target_compile_features(gecode-test-blackbox-exec PRIVATE cxx_std_17) @@ -1633,12 +1776,22 @@ if(GECODE_INSTALL) PATTERN "flatzinc/blackbox.hh" EXCLUDE PATTERN "flatzinc/blackbox-backend.hh" EXCLUDE PATTERN "flatzinc/blackbox-process.hh" EXCLUDE + PATTERN "optimize/quadratic_bound.hpp" EXCLUDE + PATTERN "optimize/lp_observations_detail.hpp" EXCLUDE + PATTERN "optimize/lp_basis_detail.hpp" EXCLUDE + PATTERN "optimize/lp_sensitivity_backend.hpp" EXCLUDE + PATTERN "optimize/lp_sensitivity_highs_detail.hpp" EXCLUDE + PATTERN "optimize/native_regular_limits.hpp" EXCLUDE PATTERN "exampleplugin" EXCLUDE PATTERN "standalone-example" EXCLUDE PATTERN "abi*" EXCLUDE) install(FILES ${PROJECT_BINARY_DIR}/gecode/support/config.hpp DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/gecode/support/) + if(GECODE_ENABLE_OPTIMIZE) + install(FILES gecode/optimize/c_api.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/gecode/optimize/) + endif() if(GECODE_REGENERATE_VARIMP) install(FILES ${GECODE_VAR_TYPE_HPP} ${GECODE_VAR_IMP_HPP} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/gecode/kernel/) diff --git a/Makefile.in b/Makefile.in index b446405125..4d9e6c42dc 100755 --- a/Makefile.in +++ b/Makefile.in @@ -1957,6 +1957,7 @@ DOXYGEN_MIN_VERSION = 1.17.0 .PHONY: doc DOCSRC_NOTGENERATED = \ + doxygen/optimize.hh \ misc/doxygen/back.png misc/doxygen/footer.html \ misc/doxygen/gecode-logo-100.png \ misc/doxygen/stylesheet.css \ diff --git a/README.md b/README.md index a75b48fbf3..1f534e0d38 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,14 @@ In particular, Gecode comes with [extensive tutorial and reference documentation](https://gecode.github.io/documentation.html). +## Optional Optimization API + +`Gecode::Optimize` provides sparse optimization models, numerical solving with +HiGHS, and bounded integer search with Gecode. Enable it with +`-DGECODE_ENABLE_OPTIMIZE=ON`. See the +[optimization build and test guide](docs/optimize.md) and the +[reference documentation](doxygen/optimize.hh) for the supported interfaces. + ## CMake Build Options CMake exposes options aligned with the Autoconf build switches. @@ -44,6 +52,7 @@ Version metadata shared by autoconf and CMake lives in `gecode-version.m4`. | `--enable-set-vars` | `GECODE_ENABLE_SET_VARS` | Supported directly | Default `ON` | | `--enable-float-vars` | `GECODE_ENABLE_FLOAT_VARS` | Supported directly | Default `ON` | | `--enable-minimodel` | `GECODE_ENABLE_MINIMODEL` | Supported directly | Default `ON` | +| None | `GECODE_ENABLE_OPTIMIZE` | CMake-only | Optional optimization component; default `OFF` | | `--enable-driver` | `GECODE_ENABLE_DRIVER` | Supported directly | Default `ON` | | `--enable-flatzinc` | `GECODE_ENABLE_FLATZINC` | Supported directly | Default `ON` | | `--enable-mpfr` | `GECODE_ENABLE_MPFR` | Supported directly | Default `ON`; uses `find_package(MPFR)` | diff --git a/cmake/GecodeConfig.cmake.in b/cmake/GecodeConfig.cmake.in index 4099c3b2bd..ece4cd5e8b 100644 --- a/cmake/GecodeConfig.cmake.in +++ b/cmake/GecodeConfig.cmake.in @@ -10,6 +10,8 @@ set(_gecode_supported_components set float minimodel + lp + optimize driver flatzinc gist) @@ -72,6 +74,10 @@ while(_gecode_dependency_queue) set(_gecode_component_dependencies int kernel) elseif(_gecode_dependency_component STREQUAL minimodel) set(_gecode_component_dependencies int set search float) + elseif(_gecode_dependency_component STREQUAL lp) + set(_gecode_component_dependencies minimodel) + elseif(_gecode_dependency_component STREQUAL optimize AND "@GECODE_OPTIMIZE_WITH_NATIVE@") + set(_gecode_component_dependencies int search) elseif(_gecode_dependency_component STREQUAL gist) set(_gecode_component_dependencies search int set float) elseif(_gecode_dependency_component STREQUAL driver) @@ -87,8 +93,22 @@ while(_gecode_dependency_queue) endwhile() include(CMakeFindDependencyMacro) -if(support IN_LIST _gecode_component_closure AND - "@GECODE_PACKAGE_NEEDS_THREADS@" STREQUAL "ON") +if(optimize IN_LIST _gecode_component_closure AND "@GECODE_OPTIMIZE_WITH_HIGHS@") + find_dependency(highs 1.15 CONFIG) +elseif(lp IN_LIST _gecode_component_closure) + find_dependency(highs CONFIG) +endif() +if(lp IN_LIST _gecode_component_closure OR + (optimize IN_LIST _gecode_component_closure AND "@GECODE_OPTIMIZE_WITH_HIGHS@")) + if(NOT TARGET highs::highs) + set(Gecode_FOUND FALSE) + set(Gecode_NOT_FOUND_MESSAGE "Requested Gecode optimization component requires highs::highs") + return() + endif() +endif() +if((support IN_LIST _gecode_component_closure AND + "@GECODE_PACKAGE_NEEDS_THREADS@" STREQUAL "ON") OR + optimize IN_LIST _gecode_component_closure) find_dependency(Threads) endif() if(float IN_LIST _gecode_component_closure AND @@ -171,6 +191,16 @@ unset(_gecode_qt_version) set(PACKAGE_PREFIX_DIR "${_gecode_package_prefix_dir}") include("${CMAKE_CURRENT_LIST_DIR}/GecodeTargets.cmake") +if(TARGET Gecode::gecodeoptimize AND NOT TARGET Gecode::optimize) + add_library(Gecode::optimize INTERFACE IMPORTED) + set_target_properties(Gecode::optimize PROPERTIES + INTERFACE_LINK_LIBRARIES Gecode::gecodeoptimize) +endif() +if(TARGET Gecode::gecodeoptimize_c AND NOT TARGET Gecode::optimize_c) + add_library(Gecode::optimize_c INTERFACE IMPORTED) + set_target_properties(Gecode::optimize_c PROPERTIES + INTERFACE_LINK_LIBRARIES Gecode::gecodeoptimize_c) +endif() set(Gecode_VERSION "@GECODE_PROJECT_VERSION@") set_and_check(Gecode_INCLUDE_DIRS "@PACKAGE_CMAKE_INSTALL_INCLUDEDIR@") diff --git a/cmake/GecodeSources.cmake b/cmake/GecodeSources.cmake index 0c9defc0f1..6b9b06951d 100644 --- a/cmake/GecodeSources.cmake +++ b/cmake/GecodeSources.cmake @@ -238,6 +238,7 @@ set(GECODE_FLATZINC_SOURCES gecode/flatzinc/blackbox-process-windows.cpp gecode/flatzinc/blackbox-propagator.cpp gecode/flatzinc/branch.cpp + gecode/flatzinc/capture.cpp gecode/flatzinc/flatzinc.cpp gecode/flatzinc/lexer.yy.cpp gecode/flatzinc/parser.tab.cpp diff --git a/docs/optimize-io.md b/docs/optimize-io.md new file mode 100644 index 0000000000..c16cce15c4 --- /dev/null +++ b/docs/optimize-io.md @@ -0,0 +1,106 @@ +# Optimization Model Exchange + +`Gecode::Optimize::read_model` and `write_model` support a linear LP/free-MPS +subset independently of the numerical backend. They preserve the model's +`double` values. Exact rational parsing, compressed files, and vendor extensions +outside the syntax below are unsupported. + +## Import + +Only `.lp` and `.mps` suffixes are accepted, case-insensitively. Unreadable files, +malformed records, NaN, coefficient infinities, numeric overflow or underflow, +and unsupported constructs produce `ModelError`. Tiny nonzero coefficients are +preserved during import; a solving backend may reject unsupported magnitudes. + +Identifiers begin with an ASCII letter or underscore and continue with letters, +digits, or underscores. Fixed-column MPS files with ordinary whitespace-separated +identifiers are supported. Names with embedded spaces, quoted names, omitted +repeated column names, and SIF extensions are unsupported. + +### LP Syntax + +- One `Minimize` or `Maximize` objective, a `Subject To` section, optional `Bounds`, + `Binary`, `General`, and `Semi` sections, and a required `End`. Common aliases + include `min`, `max`, `st`, `binaries`, `generals`, and `semi-continuous`. +- Signed linear terms, decimal or scientific numbers, repeated terms combined + additively, objective offsets, and constants on constraint left sides. + Coefficients and variable names require separating whitespace. Constant + accumulation retains compensation through right-side subtraction before + conversion to the final `double` bound. +- One `<=`, `>=`, or `=` comparison with a numeric right side per constraint. + Objectives can wrap across lines. Constraints can wrap before their comparison + or right side; complete constraints must start separate lines. Ranged + expressions with multiple comparisons are unsupported. +- Bounds `lower <= x <= upper`, `x <= upper`, `x >= lower`, `x = value`, + `lower <= x`, and `x free`, with signed infinity where appropriate. Repeated + definitions of the same bound side are rejected. Undeclared variables are + continuous on `[0,+infinity)`. +- Semi-continuous variables and semi-integer variables declared in both `General` + and `Semi`. Semi domains are `{0} union [lower,upper]`; the nonzero interval + requires a strictly positive finite lower bound. +- Backslash comments. SOS, indicators, quadratic expressions, piecewise-linear + sections, and strict comparisons are unsupported. + +### MPS Syntax + +- `NAME`, optional `OBJSENSE` on the same or next line, `ROWS`, `COLUMNS`, optional + `RHS`, `RANGES`, and `BOUNDS`, and required `ENDATA`. +- The first `N` row is the objective; later `N` rows remain free constraints. + `L`, `G`, and `E` rows, one or two row/value pairs per `COLUMNS`, `RHS`, or + `RANGES` record, decimal or scientific values, and `D` exponents are supported. +- Integer markers `INTORG` and `INTEND`. A marker-only integer column with no + explicit bounds defaults to `[0,1]`; the writer emits explicit bounds. +- Bound types `LO`, `UP`, `FX`, `FR`, `MI`, `PL`, `BV`, `LI`, `UI`, `SC`, and `SI`. + Ambiguous overlapping bounds and conflicting type declarations are rejected. + `BV` sets both bounds and cannot be combined with other bounds for that variable. + `SC` on an already integral column is rejected; use `SI` to preserve integrality. + `SI` accepts an integer marker or preceding `LI`, but cannot be combined with + `BV` or `SC`. Ordinary `LO` with `SC` or `SI` is supported. +- One right-side vector, one range vector, and one bound vector. Multiple vectors, + unknown row or column references, duplicate right-side or range entries, + unsupported sections, SOS, quadratic data, and indicators are rejected. + +## Export + +The writer uses enough significant digits to preserve each `double` value. +Canonical names such as `x0`, `x1`, `r0`, and `r1` avoid identifier collisions and +syntax ambiguities. Original display names are preserved in hex-encoded +`GECODE_NAME` comments and restored by this reader. Other readers use the +canonical names. Malformed metadata and unknown entity references are rejected. + +LP ranged rows are written as two inequalities and a `GECODE_RANGE` comment. +On reimport, both expressions and their bounds must match before they are +recombined. Other readers see the equivalent pair of inequalities. MPS uses +`RANGES`; export fails if the range width cannot be represented and reread +without changing the bounds. LP's paired inequalities support that case. + +MPS semi-variable export requires a finite upper bound; LP supports an infinite +upper bound. MPS binary export requires exactly `[0,1]` bounds and emits a single +`BV` record. Tighter or fixed binary bounds require LP. Active original indicators +and globals cannot be exported by these linear formats and are rejected. + +## Destination Handling + +Export validates and compacts the source, writes an exclusive temporary file in +the destination directory, then checks write, flush, and close operations. It +reimports the temporary file and compares every active variable type, bound and +display name, every row and its coefficients, and the objective sense, terms and +offset. Only an exact numerical match permits same-directory atomic replacement. + +The existing destination remains unchanged if validation, serialization, +reimport, comparison, writing, or replacement fails. Temporary files are removed +on ordinary failure paths. Replacement uses `rename` on POSIX and `MoveFileExW` +with `MOVEFILE_REPLACE_EXISTING` on Windows. Atomic visibility does not guarantee +power-loss durability. The old destination's inode, permissions, and ACLs are +not retained. + +## Tests + +After building as described in the [optimization guide](optimize.md), run: + +```sh +ctest --test-dir build/optimize -C Release --output-on-failure -R '^optimize-io' +``` + +The tests cover precision, domains, repeated terms, constants, malformed inputs, +semantic round trips, and preservation of the destination after export failures. diff --git a/docs/optimize.md b/docs/optimize.md new file mode 100644 index 0000000000..03e736b8a3 --- /dev/null +++ b/docs/optimize.md @@ -0,0 +1,131 @@ +# Optimization Build and Test Guide + +`Gecode::Optimize` provides owning sparse models, numerical LP/MILP solving with +HiGHS, and bounded integer search with Gecode. The component is optional and +requires CMake; `GECODE_ENABLE_OPTIMIZE` defaults to `OFF`. + +The [reference documentation](../doxygen/optimize.hh) describes model ownership, +backend restrictions, result guarantees, and the available C++, C, and Python +interfaces. The [model exchange guide](optimize-io.md) describes LP/MPS syntax. + +## Requirements + +- CMake 3.21 or newer and a C++17-capable compiler. +- HiGHS 1.15 or newer for the numerical backend and checked native LP deductions. + For a reproducible configuration, use HiGHS 1.15.1 at commit + `04024d701f79feb8e2f18bc3df0dffc04ef05088` from + [ERGO-Code/HiGHS](https://github.com/ERGO-Code/HiGHS). +- Python 3.9 or newer for binding and frontend tests. +- MiniZinc 2.10.1 with its matching standard library for MiniZinc tests. + +Dependencies must be installed separately. CMake accepts an installed HiGHS +package through `CMAKE_PREFIX_PATH`, or a source checkout through +`GECODE_OPTIMIZE_HIGHS_SOURCE`. It does not download dependencies. +See the [CMake build guide](cmake-build.md) for other Gecode build options. + +## Build and Test + +From the repository root, with a HiGHS source checkout: + +```sh +cmake -S . -B build/optimize -DCMAKE_BUILD_TYPE=Release \ + -DGECODE_ENABLE_OPTIMIZE=ON \ + -DGECODE_OPTIMIZE_HIGHS_SOURCE=/path/to/HiGHS \ + -DGECODE_OPTIMIZE_BUILD_TESTS=ON -DBUILD_TESTING=ON \ + -DGECODE_ENABLE_QT=OFF -DGECODE_ENABLE_GIST=OFF +cmake --build build/optimize --config Release --parallel 4 +ctest --test-dir build/optimize -C Release --output-on-failure +``` + +The integer and search components enable the native optimization backend. +CTest runs the optimization tests and the configured Gecode test subset. +`GECODE_OPTIMIZE_TEST_PYTHON=OFF` disables Python binding tests when the host +Python cannot load an instrumented library, such as in a sanitizer build. + +To run the native algorithm and frontend tests separately: + +```sh +ctest --test-dir build/optimize -C Release --output-on-failure \ + -R '^optimize-(native|flatzinc|minizinc)' +``` + +To build without HiGHS, set `GECODE_OPTIMIZE_WITH_HIGHS=OFF` and omit +`GECODE_OPTIMIZE_HIGHS_SOURCE`. Native integer solving remains available; +numerical solving and checked native LP deductions report unsupported status. + +## MiniZinc + +The experimental `fzn-gecode-optimize` registration supports finite integer +models admitted by the optimization frontend. It accepts linear constraints, +Boolean relations, and supported all-different, element, table, cumulative, +circuit, and regular constraints. Unsupported model features produce an error. +Numerical LP/MILP/QP MiniZinc models and general search annotations are unsupported. + +Enable the registration and provide the MiniZinc compiler to register its tests: + +```sh +cmake -S . -B build/optimize \ + -DGECODE_OPTIMIZE_MINIZINC_REGISTRATION=ON \ + -DGECODE_OPTIMIZE_MINIZINC_EXECUTABLE=/path/to/minizinc +cmake --build build/optimize --config Release --parallel 4 +ctest --test-dir build/optimize -C Release --output-on-failure +``` + +This reuses the build configured above. The tests run the compiler, driver, and +output processing against the source fixtures. To repeat that test directly: + +```sh +python3 -B test/optimize/minizinc_registration.py \ + --minizinc /path/to/minizinc \ + --binary build/optimize/bin/fzn-gecode-optimize \ + --registration build/optimize/minizinc/Release/gecode-optimize.msc +``` + +For generators that put executables in configuration directories, use the +corresponding `Release` executable path. CTest selects that path automatically. + +The native strategy can be selected with `--native-mode auto`, `race`, `plain`, +or `configured`. `--native-diagnostics on` prints the selected route and work +counters as protocol comments. For example: + +```sh +/path/to/minizinc \ + --solver build/optimize/minizinc/Release/gecode-optimize.msc \ + --native-mode auto --native-diagnostics on \ + test/optimize/minizinc-fixtures/mzn-native-knapsack.mzn +``` + +## Standalone Numerical Build + +The numerical component can also be built without the native Gecode libraries: + +```sh +cmake -S gecode/optimize -B build/optimize-numerical \ + -DCMAKE_BUILD_TYPE=Release \ + -DGECODE_OPTIMIZE_HIGHS_SOURCE=/path/to/HiGHS +cmake --build build/optimize-numerical --config Release --parallel 4 +ctest --test-dir build/optimize-numerical -C Release --output-on-failure +``` + +A standalone build cannot enable `GECODE_OPTIMIZE_WITH_NATIVE`. + +## Installation and Package Consumption + +```sh +cmake --install build/optimize --config Release --prefix /path/to/install +``` + +For a combined installation: + +```cmake +find_package(Gecode CONFIG REQUIRED COMPONENTS optimize) +target_link_libraries(my_target PRIVATE Gecode::optimize) +``` + +For a standalone installation, use `find_package(GecodeOptimize CONFIG REQUIRED)`. +Both packages provide `Gecode::optimize` and the alias `Gecode::gecodeoptimize`. +The C interface uses `gecode/optimize/c_api.h` and `Gecode::optimize_c`. + +The Python package in `python/` loads the shared C library. Set `PYTHONPATH` to +that directory and `GECODE_OPTIMIZE_LIBRARY` to the built or installed shared +library's absolute path. The package does not download a solver at runtime. diff --git a/doxygen/doxygen.conf.in b/doxygen/doxygen.conf.in index 02a6a25aef..9522fc29ef 100755 --- a/doxygen/doxygen.conf.in +++ b/doxygen/doxygen.conf.in @@ -463,6 +463,7 @@ WARN_LOGFILE = doxygen.log # with spaces. INPUT = doxygen.hh \ + "@top_srcdir@/doxygen/optimize.hh" \ license.hh \ stat.hh \ changelog.hh \ @@ -520,6 +521,12 @@ EXCLUDE_PATTERNS = \ *moc_* \ *gecode/gist/standalone-example/* \ *gecode/third-party/* \ + *gecode/optimize/quadratic_bound.hpp \ + *gecode/optimize/lp_observations_detail.hpp \ + *gecode/optimize/lp_basis_detail.hpp \ + *gecode/optimize/lp_sensitivity_backend.hpp \ + *gecode/optimize/lp_sensitivity_highs_detail.hpp \ + *gecode/optimize/native_regular_limits.hpp \ *gecode/flatzinc/exampleplugin/* \ *gecode/flatzinc/parser.tab.* *gecode/flatzinc/lexer.* diff --git a/doxygen/doxygen.hh.in b/doxygen/doxygen.hh.in index 69ae6ec5fa..f9301fca17 100755 --- a/doxygen/doxygen.hh.in +++ b/doxygen/doxygen.hh.in @@ -351,10 +351,14 @@ * You may also want to have a look at our \ref PageNotation as well as * our \ref Example. * + * The optional \ref PageOptimize "Optimize model and solver API" provides + * sparse models with numerical and bounded integer backends. + * * \section SecByTask Programming tasks * * Documentation is available for the following tasks: * - \ref TaskModel + * - \ref TaskOptimize * - \ref TaskSearch * - \ref TaskActor "Programming propagators and branchers" * - \ref TaskVar diff --git a/doxygen/optimize.hh b/doxygen/optimize.hh new file mode 100644 index 0000000000..c62c3092a4 --- /dev/null +++ b/doxygen/optimize.hh @@ -0,0 +1,435 @@ +/* Documentation for the optional optimization component. */ + +/** + * \defgroup TaskOptimize Modeling and solving with Optimize + * \ingroup TaskModel + * + * The optional C++17 namespace Gecode::Optimize offers an owning sparse model, + * numerical LP/MILP solving and an explicit bounded integer bridge to native + * %Gecode. Start with \ref PageOptimize. Existing Space-based models, propagators, + * search engines and the original FlatZinc driver retain their own interfaces. + */ + +/** + * \namespace Gecode::Optimize + * \brief Optional sparse optimization models, backends and owning results. + * \ingroup TaskOptimize + * + * See \ref PageOptimize for backend scope and \ref PageOptimizeModel for + * model ownership, result guarantees and validation. + */ + +/** + * \page PageOptimize Optimize: sparse optimization and native integer search + * + * The optimization API is included with + * \code{.cpp} + * #include + * \endcode + * It is enabled separately from the existing %Gecode libraries. A model owns + * variables, sparse rows, objective data and supported logical/global metadata; + * a solve returns a historical result rather than a mutable search Space. + * + * - \subpage PageOptimizeModel + * - \subpage PageOptimizeNative + * - \subpage PageOptimizeLP + * - \subpage PageOptimizeWorkflows + * - \subpage PageOptimizeBuild + * + * \section OptimizeRoutes Choose the interface and backend + * + * | Interface | Supported models | Restrictions | + * | --- | --- | --- | + * | Existing Space, IntVar, BoolVar, SetVar, FloatVar and native search APIs | The original CP modeling, global constraints, propagators, search customization and configured parallel search | Optimize restrictions below do not remove these existing capabilities. See \ref TaskModel and \ref TaskModelSearch. | + * | Optimize with Backend::Highs | Numerical continuous LP and mixed-integer linear models, including supported binary and semi domains | One worker per solve; numerical tolerances and adapter scaling limits apply. Exact and Certified requests are rejected. | + * | Optimize with Backend::Native | Finite Integer, Binary and SemiInteger models with exact integral linear data, retained indicators and six typed global families | Conservative native coefficient/activity limits; one deterministic worker; no arbitrary continuous or fractional model conversion. | + * | Explicit native LP/frontier/neighborhood APIs | Checked integer LP deductions, optional original-row root covers, frontier bounds, binary reliability probes and one bounded incumbent neighborhood | Explicit entry points allow direct control; the common Native policy selects suitable LP, covers and reliability. An optional sequential race compares automatic and ordinary search. | + * | QuadraticModel and solve_quadratic | Bounded continuous convex minimization or concave maximization expressed as weighted squares plus linear terms | Numerical checking; separate model type. No integer QP, quadratic constraints or general nonconvex optimization. | + * + * Gecode::Optimize::solve selects native %Gecode for active typed globals under + * Backend::Auto, and HiGHS otherwise. Auto does not select a backend from the + * requested guarantee: an ordinary linear model requesting Exact must select + * Native explicitly. Missing or unsupported backends return Unsupported; + * explicit requests are not silently substituted. Within Native, the common + * dispatcher uses solve_native_auto to preserve eligible knapsack DP and select + * optional checked LP/branching from bounded model structure. Direct + * solve_native retains ordinary BAB. The automatic route additionally applies + * bounded exact presolve, independent-component solving and ordering of exactly + * interchangeable columns when compatible. The opt-in solve_native_race API + * compares automatic and ordinary routes with sequential probes, then restarts + * the selected route under the same time/node budget. Exploration can increase + * CPU work and solve time, but can reveal a better strategy for a longer solve. + * Exploration time is configurable. The experimental MiniZinc registration + * exposes automatic, racing and explicitly configured native strategies. + * No conflict learning is added. Inspect + * Gecode::Optimize::capabilities, Gecode::Optimize::native_capabilities, + * Gecode::Optimize::native_lp_capabilities and + * Gecode::Optimize::quadratic_capabilities for the current build. + * + * \section OptimizeExample A small exact integer model + * + * \code{.cpp} + * #include + * #include + * namespace O = Gecode::Optimize; + * + * O::Model model; + * const auto x = model.add_integer(0, 4, "x"); + * const auto y = model.add_integer(0, 4, "y"); + * model.add_row({{x, 1}, {y, 1}}, 3, + * std::numeric_limits::infinity(), "demand"); + * model.minimize({{x, 2}, {y, 1}}, 7); + * O::SolveOptions options; + * options.backend = O::Backend::Native; + * options.guarantee = O::Guarantee::Exact; + * options.time_limit_seconds = 10; + * const auto result = O::solve(model, options); + * if (result.has_solution()) { + * const double chosen_y = result.value(y); + * (void)chosen_y; + * } + * // A completed optimum is x=0, y=3, objective=10. + * // Check result.termination before claiming optimality. + * \endcode + * + * For a numerical LP, use Continuous variables and Backend::Highs with + * Guarantee::Numerical. Consult \ref PageOptimizeBuild for the required build. + */ + +/** + * \page PageOptimizeModel Model ownership, logical constraints and results + * + * \section OptimizeOwnership Models and edits + * + * Gecode::Optimize::Model stores sparse ranged rows and min/max objectives with + * an offset. VariableType distinguishes Continuous, Integer, Binary, + * SemiContinuous and SemiInteger domains. Duplicate terms are coalesced + * deterministically; invalid/nonfinite coefficients are rejected. The + * VariableSpec, RowSpec and SparseRowBatch overloads provide atomic bulk + * additions, including compressed sparse row input. + * + * Variable and constraint handles carry an owner identity and never-reused + * slot. Removal leaves tombstones; foreign and deleted handles are rejected. + * Successful edits advance the revision. A Model is movable, not copyable; + * ModelSnapshot owns a historical copy. Public snapshot fields remain + * untrusted: structural and original-semantic checks run at API boundaries. + * Historical SolveResult, observations and analysis artifacts survive edits + * and destruction of their originating model/session. + * + * \section OptimizeLogic Logical and global constraints + * + * Gecode::Optimize::add_indicator retains the original implication and derives + * finite conservative M values from declared domains for numerical solving. + * It rejects insufficient bounds or overflow instead of guessing M. Generated + * rows/gates and their domain guards remain associated with the original + * metadata. Mutations that invalidate the lowering are rejected; use + * Gecode::Optimize::remove_indicator for its supported removal lifecycle. + * Native search uses the original reified implication and preserves exposed + * gate semantics. The independent checker tests the original implication too. + * + * Gecode::Optimize::add_boolean_and and Gecode::Optimize::add_boolean_or + * require Binary inputs/results. Empty AND is true; empty OR is false. + * + * | Typed helper | Original meaning | + * | --- | --- | + * | Gecode::Optimize::add_all_different | Pairwise different integer values. | + * | Gecode::Optimize::add_element | Variable index/result with explicit index base and retained array aliases. | + * | Gecode::Optimize::add_table | Membership in a positive table of integer tuples. | + * | Gecode::Optimize::add_cumulative | Mandatory fixed-duration, fixed-height tasks with half-open intervals; zero duration/height consumes no resource. | + * | Gecode::Optimize::add_circuit | One cycle through a nonempty successor array with explicit index base. | + * | Gecode::Optimize::add_regular | Deterministic finite automaton, sparse state IDs, unique state/symbol transitions, no epsilon transitions. A missing transition rejects; the empty word accepts exactly when the initial state is final. | + * + * These six families are the Optimize registry, not the full original %Gecode + * global catalog. Active globals require native compilation, which performs + * additional domain, indexing, storage and arithmetic admission. HiGHS and + * model exporters reject unsupported original metadata instead of discarding it. + * + * \section OptimizeResult Result and guarantee contracts + * + * Gecode::Optimize::SolveResult separates termination from availability of a + * validated incumbent. Test has_solution() before value(handle), and inspect + * termination before claiming completion. Values use original slots, including + * a separate active mask for tombstones. Missing bounds/gaps are absent, not + * zero. The common relative gap is + * abs(primal-dual)/max(1,abs(primal),abs(dual)); native_backend_gap, when present, + * retains a vendor's different convention. + * + * Numerical results are tolerance-qualified. Native Exact uses admitted integer + * arithmetic, original-domain/constraint checking and finite search; it does + * not export a complete independently checkable proof. Certified is unsupported + * by these solve routes. Checked LP deductions do not upgrade a complete solve + * into a certified proof artifact. Infeasibility, local exhaustion and + * unavailable numerical data are distinct outcomes. + * + * A SolveBudget shares a monotonic deadline, cancellation and node accounting. + * Copying, conversion and checking are included in the documented whole-call + * allowance. Calls into propagation, factorization or a backend are cooperative + * and can overrun a wall limit. Publication rules differ by operation: inspect + * the result status and availability flags even when historical diagnostics or + * a previously accepted incumbent remain present. + */ + +/** + * \page PageOptimizeNative Native search, starts and optional enhancements + * + * \section OptimizeNativeAdmission Exact subset and complete starts + * + * The native bridge admits finite Integer/Binary/SemiInteger domains and integral + * coefficients/finite row sides within native integer limits. SemiInteger keeps + * its zero alternative. Conservative sums of absolute products bound row, + * indicator and objective activities; cancellation is not used to evade those + * guards. Objective offsets and all attainable objective values must remain + * exactly representable in result doubles. Unsupported fractions, ranges or + * native-global storage limits fail before search; they never imply infeasibility. + * + * Complete primal_start entries use original handles and exact integral values. + * Every active ordinary slot must be supplied. Only live indicator inactivity + * gates may be completed from their exact activators; explicitly supplied gates + * must agree. Removed indicators do not imply a retained gate's value. Other + * private/fixed slots are not inferred. Unresolved partial starts are Unsupported, + * and invalid/infeasible complete assignments are InvalidModel. No near-integral + * rounding or permanent fixing is performed. A timely checked start seeds the + * incumbent and an unfixed original root with a strict improvement cutoff. + * + * Native routes require threads=1 and random_seed=0. Gap tolerances do not + * enable early gap stopping. Existing native CP search options remain available + * through their original APIs independently of these facade limitations. + * + * Ordinary native search recognizes a bounded exact binary capacity case: all + * active variables are Binary with domains [0,1], and one same-sign integral + * row covers them all, with one capacity side and no indicators or globals. + * Positive upper rows and their negative lower-row equivalents are supported. + * A capacity-indexed dynamic program uses two rolling value rows and packed + * decisions, with capacity 65536, 32 million transitions, 8 MiB accounted payload + * and a cooperative 250 ms local preprocessing cap. Its completed witness supplies a branch + * preference and its exact optimum supplies a one-sided objective bound. + * Existing search still checks and publishes the solution and termination. + * Other model shapes or size-cap misses use the existing brancher. Time and + * cancellation checks remain active during preprocessing. Explicit checked-LP + * and local-neighborhood construction do not enable this optimization. + * The DP preference follows the engine's actual descent order. BestBound favors + * the region containing that witness only when objective bounds tie. Generic + * smallest-domain/minimum-first branching and non-DP frontier order are unchanged. + * + * \section OptimizeNativeRoutes Explicit native entry points + * + * | API | Behavior and evidence | + * | --- | --- | + * | Gecode::Optimize::solve_native | Native propagation and BAB. An interrupted incumbent may be returned, but no unfinished-frontier global bound is exposed. | + * | Gecode::Optimize::solve_native_lp | Native constraints plus a sparse ordinary-row relaxation. LP bounds/domain deductions require checked integer certificates; numerical LP infeasibility alone never prunes. Native, HiGHS and checked-wide-integer support are required. | + * | Gecode::Optimize::solve_native_search | DepthFirst or BestBound frontier with explicit storage cap. Every queued/active region remains represented during partial expansion; interrupted bounds aggregate all unresolved regions and the incumbent when initial compilation established a bound. | + * | Gecode::Optimize::solve_native_neighborhoods | The same frontier plus at most one bounded BinaryHamming incumbent improvement attempt. See below. | + * + * NativeLpSettings can schedule root or after-bound-change relaxations and + * explicitly enable root_cover_cuts. Covers are independently verified against + * immutable original ordinary rows and global domains. An augmentation and its + * exact evidence retain their owning source. LP suggestions select candidates; + * they are not original feasible witnesses or proof. Local-scope cuts are not + * promoted into the global root pool. + * + * NativeSearchOptions::branching enables BinaryReliability. Only completed + * finite paired propagation probes update solve-local history. These gains are + * not LP pseudocosts. Probes choose a split; they publish neither incumbents nor + * pruning evidence. General-integer reliability and LP-informed branching are + * unsupported. + * + * \section OptimizeNeighborhood One bounded incumbent neighborhood + * + * NativeNeighborhoodOptions wraps the ordinary search options without enabling + * any existing default route. BinaryHamming waits for a checked incumbent and + * a surviving stable parent. It posts a fresh original native model, a strict + * original objective cutoff and a Hamming radius. Distance counts all eligible + * original nonfixed Binary slots without indicator_origin. It counts slots, + * not independent mathematical decisions; equality-linked slots count separately. + * Other original variables and constraints remain, including globals and semis. + * + * The neighborhood may find an improvement outside the active parent. Only a + * timely exact original-model-validated assignment is published after local + * cleanup. Local bounds, infeasibility and exhaustion never become global proof. + * The main frontier remains represented throughout the attempt. This operation + * cannot create the first incumbent of a cold solve and is not RINS, RENS, + * partial-start repair or an automatic heuristic portfolio. + * + * Settings cap status attempts, source entries, coordinator work, retained + * Spaces, distance-variable count and local elapsed time. These are not byte + * or CPU-instruction guarantees. Local caps stop optional work; outer limits + * stop the whole solve. With reliability and neighborhoods enabled, the shared + * node count equals frontier admissions + probe status attempts + neighborhood + * status attempts. The statistics' budget_nodes fields repeat that total; do + * not add them again. Optional local admissions reserve two ordinary child slots. + */ + +/** + * \page PageOptimizeLP Numerical LP observations, basis, evidence and sensitivity + * + * These explicit workflows use ordinary Continuous linear models and numerical + * HiGHS semantics. They do not automatically relax a MIP or replace unsupported + * indicators/globals. Historical artifacts own their source model and original + * slot mapping. Available zero values, unavailable data and unrequested data + * have different states; read those states before optional payloads. + * + * | Operation | Returned data | Restrictions | + * | --- | --- | --- | + * | Gecode::Optimize::solve_lp_observed | Original row activity/slack, checked duals/reduced costs and basis statuses | Accepted duals require timely optimal primal/dual data and independent KKT checks. Basis data is not a proof. | + * | Gecode::Optimize::make_lp_basis and Gecode::Optimize::solve_lp_with_basis | Immutable original basis input and explicit accepted/repaired/rejected submission | Exact owner/revision/content checks; no simultaneous primal start, silent cold fallback or faster-solve guarantee. | + * | Gecode::Optimize::analyze_lp_evidence | Independently checked numerical primal rays or Farkas multipliers using explicit private auxiliary solves | Original and auxiliary coordinates/statuses remain distinct; no exact infeasibility certificate claim. | + * | Gecode::Optimize::analyze_lp_sensitivity | One-parameter objective-coefficient or common equality-RHS interval for a selected optimal basis | Additional private factorization/system solves, zero optimization runs; numerical intervals only. | + * + * \section OptimizeSensitivity Selected-basis sensitivity + * + * LpSensitivityOptions requests unique LpObjectiveParameter or + * LpEqualityRhsParameter entries from an owning LpObservedResult. The analyzer + * checks original feasibility, dual/KKT evidence, complete basis statuses and + * reconstructed reference point before interval publication. It neither repairs + * nor chooses another basis. A degenerate optimum can have different intervals + * for different selected bases/statuses. + * + * \code{.cpp} + * namespace O = Gecode::Optimize; + * O::Model model; + * const auto x = model.add_continuous(); + * const auto y = model.add_continuous(); + * const auto balance = model.add_row({{x, 1}, {y, 1}}, 3, 3); + * model.minimize({{x, 2}, {y, 1}}, 7); + * const auto observed = O::solve_lp_observed(model); + * O::LpSensitivityOptions options; + * options.parameters = {O::LpObjectiveParameter{x}, + * O::LpEqualityRhsParameter{balance}}; + * const auto analysis = O::analyze_lp_sensitivity(observed, options); + * if (analysis.sensitivity) { + * const auto* entry = analysis.sensitivity->objective(x); + * if (entry && entry->group.state == O::LpSensitivityState::Available) { + * // Read entry->interval: tagged endpoints, slope and checks. + * } + * } + * \endcode + * + * Each interval varies one original parameter alone with the selected basis + * and nonbasic statuses fixed. Equality RHS varies both equal sides together. + * Endpoints are absolute parameter values; infinity is tagged, not a finite + * double. A singleton is an available interval. The optional objective_slope + * describes slope*(parameter-anchor), without adding the original offset again. + * Variable-bound, inequality-side, matrix-entry and simultaneous perturbation + * ranges are not supported. There is no CPLEX/Gurobi ranging equivalence claim. + * + * Complete means every requested interval passed; Partial allows per-request + * rejection. Whole-call stop, resource, allocation or cleanup failure clears + * every interval's availability, including earlier results. Source history and + * completed diagnostics can remain. Lookups perform no solver work and survive + * model/session destruction. Size/work/factor-solve caps and one cooperative + * deadline cover admission through backend cleanup. + */ + +/** + * \page PageOptimizeWorkflows Repeated solves, diagnostics and model exchange + * + * | API | Meaning and current scope | + * | --- | --- | + * | Gecode::Optimize::SolveSession | Persistent numerical backend state, compatible LP basis reuse and revalidated previous MIP hints; supports observed/basis solves. Reuse is reported, not assumed to improve runtime. | + * | Gecode::Optimize::solve_lexicographic | Highest-priority-first linear objectives, checked retention rows and explicit degradation. Unfinished stages are not a lexicographic optimum. | + * | Gecode::Optimize::analyze_conflict | Numerical deletion-filter conflict groups over original rows/bounds/domains; irreducibility is relative to the documented groups, not a minimum-cardinality conflict or exact proof. | + * | Gecode::Optimize::relax_feasibility | Selected row/bound sides with positive L1 penalties in a private model; repair residuals do not make the unchanged original model feasible. | + * | Gecode::Optimize::solve_pool | One representative per finite discrete projection; established ranked prefix is separate from a final unranked candidate and from projection exhaustion. | + * | Gecode::Optimize::presolve_integer | Explicit checked bounded-integer reductions with owning reconstruction. A presolve fixpoint is not original optimality; postsolve returns a checked original witness without transferring a reduced-model global bound. | + * | Gecode::Optimize::solve_scenarios | Serial sparse objective/bound overrides on ordinary Continuous/Integer/Binary linear models under one batch allowance; no shared search tree. Explicit Native uses its one-shot route. | + * | Gecode::Optimize::solve_quadratic | Separate weighted-square continuous convex/concave model and original-coordinate checks; see QuadraticModel and QuadraticOptions. | + * + * Workflow support is narrower than the base Model vocabulary. Consult each + * entry point before combining semis, globals, indicators, guarantees, starts or + * multistage node limits. Unsupported combinations fail explicitly; a sequence + * of solves does not imply a global consumed-node or proof contract it does not + * implement. Private model results retain explicit mappings to original slots. + * + * \section OptimizeExchange Files and FlatZinc + * + * Gecode::Optimize::read_model and Gecode::Optimize::write_model support a strict + * numerical LP/free-MPS subset. Unsupported dialect/metadata is rejected. + * Export performs a checked semantic round trip before atomic replacement; + * it does not silently omit indicators or globals. This is not full support for + * every vendor extension. + * + * The opt-in fzn-gecode-optimize driver uses immutable raw FlatZinc capture, + * Gecode::Optimize::compile_flatzinc and an independent original-source checker + * before publishing output. Its admitted integer linear, Boolean, reified and + * typed-global subset includes all-different, element, table/holey domains, + * explicit-offset circuit, fixed four-argument cumulative and literal-parameter + * six-argument Regular. Predicate signatures, annotation forms, alias/domain + * semantics and finite resource admission are checked explicitly. Other + * predicates or parameter forms may be rejected even when the original + * fzn-gecode driver supports them. + * + * The separate experimental MiniZinc registration invokes this driver in + * --minizinc mode. Its compiler library lowers supported forms and preserves + * explicit rejections elsewhere; it does not change the default registration. + * The standard -t milliseconds convention belongs to that mode: -t 0 means no + * limit, whereas the direct --time-limit 0 option requests an immediate limit. + * Ordinary incomplete MiniZinc output uses protocol status and exit zero; + * malformed/unsupported inputs remain errors. + * Namespaced --native-* extra flags expose automatic feature switches, + * sequential racing, checked LP/cover cuts, frontier order, reliability + * branching and bounded Hamming neighborhoods. --native-diagnostics on reports + * requested settings, actual route and available work counters as comments. + * NativeAutoOptions and solve_native_auto_configured expose the same automatic + * switches to C++; NativeRaceOptions::automatic controls its automatic candidate. + * Integer 0..1 source domains retain integer output while becoming internal + * binary decisions. Bounded automatic presolve can eliminate a singly defined + * affine objective auxiliary, preserving its bounds and restoring/checking + * its original value before publication. Numerical MiniZinc model support and + * general solve annotations remain outside this registration's scope. + * + * \section OptimizeBindings C and Python + * + * The installed gecode/optimize/c_api.h and shared Gecode::optimize_c target + * provide a versioned C surface. The Python 3.9+ ctypes package is in python/; + * it loads a built library and does not download a solver at runtime. The + * bindings cover model/bulk edits, logic/globals including Regular, sessions, + * complete native starts, solution pools, feasibility repair, scenarios and + * LP observations/basis/evidence/sensitivity. C++ API availability alone does + * not imply a matching binding: standalone lexicographic objectives, conflict + * analysis, integer presolve and the explicit native neighborhood API remain + * available only in C++. C handles and owning result objects + * have explicit close/destroy lifetimes; follow the header's counted-buffer and + * version fields. + */ + +/** + * \page PageOptimizeBuild Building and linking the optional component + * + * Optimize requires C++17 and CMake 3.21+. In a full %Gecode source build, + * GECODE_ENABLE_OPTIMIZE defaults to OFF. Enable it explicitly; the native + * bridge also requires integer and search components. GECODE_OPTIMIZE_WITH_NATIVE + * defaults to ON when those native targets exist. For a native-only component: + * + * \code{.sh} + * cmake -S . -B build/native-optimize \ + * -DGECODE_ENABLE_OPTIMIZE=ON -DGECODE_OPTIMIZE_WITH_HIGHS=OFF \ + * -DGECODE_ENABLE_INT_VARS=ON -DGECODE_ENABLE_SEARCH=ON \ + * -DCMAKE_BUILD_TYPE=Release + * cmake --build build/native-optimize --config Release --parallel 2 + * ctest --test-dir build/native-optimize -C Release --output-on-failure + * \endcode + * + * A standalone numerical build uses -S gecode/optimize and an installed HiGHS + * package, or GECODE_OPTIMIZE_HIGHS_SOURCE pointing to a separately obtained + * source checkout. For reproducible builds, use HiGHS 1.15.1 at commit + * 04024d701f79feb8e2f18bc3df0dffc04ef05088. CMake does not fetch it. A standalone + * build cannot enable the native bridge. With both backends disabled, model, + * checking, exchange and coordinator APIs remain available; solving returns + * Unsupported rather than a substitute implementation. + * + * After installation, use one matching package discovery route: + * \code{.cmake} + * find_package(Gecode CONFIG REQUIRED COMPONENTS optimize) # combined package + * # Or: find_package(GecodeOptimize CONFIG REQUIRED) # standalone + * target_link_libraries(my_target PRIVATE Gecode::optimize) + * \endcode + * Both expose Gecode::optimize and Gecode::gecodeoptimize aliases. For the C + * library use Gecode::optimize_c. A native-only component request does not + * require HiGHS. Compiled native libraries and generated configuration headers + * must match; do not mix different ABI/configuration or sanitizer cohorts. + * + * The existing native documentation target uses Doxygen 1.17.0 or newer and uv + * for generated helper pages. These Optimize pages are part of that target even + * when the optional solver component is disabled. Documentation does not enable + * a backend. See docs/optimize.md for build and test commands. + */ diff --git a/examples/optimize-file.cpp b/examples/optimize-file.cpp new file mode 100644 index 0000000000..42061bc9ef --- /dev/null +++ b/examples/optimize-file.cpp @@ -0,0 +1,107 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace O = Gecode::Optimize; + +static std::string json(const std::string& value) { + std::ostringstream out; + out << '"'; + for (unsigned char c : value) { + switch (c) { + case '"': out << "\\\""; break; + case '\\': out << "\\\\"; break; + case '\n': out << "\\n"; break; + case '\r': out << "\\r"; break; + case '\t': out << "\\t"; break; + default: + if (c<32) out << "\\u" << std::hex << std::setw(4) << std::setfill('0') << unsigned(c) << std::dec; + else out << c; + } + } + out << '"'; + return out.str(); +} + +static double number(const std::string& text) { + std::size_t used=0; + const double value=std::stod(text,&used); + if (used!=text.size() || !std::isfinite(value) || value<0) + throw std::invalid_argument("Option requires a nonnegative finite number"); + return value; +} + +static void optional(const std::optional& value) { + if (value && std::isfinite(*value)) std::cout << *value; + else std::cout << "null"; +} + +int main(int argc, char** argv) { + const auto begin=std::chrono::steady_clock::now(); + const auto elapsed=[&] { return std::chrono::duration(std::chrono::steady_clock::now()-begin).count(); }; + std::cout << std::setprecision(std::numeric_limits::max_digits10); + try { + if (argc<2) throw std::invalid_argument("Usage: optimize-file MODEL.lp|MODEL.mps [--time-limit SECONDS] [--relative-gap GAP] [--absolute-gap GAP] [--seed N]"); + O::SolveOptions options; + for (int arg=2; arg=argc) throw std::invalid_argument("Missing option value"); + const std::string key=argv[arg]; + const double value=number(argv[arg+1]); + if (key=="--time-limit") options.time_limit_seconds=value; + else if (key=="--relative-gap") options.relative_gap=value; + else if (key=="--absolute-gap") options.absolute_gap=value; + else if (key=="--seed" && std::floor(value)==value && value<=std::numeric_limits::max()) + options.random_seed=static_cast(value); + else throw std::invalid_argument("Unknown or invalid option: "+key); + } + options.validate(); + auto model=O::read_model(argv[1]); + const double import_seconds=elapsed(); + if (std::isfinite(options.time_limit_seconds)) + options.time_limit_seconds=std::max(0.0,options.time_limit_seconds-import_seconds); + const auto result=O::solve(model,options); + std::cout << "{\"schema_version\":1,\"model\":" << json(argv[1]) + << ",\"backend\":" << json(result.backend) + << ",\"backend_version\":" << json(result.backend_version) + << ",\"guarantee\":\"numerical\",\"termination\":" << json(O::to_string(result.termination)) + << ",\"message\":" << json(result.message) + << ",\"solution_validated\":" << (result.has_solution()?"true":"false") + << ",\"objective\":"; + optional(result.objective); + std::cout << ",\"best_bound\":"; optional(result.best_bound); + std::cout << ",\"absolute_gap\":"; optional(result.absolute_gap); + std::cout << ",\"relative_gap\":"; optional(result.relative_gap); + std::cout << ",\"native_backend_gap\":"; optional(result.native_backend_gap); + std::cout << ",\"import_seconds\":" << import_seconds + << ",\"solve_seconds\":" << result.elapsed_seconds + << ",\"elapsed_seconds\":" << elapsed() << ",\"values\":["; + for (std::size_t i=0; i +#include +#include + +int main() { + namespace O = Gecode::Optimize; + O::Model model; + const auto open = model.add_binary("open"); + const auto quantity = model.add_continuous(0.0, 100.0, "quantity"); + model.add_row({{quantity, 1.0}, {open, -100.0}}, + -std::numeric_limits::infinity(), 0.0, "capacity"); + model.add_row({{quantity, 1.0}}, 40.0, + std::numeric_limits::infinity(), "demand"); + model.minimize({{open, 12.0}, {quantity, 0.5}}); + O::SolveOptions options; + options.time_limit_seconds = 30; + auto result = O::solve(model, options); + std::cout << result.backend << ' ' << result.backend_version << ": " + << O::to_string(result.termination) << '\n'; + if (!result.has_solution()) { + std::cout << result.message << '\n'; + return 1; + } + std::cout << "open=" << result.value(open) << " quantity=" << result.value(quantity) + << " objective=" << *result.objective << '\n'; + return result.termination == O::Termination::Optimal ? 0 : 1; +} diff --git a/gecode/flatzinc/capture-records.hh b/gecode/flatzinc/capture-records.hh new file mode 100644 index 0000000000..f050dc738f --- /dev/null +++ b/gecode/flatzinc/capture-records.hh @@ -0,0 +1,122 @@ +/* Owning FlatZinc records, independent of native parser configuration. */ +#ifndef GECODE_FLATZINC_CAPTURE_RECORDS_HH +#define GECODE_FLATZINC_CAPTURE_RECORDS_HH +#include +#include +#include +#include +#include + +namespace Gecode { namespace FlatZinc { namespace Capture { + +enum class Type { Integer, Boolean, Float, Set }; +struct Reference { + Type type = Type::Integer; + std::size_t index = 0; +}; +/** Line is the parser reduction/end line, not a claimed exact start span. */ +struct Location { std::string source; std::size_t line = 0, ordinal = 0; }; +struct SetLiteral { + bool interval = false; + std::int64_t lower = 0, upper = -1; + std::vector values; +}; +enum class ValueKind { Integer, Boolean, Float, Set, Reference, Array, Atom, String, Call }; +/** Value is an owning tagged tree. Only the member selected by kind is meaningful. + * Array elements are in source order. Call uses text for its identifier and + * elements for its positional arguments (one scalar argument remains one). + */ +struct Value { + ValueKind kind = ValueKind::Integer; + std::int64_t integer = 0; + bool boolean = false; + double floating = 0; + SetLiteral set; + Reference reference; + std::string text; + std::vector elements; +}; +struct Domain { + bool present = false; + SetLiteral integers; // Integer/Boolean domain or set upper bound. + double lower = 0, upper = 0; // Float domain, when present. +}; +struct Variable { + Reference reference; + std::string name; + bool alias = false; + Reference target; // meaningful only when alias; same namespace. + bool assigned = false; // meaningful only when !alias. + Value value; // assigned literal, meaningful only when !alias && assigned. + Domain domain; // meaningful only when !alias; alias restrictions are rows. + bool introduced = false, functionally_defined = false; +}; +struct Constraint { + std::string id; + std::vector arguments; + std::vector annotations; + Location location; + bool synthesized = false; +}; +struct DeclarationAnnotations { + std::string name; + std::vector annotations; + Location location; +}; +enum class CoverageKind { Retained, AliasEquality }; +struct Coverage { + CoverageKind kind = CoverageKind::Retained; + std::size_t raw_constraint = 0; +}; +enum class Method { Satisfy, Minimize, Maximize }; +struct SolveGoal { + Method method = Method::Satisfy; + bool has_objective = false; + Value objective; // original literal or typed variable reference, never dummy 0. + std::vector annotations; + Location location; +}; +/** The original output expression is a typed value or an Array layout containing + * String fragments and a typed value Array (legacy arrayNd representation). + * DeclarationAnnotations retains the original output_array dimensions as sets. + * No printer shrinking/renumbering has occurred. + */ +struct Output { std::string name; Value expression; }; +struct Records { + // Raw state after declarations, before constraint equality rewrites. + // Each namespace is indexed by Variable.reference.index, never by name. + std::vector raw_variables; + std::vector raw_domains; + std::vector raw_constraints; + // Normalized alias state and both complete pre-posting constraint vectors. + std::vector variables; + std::vector domains; + std::vector constraints; + std::vector coverage; + std::vector declaration_annotations; + SolveGoal solve; + std::vector output; + std::string source; +}; +enum class Status { Complete, InvalidInput, Unsupported, ResourceLimit }; +struct Diagnostic { Status status = Status::InvalidInput; Location location; std::string message; }; +struct Options { + std::size_t max_input_bytes = 16 * 1024 * 1024; + std::size_t max_variables = 1000000; + std::size_t max_array_elements = 1000000; + std::size_t max_constraints = 1000000; + std::size_t max_value_depth = 64; + std::string source = ""; +}; +struct Result { + Status status = Status::InvalidInput; + // Complete means parsing/capture completed, NOT that a solver supports it. + // Unknown ordinary predicates are retained and require compiler rejection. + // Non-Complete never publishes partial records. Shared immutable ownership + // survives parser/input destruction and is safe for independent readers. + std::shared_ptr records; + std::vector diagnostics; +}; + +}}} +#endif diff --git a/gecode/flatzinc/capture.cpp b/gecode/flatzinc/capture.cpp new file mode 100644 index 0000000000..9d71d671e7 --- /dev/null +++ b/gecode/flatzinc/capture.cpp @@ -0,0 +1,323 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +int yyparse(void*); +int yylex_init(void**); +int yylex_destroy(void*); +int yyget_lineno(void*); +void yyset_extra(void*,void*); + +namespace Gecode { namespace FlatZinc { +namespace C=Capture; +namespace { +struct CaptureFailure { C::Status status; std::string message; }; +[[noreturn]] void bad(const std::string& text){throw CaptureFailure{C::Status::InvalidInput,text};} +[[noreturn]] void limit(const std::string& text){throw CaptureFailure{C::Status::ResourceLimit,text};} +C::Location location(ParserState& p,std::size_t ordinal=0){ + int line=p.yyscanner?yyget_lineno(p.yyscanner):0; + return {p.capture->options.source,static_cast(std::max(0,line)),ordinal}; +} +C::Reference ref(C::Type type,int index){if(index<0)bad("negative variable reference");return {type,static_cast(index)};} +C::SetLiteral set(AST::SetLit* s){ + C::SetLiteral out;out.interval=s->interval; + if(s->interval){out.lower=s->min;out.upper=s->max;} + else { + for(int v:s->s)out.values.push_back(v); + std::sort(out.values.begin(),out.values.end()); + out.values.erase(std::unique(out.values.begin(),out.values.end()),out.values.end()); + } + return out; +} +std::string string_value(const std::string& source){ + std::string out; + for(std::size_t i=0;ip.capture->options.max_value_depth)limit("capture expression nesting exceeds limit"); + C::Value out; + if(n->isInt()){out.kind=C::ValueKind::Integer;out.integer=n->getInt();} + else if(n->isBool()){out.kind=C::ValueKind::Boolean;out.boolean=n->getBool();} + else if(n->isFloat()){out.kind=C::ValueKind::Float;out.floating=n->getFloat();if(!std::isfinite(out.floating))bad("float literal must be finite");} + else if(n->isSet()){out.kind=C::ValueKind::Set;out.set=set(n->getSet());} + else if(n->isIntVar()){out.kind=C::ValueKind::Reference;out.reference=ref(C::Type::Integer,n->getIntVar());} + else if(n->isBoolVar()){out.kind=C::ValueKind::Reference;out.reference=ref(C::Type::Boolean,n->getBoolVar());} + else if(n->isFloatVar()){out.kind=C::ValueKind::Reference;out.reference=ref(C::Type::Float,n->getFloatVar());} + else if(n->isSetVar()){out.kind=C::ValueKind::Reference;out.reference=ref(C::Type::Set,n->getSetVar());} + else if(n->isString()){out.kind=C::ValueKind::String;out.text=string_value(n->getString());} + else if(n->isAtom()){out.kind=C::ValueKind::Atom;out.text=n->getAtom()->id;} + else if(auto* a=dynamic_cast(n)){ + out.kind=C::ValueKind::Array;if(a->a.size()>p.capture->options.max_array_elements)limit("expression array exceeds capture limit"); + for(auto* x:a->a)out.elements.push_back(value(p,x,depth+1)); + }else if(auto* call=dynamic_cast(n)){ + out.kind=C::ValueKind::Call;out.text=call->id; + if(auto* a=dynamic_cast(call->args)){ + if(a->a.size()>p.capture->options.max_array_elements)limit("call arity exceeds capture limit"); + for(auto* x:a->a)out.elements.push_back(value(p,x,depth+1)); + }else out.elements.push_back(value(p,call->args,depth+1)); + }else bad("unsupported AST expression in capture"); + return out; +} +std::vector values(ParserState& p,AST::Array* nodes){ + std::vector out;if(!nodes)return out; + if(nodes->a.size()>p.capture->options.max_array_elements)limit("argument count exceeds capture limit"); + for(auto* node:nodes->a)out.push_back(value(p,node));return out; +} +C::Constraint constraint(ParserState& p,const ConExpr& ce,bool synthesized){ + return {ce.id,values(p,ce.args),values(p,ce.ann),location(p),synthesized}; +} +std::vector variables(ParserState& p){ + if(p.intvars.size()+p.boolvars.size()+p.floatvars.size()+p.setvars.size()>p.capture->options.max_variables)limit("variable count exceeds capture limit"); + std::vector out; + const auto add=[&](const std::vector& source,C::Type type){ + for(std::size_t i=0;ialias;v.introduced=spec->introduced;v.functionally_defined=spec->funcDep; + if(v.alias){v.target=ref(type,spec->i);if(v.target.index>=i)bad("variable alias must name an earlier variable in its namespace");} + else { + v.assigned=spec->assigned; + if(type==C::Type::Integer||type==C::Type::Boolean){ + const auto domain=type==C::Type::Integer?static_cast(spec)->domain:static_cast(spec)->domain; + v.domain.present=domain();if(domain())v.domain.integers=set(domain.some()); + if(v.assigned){v.value.kind=type==C::Type::Integer?C::ValueKind::Integer:C::ValueKind::Boolean;v.value.integer=spec->i;v.value.boolean=spec->i!=0;} + }else if(type==C::Type::Float){ + auto domain=static_cast(spec)->domain;v.domain.present=domain(); + if(domain()){v.domain.lower=domain.some().first;v.domain.upper=domain.some().second; + if(!std::isfinite(v.domain.lower)||!std::isfinite(v.domain.upper))bad("float domain must be finite");} + if(v.assigned){v.value.kind=C::ValueKind::Float;v.value.floating=v.domain.lower;} + }else { + auto domain=static_cast(spec)->upperBound;v.domain.present=domain();if(domain())v.domain.integers=set(domain.some()); + if(v.assigned){v.value.kind=C::ValueKind::Set;v.value.set=v.domain.integers;} + } + } + out.push_back(std::move(v)); + } + }; + add(p.intvars,C::Type::Integer);add(p.boolvars,C::Type::Boolean);add(p.setvars,C::Type::Set);add(p.floatvars,C::Type::Float);return out; +} +template void guard(CaptureParser& c,ParserState& p,F&& f){ + if(p.hadError)return; + try{f();}catch(const CaptureFailure& e){c.fail(p,e.status,e.message);} + catch(const AST::TypeError& e){c.fail(p,C::Status::InvalidInput,e.what());} + catch(const std::out_of_range&){c.fail(p,C::Status::InvalidInput,"reference outside captured namespace");} +} +using Counts=std::array; +Counts counts(const std::vector& vars){ + Counts result{};for(const auto& v:vars)++result.at(static_cast(v.reference.type));return result; +} +void check_ref(const C::Value& v,const Counts& ns){ + if(v.kind==C::ValueKind::Reference && + v.reference.index>=ns.at(static_cast(v.reference.type))) + bad("expression references a missing variable"); + for(const auto& child:v.elements)check_ref(child,ns); +} +void validate_signature(const C::Constraint& c){ + const auto& a=c.arguments; + const auto is_int=[](const C::Value& v){return v.kind==C::ValueKind::Integer||(v.kind==C::ValueKind::Reference&&v.reference.type==C::Type::Integer);}; + const auto is_bool=[](const C::Value& v){return v.kind==C::ValueKind::Boolean||(v.kind==C::ValueKind::Reference&&v.reference.type==C::Type::Boolean);}; + const auto arity=[&](std::size_t n){if(a.size()!=n)bad("wrong argument count for "+c.id);}; + if(c.id=="int_eq"||c.id=="int_ne"||c.id=="int_le"||c.id=="int_lt"||c.id=="int_ge"||c.id=="int_gt"){ + arity(2);if(!is_int(a[0])||!is_int(a[1]))bad("integer comparison arguments have wrong types"); + }else if(c.id=="bool_eq"||c.id=="bool_le"||c.id=="bool_lt"||c.id=="bool_not"){ + arity(2);if(!is_bool(a[0])||!is_bool(a[1]))bad("Boolean comparison arguments have wrong types"); + }else if(c.id=="bool2int"){ + arity(2);if(!is_bool(a[0])||!is_int(a[1]))bad("bool2int arguments have wrong types"); + } +} +void normalize(C::Records& r){ + r.variables=r.raw_variables;r.domains=r.raw_domains; + std::array,4> indices; + for(std::size_t i=0;i(r.variables[i].reference.type)).push_back(i); + const auto index=[&](C::Reference ref){return indices.at(static_cast(ref.type)).at(ref.index);}; + std::vector parent(r.variables.size()); + for(std::size_t i=0;iparent.size())bad("cyclic variable alias"); + root=parent[root]; + } + while(parent[i]!=i){const auto next=parent[i];parent[i]=root;i=next;} + return root; + }; + for(std::size_t n=0;nj)std::swap(i,j); + if(i!=j){auto& old=r.variables[j]; + if(old.assigned||old.domain.present){C::Value variable;variable.kind=C::ValueKind::Reference;variable.reference=r.variables[i].reference; + C::Value domain;domain.kind=C::ValueKind::Set; + if(old.assigned){domain.set.interval=true;domain.set.lower=domain.set.upper=old.reference.type==C::Type::Boolean?old.value.boolean:old.value.integer;} + else domain.set=old.domain.integers; + r.domains.push_back({"int_in",{variable,domain},{},c.location,true}); + } + parent[j]=i;old.alias=true;old.target=r.variables[i].reference;old.assigned=false;old.domain={};old.value={}; + } + r.coverage.push_back({C::CoverageKind::AliasEquality,n}); + }else {r.constraints.push_back(c);r.coverage.push_back({C::CoverageKind::Retained,n});} + } + for(auto& v:r.variables)if(v.alias)v.target=r.variables[base(v.reference)].reference; +} +} +void CaptureParser::fail(ParserState& p,C::Status value,const std::string& message){ + if(status==C::Status::Complete){status=value;diagnostics.push_back({value,location(p),message});} + p.hadError=true; +} +bool CaptureParser::array_size(ParserState& p,int n){ + if(n<0||static_cast(n)>options.max_array_elements || + p.arrays.size()+p.floatvals.size()+p.setvals.size()+static_cast(std::max(0,n))+1>static_cast(std::numeric_limits::max()))fail(p,C::Status::ResourceLimit,"declared array length exceeds capture limit");return !p.hadError; +} +bool CaptureParser::variable_count(ParserState& p,int n,bool initialized_array){ + const auto current=p.intvars.size()+p.boolvars.size()+p.setvars.size()+p.floatvars.size(); + // Initialized arrays may reuse existing slots; the exact total is checked + // after declarations. The input limit already bounds their explicit list. + if(n<0 || current>options.max_variables || (!initialized_array && + static_cast(n)>options.max_variables-current)) + fail(p,C::Status::ResourceLimit,"variable count exceeds capture limit"); + return !p.hadError; +} +bool CaptureParser::annotations(ParserState& p,AST::Array* ann){ + guard(*this,p,[&]{for(const auto& v:values(p,ann))if(v.kind==C::ValueKind::Call&&v.text=="output_array"){ + if(v.elements.size()!=1 || v.elements[0].kind!=C::ValueKind::Array)bad("output_array requires one array of dimensions"); + for(const auto& dim:v.elements[0].elements)if(dim.kind!=C::ValueKind::Set)bad("output_array dimensions must be sets"); + }});return !p.hadError; +} +void CaptureParser::declaration(ParserState& p,const std::string& name,AST::Array* ann,int output_length){ + if(!annotations(p,ann))return; + guard(*this,p,[&]{ + auto captured=values(p,ann); + for(const auto& a:captured)if(a.kind==C::ValueKind::Call&&a.text=="output_array"){ + if(output_length<0)bad("output_array requires an array declaration"); + const auto& dims=a.elements[0].elements;if(dims.empty())bad("output_array requires at least one dimension"); + std::size_t total=1; + for(const auto& d:dims){ + const auto width=d.set.interval ? (d.set.upper(d.set.upper-d.set.lower+1)) : d.set.values.size(); + if(width && total>options.max_array_elements/width)limit("output dimensions exceed capture limit"); + total*=width; + } + if(total!=static_cast(output_length))bad("output_array dimensions do not match declaration length"); + } + records.declaration_annotations.push_back({name,std::move(captured),location(p,records.declaration_annotations.size())}); + }); +} +void CaptureParser::begin(ParserState& p){guard(*this,p,[&]{ + records.source=options.source;records.raw_variables=variables(p);namespace_counts=counts(records.raw_variables); + if(p.domainConstraints.size()>options.max_constraints)limit("declaration domain count exceeds capture limit"); + for(auto* c:p.domainConstraints)records.raw_domains.push_back(::Gecode::FlatZinc::constraint(p,*c,true)); + declarations_recorded=true; +});} +void CaptureParser::constraint(ParserState& p,const std::string& id,AST::Array* args,AST::Array* ann){guard(*this,p,[&]{ + if(records.raw_constraints.size()>=options.max_constraints-records.raw_domains.size())limit("constraint count exceeds capture limit"); + if(id.compare(0,18,"gecode_on_restart_")==0)throw CaptureFailure{C::Status::Unsupported,"native restart-state predicates are unsupported by capture"}; + C::Constraint out{id,values(p,args),values(p,ann),location(p,records.raw_constraints.size()),false}; + validate_signature(out);for(const auto& v:out.arguments)check_ref(v,namespace_counts); + records.raw_constraints.push_back(std::move(out)); +});} +void CaptureParser::objective(ParserState& p,AST::Node* n){guard(*this,p,[&]{records.solve.objective=value(p,n);records.solve.has_objective=true;});} +void CaptureParser::solve(ParserState& p,C::Method method,AST::Array* ann){guard(*this,p,[&]{ + records.solve.method=method;records.solve.annotations=values(p,ann);records.solve.location=location(p); + if(method==C::Method::Satisfy){records.solve.has_objective=false;records.solve.objective={};} + else {if(!records.solve.has_objective)bad("missing original objective");check_ref(records.solve.objective,namespace_counts);} + solve_recorded=true; +});} +void CaptureParser::finish(ParserState& p){guard(*this,p,[&]{ + if(!declarations_recorded||!solve_recorded)bad("incomplete captured model"); + normalize(records); + for(const auto& out:p._output){auto expression=value(p,out.second);check_ref(expression,namespace_counts);records.output.push_back({out.first,std::move(expression)});} + std::sort(records.output.begin(),records.output.end(),[](const C::Output& a,const C::Output& b){return a.namestatic_cast(std::numeric_limits::max())|| + options.max_variables>static_cast(std::numeric_limits::max())|| + options.max_array_elements>static_cast(std::numeric_limits::max())|| + options.max_value_depth>1024)return failure(Status::InvalidInput,options,"capture limits exceed parser address/index range"); + if(input.size()>options.max_input_bytes)return failure(Status::ResourceLimit,options,"input exceeds capture byte limit"); + if(input.find('\0')!=std::string::npos)return failure(Status::InvalidInput,options,"embedded NUL in FlatZinc input"); + for(std::size_t i=0;i(input[i++]);if(lead<128)continue; + unsigned n=lead>=0xc2&&lead<=0xdf?1:lead>=0xe0&&lead<=0xef?2:lead>=0xf0&&lead<=0xf4?3:0; + if(!n || n>input.size()-i)return failure(Status::InvalidInput,options,"input is not valid UTF-8"); + std::uint32_t code=lead&((1u<<(6-n))-1u); + for(unsigned k=0;k(input[i++]);if((c&0xc0)!=0x80)return failure(Status::InvalidInput,options,"input is not valid UTF-8");code=(code<<6)|(c&0x3f);} + if((n==1&&code<0x80)||(n==2&&code<0x800)||(n==3&&code<0x10000)||code>0x10ffff||(code>=0xd800&&code<=0xdfff))return failure(Status::InvalidInput,options,"input is not valid UTF-8"); + } + + std::size_t depth=0;bool comment=false,quoted=false,escaped=false; + for(char c:input){ + if(comment){if(c=='\n')comment=false;continue;} + if(quoted){ + if(c=='\n' || c=='\r')return failure(Status::InvalidInput,options,"newline in string literal"); + if(escaped){ + escaped=false;continue; + } + if(c=='\\'){escaped=true;continue;} + if(c=='"')quoted=false; + continue; + } + if(c=='%'){comment=true;continue;} + if(c=='"'){quoted=true;continue;} + if(c=='('||c=='['||c=='{'){ + if(++depth>options.max_value_depth)return failure(Status::ResourceLimit,options,"source nesting exceeds capture limit"); + }else if((c==')'||c==']'||c=='}')&&depth) --depth; + } + if(quoted)return failure(Status::InvalidInput,options,"unterminated string literal"); + std::ostringstream errors;CaptureParser capture(options);ParserState p(input,errors,nullptr);p.capture=&capture; + struct Cleanup{ParserState& p;~Cleanup(){if(p.yyscanner)yylex_destroy(p.yyscanner);CaptureParser::cleanup(p);}} cleanup{p}; + if(yylex_init(&p.yyscanner))throw std::bad_alloc();yyset_extra(&p,p.yyscanner); + try{ + const int code=yyparse(&p); + if(code==2)capture.fail(p,Status::ResourceLimit,"generated parser exhausted its storage"); + else if(code||p.hadError){if(capture.status==Status::Complete)capture.fail(p,Status::InvalidInput,errors.str().empty()?"invalid FlatZinc input":errors.str());} + else capture.finish(p); + }catch(const AST::TypeError& e){capture.fail(p,Status::InvalidInput,e.what());} + catch(const std::out_of_range&){capture.fail(p,Status::InvalidInput,"invalid parser reference");} + if(capture.status!=Status::Complete)return {capture.status,nullptr,std::move(capture.diagnostics)}; + return {Status::Complete,std::make_shared(std::move(capture.records)),{}}; +} +Result parse(std::istream& input,const Options& options){ + if(options.max_input_bytes>static_cast(std::numeric_limits::max()))return failure(Status::InvalidInput,options,"capture input limit exceeds parser index range"); + std::string text;char block[4096]; + while(input){ + const auto room=options.max_input_bytes-text.size(); + const auto n=static_cast(std::min(sizeof(block),room+1)); + bool failed=false; + try { input.read(block,n); } catch(const std::bad_alloc&) { throw; } catch(...) { failed=true; } + const auto got=input.gcount(); + if(got<0 || static_cast(got)>room)return failure(Status::ResourceLimit,options,"input exceeds capture byte limit"); + text.append(block,static_cast(got)); + if(input.bad() || (failed&&!input.eof()))return failure(Status::InvalidInput,options,"input read failed"); + } + if(input.bad() || (!input.eof()&&input.fail()))return failure(Status::InvalidInput,options,"input read failed"); + return parse_string(text,options); +} +} +}} diff --git a/gecode/flatzinc/capture.hh b/gecode/flatzinc/capture.hh new file mode 100644 index 0000000000..c9900302c1 --- /dev/null +++ b/gecode/flatzinc/capture.hh @@ -0,0 +1,36 @@ +/* Owning FlatZinc parser records. No Optimize or solver-state dependencies. */ +#ifndef GECODE_FLATZINC_CAPTURE_HH +#define GECODE_FLATZINC_CAPTURE_HH + +#include +#include +#include + +// Same shared-library boundary as the native FlatZinc entry points, without +// including native Space/AST implementation headers in this record API. +#if !defined(GECODE_STATIC_LIBS) && (defined(__CYGWIN__) || defined(__MINGW32__) || defined(_MSC_VER)) +# ifdef GECODE_BUILD_FLATZINC +# define GECODE_CAPTURE_EXPORT __declspec(dllexport) +# else +# define GECODE_CAPTURE_EXPORT __declspec(dllimport) +# endif +#elif defined(GECODE_GCC_HAS_CLASS_VISIBILITY) +# define GECODE_CAPTURE_EXPORT __attribute__((visibility("default"))) +#else +# define GECODE_CAPTURE_EXPORT +#endif + +namespace Gecode { namespace FlatZinc { namespace Capture { + +/** Capture without constructing a native Space or invoking any registry poster. + * Input size and nesting are bounded before parsing; declared bulk allocation + * and captured record counts have explicit limits. No solver is called. + * Allocation failure may throw std::bad_alloc. All other input failures have + * a typed Result and never publish partial records. + */ +GECODE_CAPTURE_EXPORT Result parse(std::istream& input, const Options& options = {}); +GECODE_CAPTURE_EXPORT Result parse_string(const std::string& input, const Options& options = {}); + +}}} +#undef GECODE_CAPTURE_EXPORT +#endif diff --git a/gecode/flatzinc/lexer.lxx b/gecode/flatzinc/lexer.lxx index 564744382d..4a9a1af33e 100755 --- a/gecode/flatzinc/lexer.lxx +++ b/gecode/flatzinc/lexer.lxx @@ -50,6 +50,8 @@ void yyerror(void*, const char*); #include #include +#include +#include #include @@ -67,6 +69,34 @@ bool parseInt(const char* text, int& value) { return true; } +bool parseCaptureInt(const char* text, int& value) { + bool negative=*text=='-';if(negative)++text; + unsigned radix=10; + if(text[0]=='0'&&text[1]=='x'){radix=16;text+=2;} + else if(text[0]=='0'&&text[1]=='o'){radix=8;text+=2;} + const std::uint64_t maximum=negative?static_cast(-static_cast(Gecode::Int::Limits::min)): + static_cast(Gecode::Int::Limits::max); + std::uint64_t result=0;if(!*text)return false; + for(;*text;++text){ + const unsigned char c=static_cast(*text); + unsigned digit=c>='0'&&c<='9'?c-'0':c>='a'&&c<='f'?c-'a'+10:c>='A'&&c<='F'?c-'A'+10:99; + if(digit>=radix||result>(maximum-digit)/radix)return false; + result=result*radix+digit; + } + value=static_cast(negative?-static_cast(result):static_cast(result));return true; +} + +bool parseCaptureFloat(const char* text,double& value) { + std::istringstream input(text);input.imbue(std::locale::classic()); + input >> std::noskipws >> value; + if(input.fail() || !input.eof() || !std::isfinite(value)) return false; + if(value==0) { + for(const char* p=text;*p && *p!='e' && *p!='E';++p) + if(*p>='1' && *p<='9')return false; // nonzero literal underflowed to zero + } + return true; +} + int yy_input_proc(char* buf, int size, yyscan_t yyscanner); #define YY_INPUT(buf, result, max_size) \ result = yy_input_proc(buf, max_size, yyscanner); @@ -80,7 +110,7 @@ int yy_input_proc(char* buf, int size, yyscan_t yyscanner); "true" { yylval->iValue = 1; return FZ_BOOL_LIT; } "false" { yylval->iValue = 0; return FZ_BOOL_LIT; } --?[0-9]+ { if (parseInt(yytext,yylval->iValue)) +-?[0-9]+ { if (static_cast(yyextra)->capture ? parseCaptureInt(yytext,yylval->iValue) : parseInt(yytext,yylval->iValue)) return FZ_INT_LIT; else yyerror(("The literal '" + std::string(yytext) @@ -89,7 +119,7 @@ int yy_input_proc(char* buf, int size, yyscan_t yyscanner); + ".." + std::to_string(Gecode::Int::Limits::max) + ")").c_str()); } --?0x[0-9A-Fa-f]+ { if (parseInt(yytext,yylval->iValue)) +-?0x[0-9A-Fa-f]+ { if (static_cast(yyextra)->capture ? parseCaptureInt(yytext,yylval->iValue) : parseInt(yytext,yylval->iValue)) return FZ_INT_LIT; else yyerror(("The literal '" + std::string(yytext) @@ -98,7 +128,7 @@ int yy_input_proc(char* buf, int size, yyscan_t yyscanner); + ".." + std::to_string(Gecode::Int::Limits::max) + ")").c_str()); } --?0o[0-7]+ { if (parseInt(yytext,yylval->iValue)) +-?0o[0-7]+ { if (static_cast(yyextra)->capture ? parseCaptureInt(yytext,yylval->iValue) : parseInt(yytext,yylval->iValue)) return FZ_INT_LIT; else yyerror(("The literal '" + std::string(yytext) @@ -107,11 +137,17 @@ int yy_input_proc(char* buf, int size, yyscan_t yyscanner); + ".." + std::to_string(Gecode::Int::Limits::max) + ")").c_str()); } --?[0-9]+\.[0-9]+ { yylval->dValue = strtod(yytext,NULL); +-?[0-9]+\.[0-9]+ { if(static_cast(yyextra)->capture) { + if(!parseCaptureFloat(yytext,yylval->dValue)) yyerror("Float literal is nonfinite or outside representable range"); + } else yylval->dValue = strtod(yytext,NULL); return FZ_FLOAT_LIT; } --?[0-9]+\.[0-9]+[Ee][+-]?[0-9]+ { yylval->dValue = strtod(yytext,NULL); +-?[0-9]+\.[0-9]+[Ee][+-]?[0-9]+ { if(static_cast(yyextra)->capture) { + if(!parseCaptureFloat(yytext,yylval->dValue)) yyerror("Float literal is nonfinite or outside representable range"); + } else yylval->dValue = strtod(yytext,NULL); return FZ_FLOAT_LIT; } --?[0-9]+[Ee][+-]?[0-9]+ { yylval->dValue = strtod(yytext,NULL); +-?[0-9]+[Ee][+-]?[0-9]+ { if(static_cast(yyextra)->capture) { + if(!parseCaptureFloat(yytext,yylval->dValue)) yyerror("Float literal is nonfinite or outside representable range"); + } else yylval->dValue = strtod(yytext,NULL); return FZ_FLOAT_LIT; } [=:;{}(),\[\]\.] { return *yytext; } \.\. { return FZ_DOTDOT; } @@ -155,6 +191,15 @@ int yy_input_proc(char* buf, int size, yyscan_t yyscanner); "where" { return FZ_WHERE; } [A-Za-z][A-Za-z0-9_]* { yylval->sValue = strdup(yytext); return FZ_ID; } _[_]*[A-Za-z][A-Za-z0-9_]* { yylval->sValue = strdup(yytext); return FZ_U_ID; } +\"([^"\\\n]|\\[^\n])*\" { + if(!static_cast(yyextra)->capture) { + // Preserve the legacy scanner's first-quote behavior. + const char* close=strchr(yytext+1,'"'); + yyless(static_cast(close-yytext+1)); + } + yylval->sValue = strdup(yytext+1); + yylval->sValue[strlen(yytext)-2] = 0; + return FZ_STRING_LIT; } \"[^"\n]*\" { yylval->sValue = strdup(yytext+1); yylval->sValue[strlen(yytext)-2] = 0; diff --git a/gecode/flatzinc/lexer.yy.cpp b/gecode/flatzinc/lexer.yy.cpp index 38024f7ece..fe3e94b807 100644 --- a/gecode/flatzinc/lexer.yy.cpp +++ b/gecode/flatzinc/lexer.yy.cpp @@ -46,7 +46,7 @@ #if defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L /* C99 says to define __STDC_LIMIT_MACROS before including stdint.h, - * if you want the limit (max/min) macros for int types. + * if you want the limit (max/min) macros for int types. */ #ifndef __STDC_LIMIT_MACROS #define __STDC_LIMIT_MACROS 1 @@ -59,11 +59,12 @@ typedef int16_t flex_int16_t; typedef uint16_t flex_uint16_t; typedef int32_t flex_int32_t; typedef uint32_t flex_uint32_t; +typedef uint64_t flex_uint64_t; #else typedef signed char flex_int8_t; typedef short int flex_int16_t; typedef int flex_int32_t; -typedef unsigned char flex_uint8_t; +typedef unsigned char flex_uint8_t; typedef unsigned short int flex_uint16_t; typedef unsigned int flex_uint32_t; @@ -187,7 +188,7 @@ typedef size_t yy_size_t; #define EOB_ACT_CONTINUE_SCAN 0 #define EOB_ACT_END_OF_FILE 1 #define EOB_ACT_LAST_MATCH 2 - + /* Note: We specifically omit the test for yy_rule_can_match_eol because it requires * access to the local variable yy_act. Since yyless() is a macro, it would break * existing scanners that call yyless() from OUTSIDE yylex. @@ -197,7 +198,7 @@ typedef size_t yy_size_t; */ #define YY_LESS_LINENO(n) \ do { \ - int yyl;\ + yy_size_t yyl;\ for ( yyl = n; yyl < yyleng; ++yyl )\ if ( yytext[yyl] == '\n' )\ --yylineno;\ @@ -209,7 +210,7 @@ typedef size_t yy_size_t; if ( *p == '\n' )\ --yylineno;\ }while(0) - + /* Return all but the first "n" matched characters back to the input stream. */ #define yyless(n) \ do \ @@ -242,7 +243,7 @@ struct yy_buffer_state /* Number of characters read into yy_ch_buf, not including EOB * characters. */ - int yy_n_chars; + yy_size_t yy_n_chars; /* Whether we "own" the buffer - i.e., we know we created it, * and can realloc() it to grow it, and should free() it to @@ -319,7 +320,7 @@ static void yy_init_buffer ( YY_BUFFER_STATE b, FILE *file , yyscan_t yyscanner YY_BUFFER_STATE yy_scan_buffer ( char *base, yy_size_t size , yyscan_t yyscanner ); YY_BUFFER_STATE yy_scan_string ( const char *yy_str , yyscan_t yyscanner ); -YY_BUFFER_STATE yy_scan_bytes ( const char *bytes, int len , yyscan_t yyscanner ); +YY_BUFFER_STATE yy_scan_bytes ( const char *bytes, yy_size_t len , yyscan_t yyscanner ); void *yyalloc ( yy_size_t , yyscan_t yyscanner ); void *yyrealloc ( void *, yy_size_t , yyscan_t yyscanner ); @@ -366,12 +367,12 @@ static void yynoreturn yy_fatal_error ( const char* msg , yyscan_t yyscanner ); */ #define YY_DO_BEFORE_ACTION \ yyg->yytext_ptr = yy_bp; \ - yyleng = (int) (yy_cp - yy_bp); \ + yyleng = (yy_size_t) (yy_cp - yy_bp); \ yyg->yy_hold_char = *yy_cp; \ *yy_cp = '\0'; \ yyg->yy_c_buf_p = yy_cp; -#define YY_NUM_RULES 56 -#define YY_END_OF_BUFFER 57 +#define YY_NUM_RULES 57 +#define YY_END_OF_BUFFER 58 /* This struct is not used in this scanner, but its presence is necessary. */ struct yy_trans_info @@ -379,32 +380,33 @@ struct yy_trans_info flex_int32_t yy_verify; flex_int32_t yy_nxt; }; -static const flex_int16_t yy_accept[221] = +static const flex_int16_t yy_accept[226] = { 0, - 0, 0, 57, 55, 2, 1, 55, 3, 12, 55, - 12, 6, 6, 12, 52, 55, 52, 52, 52, 52, + 0, 0, 58, 56, 2, 1, 56, 3, 12, 56, + 12, 6, 6, 12, 52, 56, 52, 52, 52, 52, + 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, + 52, 52, 0, 54, 0, 3, 6, 6, 13, 0, + 0, 0, 0, 14, 52, 53, 0, 52, 52, 52, + 52, 52, 52, 52, 52, 52, 52, 52, 28, 52, + 52, 52, 52, 34, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, - 52, 52, 0, 54, 3, 6, 6, 13, 0, 0, - 0, 0, 14, 52, 53, 0, 52, 52, 52, 52, - 52, 52, 52, 52, 52, 52, 52, 28, 52, 52, - 52, 52, 34, 52, 52, 52, 52, 52, 52, 52, - 52, 52, 52, 52, 52, 52, 52, 52, 52, 9, - 0, 11, 8, 7, 53, 52, 16, 52, 52, 52, - 52, 52, 52, 52, 52, 52, 52, 52, 52, 30, - - 31, 52, 52, 52, 37, 52, 52, 52, 40, 52, - 52, 52, 52, 52, 52, 52, 52, 49, 52, 0, - 52, 52, 18, 19, 52, 52, 22, 52, 25, 52, - 52, 52, 52, 52, 52, 52, 52, 52, 52, 42, - 52, 52, 45, 46, 4, 52, 48, 52, 52, 0, - 10, 52, 17, 52, 52, 52, 24, 5, 26, 52, - 52, 52, 52, 52, 52, 52, 52, 52, 43, 52, - 47, 52, 51, 52, 52, 52, 23, 52, 52, 52, - 52, 36, 52, 39, 52, 52, 44, 52, 52, 52, - 21, 52, 29, 52, 52, 52, 35, 52, 52, 52, - - 52, 27, 32, 33, 52, 52, 52, 52, 52, 38, - 41, 52, 15, 20, 52, 52, 52, 52, 50, 0 + 55, 9, 0, 11, 8, 7, 53, 52, 16, 52, + 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, + + 52, 30, 31, 52, 52, 52, 37, 52, 52, 52, + 40, 52, 52, 52, 52, 52, 52, 52, 52, 49, + 52, 0, 54, 0, 0, 52, 52, 18, 19, 52, + 52, 22, 52, 25, 52, 52, 52, 52, 52, 52, + 52, 52, 52, 52, 42, 52, 52, 45, 46, 4, + 52, 48, 52, 52, 0, 10, 52, 17, 52, 52, + 52, 24, 5, 26, 52, 52, 52, 52, 52, 52, + 52, 52, 52, 43, 52, 47, 52, 51, 52, 52, + 52, 23, 52, 52, 52, 52, 36, 52, 39, 52, + 52, 44, 52, 52, 52, 21, 52, 29, 52, 52, + + 52, 35, 52, 52, 52, 52, 27, 32, 33, 52, + 52, 52, 52, 52, 38, 41, 52, 15, 20, 52, + 52, 52, 52, 50, 0 } ; static const YY_CHAR yy_ec[256] = @@ -418,11 +420,11 @@ static const YY_CHAR yy_ec[256] = 6, 1, 1, 1, 14, 14, 14, 14, 15, 14, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, - 6, 1, 6, 1, 17, 1, 18, 19, 20, 21, + 6, 17, 6, 1, 18, 1, 19, 20, 21, 22, - 22, 23, 24, 25, 26, 16, 16, 27, 28, 29, - 30, 31, 16, 32, 33, 34, 35, 36, 37, 38, - 39, 40, 6, 1, 6, 1, 1, 1, 1, 1, + 23, 24, 25, 26, 27, 16, 16, 28, 29, 30, + 31, 32, 16, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 6, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, @@ -439,156 +441,163 @@ static const YY_CHAR yy_ec[256] = 1, 1, 1, 1, 1 } ; -static const YY_CHAR yy_meta[41] = +static const YY_CHAR yy_meta[42] = { 0, 1, 1, 2, 1, 1, 1, 1, 1, 1, 3, - 3, 3, 1, 4, 4, 5, 5, 4, 4, 4, - 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, - 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 + 3, 3, 1, 4, 4, 5, 1, 5, 4, 4, + 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5 } ; -static const flex_int16_t yy_base[227] = +static const flex_int16_t yy_base[233] = { 0, - 0, 0, 276, 277, 277, 277, 271, 0, 277, 31, - 265, 35, 18, 260, 0, 255, 20, 241, 33, 248, - 26, 40, 31, 247, 43, 36, 44, 246, 52, 56, - 249, 241, 261, 277, 0, 0, 0, 277, 73, 82, - 69, 0, 277, 0, 0, 247, 58, 231, 232, 228, - 231, 236, 225, 75, 230, 226, 226, 0, 78, 220, - 215, 223, 0, 217, 218, 227, 228, 213, 212, 215, - 217, 211, 209, 219, 205, 208, 207, 205, 214, 89, - 95, 103, 92, 0, 0, 205, 0, 216, 206, 210, - 198, 212, 207, 202, 199, 193, 207, 204, 196, 0, - - 0, 196, 195, 189, 0, 198, 188, 191, 0, 179, - 179, 188, 179, 183, 189, 183, 187, 182, 175, 109, - 172, 166, 0, 0, 170, 168, 176, 178, 0, 178, - 165, 164, 162, 168, 167, 159, 167, 160, 158, 173, - 167, 159, 0, 0, 0, 165, 0, 168, 163, 112, - 115, 166, 0, 151, 155, 158, 0, 0, 0, 154, - 158, 152, 151, 142, 155, 153, 150, 152, 0, 147, - 0, 141, 0, 135, 150, 133, 0, 136, 143, 124, - 123, 0, 144, 0, 122, 130, 0, 125, 132, 131, - 0, 127, 0, 133, 132, 119, 0, 123, 134, 120, - - 120, 0, 0, 0, 126, 113, 97, 89, 75, 0, - 0, 86, 0, 0, 52, 38, 34, 43, 0, 277, - 127, 132, 135, 137, 140, 142 + 0, 0, 298, 299, 299, 299, 38, 0, 299, 33, + 288, 47, 37, 283, 0, 277, 20, 263, 32, 270, + 36, 46, 37, 269, 49, 45, 52, 268, 61, 65, + 271, 263, 73, 299, 284, 0, 0, 97, 299, 83, + 92, 62, 0, 299, 0, 0, 269, 49, 253, 254, + 250, 253, 258, 247, 61, 252, 248, 248, 0, 89, + 242, 237, 245, 0, 239, 240, 249, 250, 235, 234, + 237, 239, 233, 231, 241, 227, 230, 229, 227, 236, + 109, 104, 111, 118, 107, 0, 0, 227, 0, 238, + 228, 232, 220, 234, 229, 224, 221, 215, 229, 226, + + 218, 0, 0, 218, 217, 211, 0, 220, 210, 213, + 0, 201, 201, 210, 201, 205, 211, 205, 209, 204, + 197, 121, 299, 0, 124, 194, 188, 0, 0, 192, + 190, 198, 200, 0, 200, 187, 186, 184, 190, 189, + 181, 189, 182, 180, 195, 189, 181, 0, 0, 0, + 187, 0, 190, 185, 129, 132, 188, 0, 173, 177, + 180, 0, 0, 0, 176, 180, 174, 173, 164, 177, + 175, 172, 174, 0, 169, 0, 163, 0, 157, 172, + 155, 0, 158, 165, 146, 145, 0, 166, 0, 144, + 152, 0, 147, 154, 153, 0, 149, 0, 155, 154, + + 141, 0, 145, 156, 142, 142, 0, 0, 0, 148, + 148, 133, 121, 111, 0, 0, 114, 0, 0, 112, + 80, 42, 32, 0, 299, 144, 149, 152, 154, 157, + 159, 164 } ; -static const flex_int16_t yy_def[227] = +static const flex_int16_t yy_def[233] = { 0, - 220, 1, 220, 220, 220, 220, 221, 222, 220, 220, - 220, 220, 12, 220, 223, 224, 223, 223, 223, 223, - 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, - 223, 223, 221, 220, 222, 12, 13, 220, 220, 220, - 220, 225, 220, 223, 226, 224, 223, 223, 223, 223, - 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, - 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, - 223, 223, 223, 223, 223, 223, 223, 223, 223, 220, - 220, 220, 220, 225, 226, 223, 223, 223, 223, 223, - 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, - - 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, - 223, 223, 223, 223, 223, 223, 223, 223, 223, 220, - 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, - 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, - 223, 223, 223, 223, 223, 223, 223, 223, 223, 220, - 220, 223, 223, 223, 223, 223, 223, 223, 223, 223, - 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, - 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, - 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, - 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, - - 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, - 223, 223, 223, 223, 223, 223, 223, 223, 223, 0, - 220, 220, 220, 220, 220, 220 + 225, 1, 225, 225, 225, 225, 226, 227, 225, 225, + 225, 225, 225, 225, 228, 229, 228, 228, 228, 228, + 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, + 228, 228, 226, 225, 226, 227, 12, 225, 225, 225, + 225, 225, 230, 225, 228, 231, 229, 228, 228, 228, + 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, + 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, + 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, + 232, 225, 225, 225, 225, 230, 231, 228, 228, 228, + 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, + + 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, + 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, + 228, 232, 225, 232, 225, 228, 228, 228, 228, 228, + 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, + 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, + 228, 228, 228, 228, 225, 225, 228, 228, 228, 228, + 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, + 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, + 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, + 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, + + 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, + 228, 228, 228, 228, 228, 228, 228, 228, 228, 228, + 228, 228, 228, 228, 0, 225, 225, 225, 225, 225, + 225, 225 } ; -static const flex_int16_t yy_nxt[318] = +static const flex_int16_t yy_nxt[341] = { 0, 4, 5, 6, 7, 8, 9, 4, 10, 11, 12, - 13, 13, 14, 15, 15, 15, 16, 17, 18, 19, - 20, 21, 22, 15, 15, 23, 24, 25, 15, 26, - 27, 28, 29, 30, 15, 31, 32, 15, 15, 15, - 36, 37, 37, 39, 37, 37, 37, 220, 47, 40, - 50, 48, 53, 58, 54, 220, 40, 55, 63, 59, - 61, 65, 51, 219, 41, 218, 56, 217, 62, 68, - 64, 216, 42, 69, 57, 66, 70, 73, 83, 83, - 74, 71, 80, 80, 80, 72, 86, 75, 81, 81, - 76, 82, 82, 82, 77, 94, 87, 99, 80, 80, - - 80, 83, 83, 120, 82, 82, 82, 215, 214, 95, - 120, 100, 82, 82, 82, 150, 150, 213, 151, 151, - 151, 151, 151, 151, 151, 151, 151, 33, 212, 33, - 33, 33, 35, 211, 35, 35, 35, 44, 44, 44, - 45, 45, 84, 84, 85, 85, 85, 210, 209, 208, - 207, 206, 205, 204, 203, 202, 201, 200, 199, 198, - 197, 196, 195, 194, 193, 192, 191, 190, 189, 188, - 187, 186, 185, 184, 183, 182, 181, 180, 179, 178, - 177, 176, 175, 174, 173, 172, 171, 170, 169, 168, - 167, 166, 165, 164, 163, 162, 161, 160, 159, 158, - - 157, 156, 155, 154, 153, 152, 149, 148, 147, 146, - 145, 144, 143, 142, 141, 140, 139, 138, 137, 136, - 135, 134, 133, 132, 131, 130, 129, 128, 127, 126, - 125, 124, 123, 122, 121, 119, 118, 117, 116, 115, - 114, 113, 112, 111, 110, 109, 108, 107, 106, 105, - 104, 103, 102, 101, 98, 97, 96, 93, 92, 91, - 90, 89, 88, 46, 34, 79, 78, 67, 60, 52, - 49, 46, 43, 38, 34, 220, 3, 220, 220, 220, - 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, - 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, - - 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, - 220, 220, 220, 220, 220, 220, 220 + 13, 13, 14, 15, 15, 15, 4, 16, 17, 18, + 19, 20, 21, 22, 15, 15, 23, 24, 25, 15, + 26, 27, 28, 29, 30, 15, 31, 32, 15, 15, + 15, 34, 37, 38, 38, 40, 38, 38, 38, 48, + 51, 41, 49, 224, 35, 40, 38, 38, 38, 41, + 59, 41, 52, 54, 56, 55, 60, 62, 64, 41, + 66, 85, 85, 57, 223, 63, 34, 42, 88, 69, + 65, 58, 96, 70, 67, 43, 71, 74, 89, 35, + 75, 72, 82, 82, 82, 73, 97, 76, 83, 83, + + 77, 84, 84, 84, 78, 40, 38, 38, 38, 101, + 222, 41, 123, 82, 82, 82, 85, 85, 125, 41, + 84, 84, 84, 102, 123, 124, 125, 84, 84, 84, + 155, 155, 221, 156, 156, 156, 220, 124, 156, 156, + 156, 156, 156, 156, 33, 219, 33, 33, 33, 36, + 218, 36, 36, 36, 45, 45, 45, 46, 46, 86, + 86, 87, 87, 87, 122, 217, 122, 122, 122, 216, + 215, 214, 213, 212, 211, 210, 209, 208, 207, 206, + 205, 204, 203, 202, 201, 200, 199, 198, 197, 196, + 195, 194, 193, 192, 191, 190, 189, 188, 187, 186, + + 185, 184, 183, 182, 181, 180, 179, 178, 177, 176, + 175, 174, 173, 172, 171, 170, 169, 168, 167, 166, + 165, 164, 163, 162, 161, 160, 159, 158, 157, 154, + 153, 152, 151, 150, 149, 148, 147, 146, 145, 144, + 143, 142, 141, 140, 139, 138, 137, 136, 135, 134, + 133, 132, 131, 130, 129, 128, 127, 126, 121, 120, + 119, 118, 117, 116, 115, 114, 113, 112, 111, 110, + 109, 108, 107, 106, 105, 104, 103, 100, 99, 98, + 95, 94, 93, 92, 91, 90, 47, 81, 80, 79, + 68, 61, 53, 50, 47, 44, 39, 225, 3, 225, + + 225, 225, 225, 225, 225, 225, 225, 225, 225, 225, + 225, 225, 225, 225, 225, 225, 225, 225, 225, 225, + 225, 225, 225, 225, 225, 225, 225, 225, 225, 225, + 225, 225, 225, 225, 225, 225, 225, 225, 225, 225 } ; -static const flex_int16_t yy_chk[318] = +static const flex_int16_t yy_chk[341] = { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 10, 10, 10, 12, 12, 12, 12, 13, 17, 12, - 19, 17, 21, 23, 21, 13, 12, 22, 26, 23, - 25, 27, 19, 218, 12, 217, 22, 216, 25, 29, - 26, 215, 12, 29, 22, 27, 29, 30, 41, 41, - 30, 29, 39, 39, 39, 29, 47, 30, 40, 40, - 30, 40, 40, 40, 30, 54, 47, 59, 80, 80, - - 80, 83, 83, 80, 81, 81, 81, 212, 209, 54, - 80, 59, 82, 82, 82, 120, 120, 208, 120, 120, - 120, 150, 150, 150, 151, 151, 151, 221, 207, 221, - 221, 221, 222, 206, 222, 222, 222, 223, 223, 223, - 224, 224, 225, 225, 226, 226, 226, 205, 201, 200, - 199, 198, 196, 195, 194, 192, 190, 189, 188, 186, - 185, 183, 181, 180, 179, 178, 176, 175, 174, 172, - 170, 168, 167, 166, 165, 164, 163, 162, 161, 160, - 156, 155, 154, 152, 149, 148, 146, 142, 141, 140, - 139, 138, 137, 136, 135, 134, 133, 132, 131, 130, - - 128, 127, 126, 125, 122, 121, 119, 118, 117, 116, - 115, 114, 113, 112, 111, 110, 108, 107, 106, 104, - 103, 102, 99, 98, 97, 96, 95, 94, 93, 92, - 91, 90, 89, 88, 86, 79, 78, 77, 76, 75, - 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, - 64, 62, 61, 60, 57, 56, 55, 53, 52, 51, - 50, 49, 48, 46, 33, 32, 31, 28, 24, 20, - 18, 16, 14, 11, 7, 3, 220, 220, 220, 220, - 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, - 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, - - 220, 220, 220, 220, 220, 220, 220, 220, 220, 220, - 220, 220, 220, 220, 220, 220, 220 + 1, 7, 10, 10, 10, 13, 13, 13, 13, 17, + 19, 13, 17, 223, 7, 12, 12, 12, 12, 13, + 23, 12, 19, 21, 22, 21, 23, 25, 26, 12, + 27, 42, 42, 22, 222, 25, 33, 12, 48, 29, + 26, 22, 55, 29, 27, 12, 29, 30, 48, 33, + 30, 29, 40, 40, 40, 29, 55, 30, 41, 41, + + 30, 41, 41, 41, 30, 38, 38, 38, 38, 60, + 221, 38, 81, 82, 82, 82, 85, 85, 82, 38, + 83, 83, 83, 60, 122, 81, 82, 84, 84, 84, + 125, 125, 220, 125, 125, 125, 217, 122, 155, 155, + 155, 156, 156, 156, 226, 214, 226, 226, 226, 227, + 213, 227, 227, 227, 228, 228, 228, 229, 229, 230, + 230, 231, 231, 231, 232, 212, 232, 232, 232, 211, + 210, 206, 205, 204, 203, 201, 200, 199, 197, 195, + 194, 193, 191, 190, 188, 186, 185, 184, 183, 181, + 180, 179, 177, 175, 173, 172, 171, 170, 169, 168, + + 167, 166, 165, 161, 160, 159, 157, 154, 153, 151, + 147, 146, 145, 144, 143, 142, 141, 140, 139, 138, + 137, 136, 135, 133, 132, 131, 130, 127, 126, 121, + 120, 119, 118, 117, 116, 115, 114, 113, 112, 110, + 109, 108, 106, 105, 104, 101, 100, 99, 98, 97, + 96, 95, 94, 93, 92, 91, 90, 88, 80, 79, + 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, + 68, 67, 66, 65, 63, 62, 61, 58, 57, 56, + 54, 53, 52, 51, 50, 49, 47, 35, 32, 31, + 28, 24, 20, 18, 16, 14, 11, 3, 225, 225, + + 225, 225, 225, 225, 225, 225, 225, 225, 225, 225, + 225, 225, 225, 225, 225, 225, 225, 225, 225, 225, + 225, 225, 225, 225, 225, 225, 225, 225, 225, 225, + 225, 225, 225, 225, 225, 225, 225, 225, 225, 225 } ; /* Table of booleans, true if rule could match eol. */ -static const flex_int32_t yy_rule_can_match_eol[57] = +static const flex_int32_t yy_rule_can_match_eol[58] = { 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; +1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; /* The intent behind this definition is that it'll catch * any uses of REJECT which flex missed. @@ -597,7 +606,7 @@ static const flex_int32_t yy_rule_can_match_eol[57] = #define yymore() yymore_used_but_not_detected #define YY_MORE_ADJ 0 #define YY_RESTORE_YY_MORE_OFFSET -#line 1 "./gecode/flatzinc/lexer.lxx" +#line 1 "gecode/flatzinc/lexer.lxx" /* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ /* * Main authors: @@ -630,7 +639,7 @@ static const flex_int32_t yy_rule_can_match_eol[57] = * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ -#line 40 "./gecode/flatzinc/lexer.lxx" +#line 40 "gecode/flatzinc/lexer.lxx" #if defined __GNUC__ #pragma GCC diagnostic ignored "-Wunused-function" #pragma GCC diagnostic ignored "-Wunused-parameter" @@ -644,6 +653,8 @@ void yyerror(void*, const char*); #include #include +#include +#include #include @@ -661,11 +672,39 @@ bool parseInt(const char* text, int& value) { return true; } +bool parseCaptureInt(const char* text, int& value) { + bool negative=*text=='-';if(negative)++text; + unsigned radix=10; + if(text[0]=='0'&&text[1]=='x'){radix=16;text+=2;} + else if(text[0]=='0'&&text[1]=='o'){radix=8;text+=2;} + const std::uint64_t maximum=negative?static_cast(-static_cast(Gecode::Int::Limits::min)): + static_cast(Gecode::Int::Limits::max); + std::uint64_t result=0;if(!*text)return false; + for(;*text;++text){ + const unsigned char c=static_cast(*text); + unsigned digit=c>='0'&&c<='9'?c-'0':c>='a'&&c<='f'?c-'a'+10:c>='A'&&c<='F'?c-'A'+10:99; + if(digit>=radix||result>(maximum-digit)/radix)return false; + result=result*radix+digit; + } + value=static_cast(negative?-static_cast(result):static_cast(result));return true; +} + +bool parseCaptureFloat(const char* text,double& value) { + std::istringstream input(text);input.imbue(std::locale::classic()); + input >> std::noskipws >> value; + if(input.fail() || !input.eof() || !std::isfinite(value)) return false; + if(value==0) { + for(const char* p=text;*p && *p!='e' && *p!='E';++p) + if(*p>='1' && *p<='9')return false; // nonzero literal underflowed to zero + } + return true; +} + int yy_input_proc(char* buf, int size, yyscan_t yyscanner); #define YY_INPUT(buf, result, max_size) \ result = yy_input_proc(buf, max_size, yyscanner); -#line 667 "gecode/flatzinc/lexer.yy.cpp" -#line 668 "gecode/flatzinc/lexer.yy.cpp" +#line 706 "gecode/flatzinc/lexer.yy.cpp" +#line 707 "gecode/flatzinc/lexer.yy.cpp" #define INITIAL 0 @@ -694,8 +733,8 @@ struct yyguts_t size_t yy_buffer_stack_max; /**< capacity of stack. */ YY_BUFFER_STATE * yy_buffer_stack; /**< Stack as an array. */ char yy_hold_char; - int yy_n_chars; - int yyleng_r; + yy_size_t yy_n_chars; + yy_size_t yyleng_r; char *yy_c_buf_p; int yy_init; int yy_start; @@ -722,7 +761,7 @@ static int yy_init_globals ( yyscan_t yyscanner ); /* This must go here because YYSTYPE and YYLTYPE are included * from bison output in section 1.*/ # define yylval yyg->yylval_r - + int yylex_init (yyscan_t* scanner); int yylex_init_extra ( YY_EXTRA_TYPE user_defined, yyscan_t* scanner); @@ -748,7 +787,7 @@ FILE *yyget_out ( yyscan_t yyscanner ); void yyset_out ( FILE * _out_str , yyscan_t yyscanner ); - int yyget_leng ( yyscan_t yyscanner ); + yy_size_t yyget_leng ( yyscan_t yyscanner ); char *yyget_text ( yyscan_t yyscanner ); @@ -777,9 +816,9 @@ extern int yywrap ( yyscan_t yyscanner ); #endif #ifndef YY_NO_UNPUT - + static void yyunput ( int c, char *buf_ptr , yyscan_t yyscanner); - + #endif #ifndef yytext_ptr @@ -825,7 +864,7 @@ static int input ( yyscan_t yyscanner ); if ( YY_CURRENT_BUFFER_LVALUE->yy_is_interactive ) \ { \ int c = '*'; \ - int n; \ + yy_size_t n; \ for ( n = 0; n < max_size && \ (c = getc( yyin )) != EOF && c != '\n'; ++n ) \ buf[n] = (char) c; \ @@ -939,10 +978,10 @@ YY_DECL } { -#line 75 "./gecode/flatzinc/lexer.lxx" +#line 105 "gecode/flatzinc/lexer.lxx" -#line 945 "gecode/flatzinc/lexer.yy.cpp" +#line 984 "gecode/flatzinc/lexer.yy.cpp" while ( /*CONSTCOND*/1 ) /* loops until end-of-file is reached */ { @@ -969,13 +1008,13 @@ YY_DECL while ( yy_chk[yy_base[yy_current_state] + yy_c] != yy_current_state ) { yy_current_state = (int) yy_def[yy_current_state]; - if ( yy_current_state >= 221 ) + if ( yy_current_state >= 226 ) yy_c = yy_meta[yy_c]; } yy_current_state = yy_nxt[yy_base[yy_current_state] + yy_c]; ++yy_cp; } - while ( yy_base[yy_current_state] != 277 ); + while ( yy_base[yy_current_state] != 299 ); yy_find_action: yy_act = yy_accept[yy_current_state]; @@ -990,10 +1029,10 @@ YY_DECL if ( yy_act != YY_END_OF_BUFFER && yy_rule_can_match_eol[yy_act] ) { - int yyl; + yy_size_t yyl; for ( yyl = 0; yyl < yyleng; ++yyl ) if ( yytext[yyl] == '\n' ) - + do{ yylineno++; yycolumn=0; }while(0) @@ -1014,33 +1053,33 @@ YY_DECL case 1: /* rule 1 can match eol */ YY_RULE_SETUP -#line 77 "./gecode/flatzinc/lexer.lxx" +#line 107 "gecode/flatzinc/lexer.lxx" { /*yylineno++;*/ /* ignore EOL */ } YY_BREAK case 2: YY_RULE_SETUP -#line 78 "./gecode/flatzinc/lexer.lxx" +#line 108 "gecode/flatzinc/lexer.lxx" { /* ignore whitespace */ } YY_BREAK case 3: YY_RULE_SETUP -#line 79 "./gecode/flatzinc/lexer.lxx" +#line 109 "gecode/flatzinc/lexer.lxx" { /* ignore comments */ } YY_BREAK case 4: YY_RULE_SETUP -#line 81 "./gecode/flatzinc/lexer.lxx" +#line 111 "gecode/flatzinc/lexer.lxx" { yylval->iValue = 1; return FZ_BOOL_LIT; } YY_BREAK case 5: YY_RULE_SETUP -#line 82 "./gecode/flatzinc/lexer.lxx" +#line 112 "gecode/flatzinc/lexer.lxx" { yylval->iValue = 0; return FZ_BOOL_LIT; } YY_BREAK case 6: YY_RULE_SETUP -#line 83 "./gecode/flatzinc/lexer.lxx" -{ if (parseInt(yytext,yylval->iValue)) +#line 113 "gecode/flatzinc/lexer.lxx" +{ if (static_cast(yyextra)->capture ? parseCaptureInt(yytext,yylval->iValue) : parseInt(yytext,yylval->iValue)) return FZ_INT_LIT; else yyerror(("The literal '" + std::string(yytext) @@ -1052,8 +1091,8 @@ YY_RULE_SETUP YY_BREAK case 7: YY_RULE_SETUP -#line 92 "./gecode/flatzinc/lexer.lxx" -{ if (parseInt(yytext,yylval->iValue)) +#line 122 "gecode/flatzinc/lexer.lxx" +{ if (static_cast(yyextra)->capture ? parseCaptureInt(yytext,yylval->iValue) : parseInt(yytext,yylval->iValue)) return FZ_INT_LIT; else yyerror(("The literal '" + std::string(yytext) @@ -1065,8 +1104,8 @@ YY_RULE_SETUP YY_BREAK case 8: YY_RULE_SETUP -#line 101 "./gecode/flatzinc/lexer.lxx" -{ if (parseInt(yytext,yylval->iValue)) +#line 131 "gecode/flatzinc/lexer.lxx" +{ if (static_cast(yyextra)->capture ? parseCaptureInt(yytext,yylval->iValue) : parseInt(yytext,yylval->iValue)) return FZ_INT_LIT; else yyerror(("The literal '" + std::string(yytext) @@ -1078,251 +1117,270 @@ YY_RULE_SETUP YY_BREAK case 9: YY_RULE_SETUP -#line 110 "./gecode/flatzinc/lexer.lxx" -{ yylval->dValue = strtod(yytext,NULL); +#line 140 "gecode/flatzinc/lexer.lxx" +{ if(static_cast(yyextra)->capture) { + if(!parseCaptureFloat(yytext,yylval->dValue)) yyerror("Float literal is nonfinite or outside representable range"); + } else yylval->dValue = strtod(yytext,NULL); return FZ_FLOAT_LIT; } YY_BREAK case 10: YY_RULE_SETUP -#line 112 "./gecode/flatzinc/lexer.lxx" -{ yylval->dValue = strtod(yytext,NULL); +#line 144 "gecode/flatzinc/lexer.lxx" +{ if(static_cast(yyextra)->capture) { + if(!parseCaptureFloat(yytext,yylval->dValue)) yyerror("Float literal is nonfinite or outside representable range"); + } else yylval->dValue = strtod(yytext,NULL); return FZ_FLOAT_LIT; } YY_BREAK case 11: YY_RULE_SETUP -#line 114 "./gecode/flatzinc/lexer.lxx" -{ yylval->dValue = strtod(yytext,NULL); +#line 148 "gecode/flatzinc/lexer.lxx" +{ if(static_cast(yyextra)->capture) { + if(!parseCaptureFloat(yytext,yylval->dValue)) yyerror("Float literal is nonfinite or outside representable range"); + } else yylval->dValue = strtod(yytext,NULL); return FZ_FLOAT_LIT; } YY_BREAK case 12: YY_RULE_SETUP -#line 116 "./gecode/flatzinc/lexer.lxx" +#line 152 "gecode/flatzinc/lexer.lxx" { return *yytext; } YY_BREAK case 13: YY_RULE_SETUP -#line 117 "./gecode/flatzinc/lexer.lxx" +#line 153 "gecode/flatzinc/lexer.lxx" { return FZ_DOTDOT; } YY_BREAK case 14: YY_RULE_SETUP -#line 118 "./gecode/flatzinc/lexer.lxx" +#line 154 "gecode/flatzinc/lexer.lxx" { return FZ_COLONCOLON; } YY_BREAK case 15: YY_RULE_SETUP -#line 119 "./gecode/flatzinc/lexer.lxx" +#line 155 "gecode/flatzinc/lexer.lxx" { return FZ_ANNOTATION; } YY_BREAK case 16: YY_RULE_SETUP -#line 120 "./gecode/flatzinc/lexer.lxx" +#line 156 "gecode/flatzinc/lexer.lxx" { return FZ_ANY; } YY_BREAK case 17: YY_RULE_SETUP -#line 121 "./gecode/flatzinc/lexer.lxx" +#line 157 "gecode/flatzinc/lexer.lxx" { return FZ_ARRAY; } YY_BREAK case 18: YY_RULE_SETUP -#line 122 "./gecode/flatzinc/lexer.lxx" +#line 158 "gecode/flatzinc/lexer.lxx" { return FZ_BOOL; } YY_BREAK case 19: YY_RULE_SETUP -#line 123 "./gecode/flatzinc/lexer.lxx" +#line 159 "gecode/flatzinc/lexer.lxx" { return FZ_CASE; } YY_BREAK case 20: YY_RULE_SETUP -#line 124 "./gecode/flatzinc/lexer.lxx" +#line 160 "gecode/flatzinc/lexer.lxx" { return FZ_CONSTRAINT; } YY_BREAK case 21: YY_RULE_SETUP -#line 125 "./gecode/flatzinc/lexer.lxx" +#line 161 "gecode/flatzinc/lexer.lxx" { return FZ_DEFAULT; } YY_BREAK case 22: YY_RULE_SETUP -#line 126 "./gecode/flatzinc/lexer.lxx" +#line 162 "gecode/flatzinc/lexer.lxx" { return FZ_ELSE; } YY_BREAK case 23: YY_RULE_SETUP -#line 127 "./gecode/flatzinc/lexer.lxx" +#line 163 "gecode/flatzinc/lexer.lxx" { return FZ_ELSEIF; } YY_BREAK case 24: YY_RULE_SETUP -#line 128 "./gecode/flatzinc/lexer.lxx" +#line 164 "gecode/flatzinc/lexer.lxx" { return FZ_ENDIF; } YY_BREAK case 25: YY_RULE_SETUP -#line 129 "./gecode/flatzinc/lexer.lxx" +#line 165 "gecode/flatzinc/lexer.lxx" { return FZ_ENUM; } YY_BREAK case 26: YY_RULE_SETUP -#line 130 "./gecode/flatzinc/lexer.lxx" +#line 166 "gecode/flatzinc/lexer.lxx" { return FZ_FLOAT; } YY_BREAK case 27: YY_RULE_SETUP -#line 131 "./gecode/flatzinc/lexer.lxx" +#line 167 "gecode/flatzinc/lexer.lxx" { return FZ_FUNCTION; } YY_BREAK case 28: YY_RULE_SETUP -#line 132 "./gecode/flatzinc/lexer.lxx" +#line 168 "gecode/flatzinc/lexer.lxx" { return FZ_IF; } YY_BREAK case 29: YY_RULE_SETUP -#line 133 "./gecode/flatzinc/lexer.lxx" +#line 169 "gecode/flatzinc/lexer.lxx" { return FZ_INCLUDE; } YY_BREAK case 30: YY_RULE_SETUP -#line 134 "./gecode/flatzinc/lexer.lxx" +#line 170 "gecode/flatzinc/lexer.lxx" { return FZ_INT; } YY_BREAK case 31: YY_RULE_SETUP -#line 135 "./gecode/flatzinc/lexer.lxx" +#line 171 "gecode/flatzinc/lexer.lxx" { return FZ_LET; } YY_BREAK case 32: YY_RULE_SETUP -#line 136 "./gecode/flatzinc/lexer.lxx" +#line 172 "gecode/flatzinc/lexer.lxx" { yylval->bValue = false; return FZ_MAXIMIZE; } YY_BREAK case 33: YY_RULE_SETUP -#line 137 "./gecode/flatzinc/lexer.lxx" +#line 173 "gecode/flatzinc/lexer.lxx" { yylval->bValue = true; return FZ_MINIMIZE; } YY_BREAK case 34: YY_RULE_SETUP -#line 138 "./gecode/flatzinc/lexer.lxx" +#line 174 "gecode/flatzinc/lexer.lxx" { return FZ_OF; } YY_BREAK case 35: YY_RULE_SETUP -#line 139 "./gecode/flatzinc/lexer.lxx" +#line 175 "gecode/flatzinc/lexer.lxx" { return FZ_SATISFY; } YY_BREAK case 36: YY_RULE_SETUP -#line 140 "./gecode/flatzinc/lexer.lxx" +#line 176 "gecode/flatzinc/lexer.lxx" { return FZ_OUTPUT; } YY_BREAK case 37: YY_RULE_SETUP -#line 141 "./gecode/flatzinc/lexer.lxx" +#line 177 "gecode/flatzinc/lexer.lxx" { yylval->bValue = false; return FZ_PAR; } YY_BREAK case 38: YY_RULE_SETUP -#line 142 "./gecode/flatzinc/lexer.lxx" +#line 178 "gecode/flatzinc/lexer.lxx" { return FZ_PREDICATE; } YY_BREAK case 39: YY_RULE_SETUP -#line 143 "./gecode/flatzinc/lexer.lxx" +#line 179 "gecode/flatzinc/lexer.lxx" { return FZ_RECORD; } YY_BREAK case 40: YY_RULE_SETUP -#line 144 "./gecode/flatzinc/lexer.lxx" +#line 180 "gecode/flatzinc/lexer.lxx" { return FZ_SET; } YY_BREAK case 41: YY_RULE_SETUP -#line 145 "./gecode/flatzinc/lexer.lxx" +#line 181 "gecode/flatzinc/lexer.lxx" { return FZ_SHOWCOND; } YY_BREAK case 42: YY_RULE_SETUP -#line 146 "./gecode/flatzinc/lexer.lxx" +#line 182 "gecode/flatzinc/lexer.lxx" { return FZ_SHOW; } YY_BREAK case 43: YY_RULE_SETUP -#line 147 "./gecode/flatzinc/lexer.lxx" +#line 183 "gecode/flatzinc/lexer.lxx" { return FZ_SOLVE; } YY_BREAK case 44: YY_RULE_SETUP -#line 148 "./gecode/flatzinc/lexer.lxx" +#line 184 "gecode/flatzinc/lexer.lxx" { return FZ_STRING; } YY_BREAK case 45: YY_RULE_SETUP -#line 149 "./gecode/flatzinc/lexer.lxx" +#line 185 "gecode/flatzinc/lexer.lxx" { return FZ_TEST; } YY_BREAK case 46: YY_RULE_SETUP -#line 150 "./gecode/flatzinc/lexer.lxx" +#line 186 "gecode/flatzinc/lexer.lxx" { return FZ_THEN; } YY_BREAK case 47: YY_RULE_SETUP -#line 151 "./gecode/flatzinc/lexer.lxx" +#line 187 "gecode/flatzinc/lexer.lxx" { return FZ_TUPLE; } YY_BREAK case 48: YY_RULE_SETUP -#line 152 "./gecode/flatzinc/lexer.lxx" +#line 188 "gecode/flatzinc/lexer.lxx" { return FZ_TYPE; } YY_BREAK case 49: YY_RULE_SETUP -#line 153 "./gecode/flatzinc/lexer.lxx" +#line 189 "gecode/flatzinc/lexer.lxx" { yylval->bValue = true; return FZ_VAR; } YY_BREAK case 50: YY_RULE_SETUP -#line 154 "./gecode/flatzinc/lexer.lxx" +#line 190 "gecode/flatzinc/lexer.lxx" { return FZ_VARIANT_RECORD; } YY_BREAK case 51: YY_RULE_SETUP -#line 155 "./gecode/flatzinc/lexer.lxx" +#line 191 "gecode/flatzinc/lexer.lxx" { return FZ_WHERE; } YY_BREAK case 52: YY_RULE_SETUP -#line 156 "./gecode/flatzinc/lexer.lxx" +#line 192 "gecode/flatzinc/lexer.lxx" { yylval->sValue = strdup(yytext); return FZ_ID; } YY_BREAK case 53: YY_RULE_SETUP -#line 157 "./gecode/flatzinc/lexer.lxx" +#line 193 "gecode/flatzinc/lexer.lxx" { yylval->sValue = strdup(yytext); return FZ_U_ID; } YY_BREAK case 54: YY_RULE_SETUP -#line 158 "./gecode/flatzinc/lexer.lxx" +#line 194 "gecode/flatzinc/lexer.lxx" { + if(!static_cast(yyextra)->capture) { + // Preserve the legacy scanner's first-quote behavior. + const char* close=strchr(yytext+1,'"'); + yyless(static_cast(close-yytext+1)); + } yylval->sValue = strdup(yytext+1); yylval->sValue[strlen(yytext)-2] = 0; return FZ_STRING_LIT; } YY_BREAK case 55: YY_RULE_SETUP -#line 162 "./gecode/flatzinc/lexer.lxx" -{ yyerror("Unknown character"); } +#line 203 "gecode/flatzinc/lexer.lxx" +{ + yylval->sValue = strdup(yytext+1); + yylval->sValue[strlen(yytext)-2] = 0; + return FZ_STRING_LIT; } YY_BREAK case 56: YY_RULE_SETUP -#line 163 "./gecode/flatzinc/lexer.lxx" +#line 207 "gecode/flatzinc/lexer.lxx" +{ yyerror("Unknown character"); } + YY_BREAK +case 57: +YY_RULE_SETUP +#line 208 "gecode/flatzinc/lexer.lxx" ECHO; YY_BREAK -#line 1325 "gecode/flatzinc/lexer.yy.cpp" +#line 1383 "gecode/flatzinc/lexer.yy.cpp" case YY_STATE_EOF(INITIAL): yyterminate(); @@ -1510,7 +1568,7 @@ static int yy_get_next_buffer (yyscan_t yyscanner) else { - int num_to_read = + yy_size_t num_to_read = YY_CURRENT_BUFFER_LVALUE->yy_buf_size - number_to_move - 1; while ( num_to_read <= 0 ) @@ -1524,7 +1582,7 @@ static int yy_get_next_buffer (yyscan_t yyscanner) if ( b->yy_is_our_buffer ) { - int new_size = b->yy_buf_size * 2; + yy_size_t new_size = b->yy_buf_size * 2; if ( new_size <= 0 ) b->yy_buf_size += b->yy_buf_size / 8; @@ -1582,7 +1640,7 @@ static int yy_get_next_buffer (yyscan_t yyscanner) if ((yyg->yy_n_chars + number_to_move) > YY_CURRENT_BUFFER_LVALUE->yy_buf_size) { /* Extend the array by 50%, plus the number we really need. */ - int new_size = yyg->yy_n_chars + number_to_move + (yyg->yy_n_chars >> 1); + yy_size_t new_size = yyg->yy_n_chars + number_to_move + (yyg->yy_n_chars >> 1); YY_CURRENT_BUFFER_LVALUE->yy_ch_buf = (char *) yyrealloc( (void *) YY_CURRENT_BUFFER_LVALUE->yy_ch_buf, (yy_size_t) new_size , yyscanner ); if ( ! YY_CURRENT_BUFFER_LVALUE->yy_ch_buf ) @@ -1621,7 +1679,7 @@ static int yy_get_next_buffer (yyscan_t yyscanner) while ( yy_chk[yy_base[yy_current_state] + yy_c] != yy_current_state ) { yy_current_state = (int) yy_def[yy_current_state]; - if ( yy_current_state >= 221 ) + if ( yy_current_state >= 226 ) yy_c = yy_meta[yy_c]; } yy_current_state = yy_nxt[yy_base[yy_current_state] + yy_c]; @@ -1650,11 +1708,11 @@ static int yy_get_next_buffer (yyscan_t yyscanner) while ( yy_chk[yy_base[yy_current_state] + yy_c] != yy_current_state ) { yy_current_state = (int) yy_def[yy_current_state]; - if ( yy_current_state >= 221 ) + if ( yy_current_state >= 226 ) yy_c = yy_meta[yy_c]; } yy_current_state = yy_nxt[yy_base[yy_current_state] + yy_c]; - yy_is_jam = (yy_current_state == 220); + yy_is_jam = (yy_current_state == 225); (void)yyg; return yy_is_jam ? 0 : yy_current_state; @@ -1675,7 +1733,7 @@ static int yy_get_next_buffer (yyscan_t yyscanner) if ( yy_cp < YY_CURRENT_BUFFER_LVALUE->yy_ch_buf + 2 ) { /* need to shift things up to make room */ /* +2 for EOB chars. */ - int number_to_move = yyg->yy_n_chars + 2; + yy_size_t number_to_move = yyg->yy_n_chars + 2; char *dest = &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[ YY_CURRENT_BUFFER_LVALUE->yy_buf_size + 2]; char *source = @@ -1731,7 +1789,7 @@ static int yy_get_next_buffer (yyscan_t yyscanner) else { /* need more input */ - int offset = (int) (yyg->yy_c_buf_p - yyg->yytext_ptr); + yy_size_t offset = yyg->yy_c_buf_p - yyg->yytext_ptr; ++yyg->yy_c_buf_p; switch ( yy_get_next_buffer( yyscanner ) ) @@ -1778,7 +1836,7 @@ static int yy_get_next_buffer (yyscan_t yyscanner) yyg->yy_hold_char = *++yyg->yy_c_buf_p; if ( c == '\n' ) - + do{ yylineno++; yycolumn=0; }while(0) @@ -1861,7 +1919,7 @@ static void yy_load_buffer_state (yyscan_t yyscanner) YY_BUFFER_STATE yy_create_buffer (FILE * file, int size , yyscan_t yyscanner) { YY_BUFFER_STATE b; - + b = (YY_BUFFER_STATE) yyalloc( sizeof( struct yy_buffer_state ) , yyscanner ); if ( ! b ) YY_FATAL_ERROR( "out of dynamic memory in yy_create_buffer()" ); @@ -1927,7 +1985,7 @@ static void yy_load_buffer_state (yyscan_t yyscanner) } b->yy_is_interactive = file ? (isatty( fileno(file) ) > 0) : 0; - + errno = oerrno; } @@ -2069,7 +2127,7 @@ static void yyensure_buffer_stack (yyscan_t yyscanner) YY_BUFFER_STATE yy_scan_buffer (char * base, yy_size_t size , yyscan_t yyscanner) { YY_BUFFER_STATE b; - + if ( size < 2 || base[size-2] != YY_END_OF_BUFFER_CHAR || base[size-1] != YY_END_OF_BUFFER_CHAR ) @@ -2105,7 +2163,7 @@ YY_BUFFER_STATE yy_scan_buffer (char * base, yy_size_t size , yyscan_t yyscann */ YY_BUFFER_STATE yy_scan_string (const char * yystr , yyscan_t yyscanner) { - + return yy_scan_bytes( yystr, (int) strlen(yystr) , yyscanner); } @@ -2116,13 +2174,13 @@ YY_BUFFER_STATE yy_scan_string (const char * yystr , yyscan_t yyscanner) * @param yyscanner The scanner object. * @return the newly allocated buffer state object. */ -YY_BUFFER_STATE yy_scan_bytes (const char * yybytes, int _yybytes_len , yyscan_t yyscanner) +YY_BUFFER_STATE yy_scan_bytes (const char * yybytes, yy_size_t _yybytes_len , yyscan_t yyscanner) { YY_BUFFER_STATE b; char *buf; yy_size_t n; - int i; - + yy_size_t i; + /* Get memory for full buffer, including space for trailing EOB's. */ n = (yy_size_t) (_yybytes_len + 2); buf = (char *) yyalloc( n , yyscanner ); @@ -2165,7 +2223,7 @@ static void yynoreturn yy_fatal_error (const char* msg , yyscan_t yyscanner) do \ { \ /* Undo effects of setting up yytext. */ \ - int yyless_macro_arg = (n); \ + yy_size_t yyless_macro_arg = (n); \ YY_LESS_LINENO(yyless_macro_arg);\ yytext[yyleng] = yyg->yy_hold_char; \ yyg->yy_c_buf_p = yytext + yyless_macro_arg; \ @@ -2195,7 +2253,7 @@ int yyget_lineno (yyscan_t yyscanner) if (! YY_CURRENT_BUFFER) return 0; - + return yylineno; } @@ -2208,7 +2266,7 @@ int yyget_column (yyscan_t yyscanner) if (! YY_CURRENT_BUFFER) return 0; - + return yycolumn; } @@ -2233,7 +2291,7 @@ FILE *yyget_out (yyscan_t yyscanner) /** Get the length of the current token. * @param yyscanner The scanner object. */ -int yyget_leng (yyscan_t yyscanner) +yy_size_t yyget_leng (yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; return yyleng; @@ -2270,7 +2328,7 @@ void yyset_lineno (int _line_number , yyscan_t yyscanner) /* lineno is only valid if an input buffer exists. */ if (! YY_CURRENT_BUFFER ) YY_FATAL_ERROR( "yyset_lineno called with no buffer" ); - + yylineno = _line_number; } @@ -2285,7 +2343,7 @@ void yyset_column (int _column_no , yyscan_t yyscanner) /* column is only valid if an input buffer exists. */ if (! YY_CURRENT_BUFFER ) YY_FATAL_ERROR( "yyset_column called with no buffer" ); - + yycolumn = _column_no; } @@ -2514,7 +2572,7 @@ void yyfree (void * ptr , yyscan_t yyscanner) #define YYTABLES_NAME "yytables" -#line 163 "./gecode/flatzinc/lexer.lxx" +#line 208 "gecode/flatzinc/lexer.lxx" int yy_input_proc(char* buf, int size, yyscan_t yyscanner) { Gecode::FlatZinc::ParserState* parm = @@ -2523,4 +2581,3 @@ int yy_input_proc(char* buf, int size, yyscan_t yyscanner) { // work around warning that yyunput is unused yyunput (0,buf,yyscanner); } - diff --git a/gecode/flatzinc/parser.hh b/gecode/flatzinc/parser.hh index ab8b6b0881..44260e1751 100644 --- a/gecode/flatzinc/parser.hh +++ b/gecode/flatzinc/parser.hh @@ -35,6 +35,7 @@ #define FLATZINC_PARSER_HH #include +#include // This is a workaround for a bug in flex that only shows up // with the Microsoft C++ compiler @@ -181,19 +182,45 @@ namespace Gecode { namespace FlatZinc { return SymbolEntry(ST_FLOATVALARRAY, i); } + class ParserState; + /** Private parser bridge; absent for every legacy parse entry point. */ + class CaptureParser { + public: + Capture::Options options; + Capture::Records records; + Capture::Status status = Capture::Status::Complete; + std::vector diagnostics; + std::array namespace_counts{}; + bool declarations_recorded = false; + bool solve_recorded = false; + explicit CaptureParser(const Capture::Options& value) : options(value) {} + void fail(ParserState&, Capture::Status, const std::string&); + bool array_size(ParserState&, int); + bool variable_count(ParserState&, int, bool initialized_array=false); + bool annotations(ParserState&, AST::Array*); + void declaration(ParserState&, const std::string&, AST::Array*, int output_length=-1); + void begin(ParserState&); + void constraint(ParserState&, const std::string&, AST::Array*, AST::Array*); + void objective(ParserState&, AST::Node*); + void solve(ParserState&, Capture::Method, AST::Array*); + void finish(ParserState&); + static void cleanup(ParserState&) noexcept; + }; + /// %State of the %FlatZinc parser class ParserState { public: ParserState(const std::string& b, std::ostream& err0, Gecode::FlatZinc::FlatZincSpace* fg0) - : buf(b.c_str()), pos(0), length(b.size()), fg(fg0), + : yyscanner(nullptr), buf(b.c_str()), pos(0), length(b.size()), fg(fg0), hadError(false), err(err0) {} ParserState(char* buf0, int length0, std::ostream& err0, Gecode::FlatZinc::FlatZincSpace* fg0) - : buf(buf0), pos(0), length(length0), fg(fg0), + : yyscanner(nullptr), buf(buf0), pos(0), length(length0), fg(fg0), hadError(false), err(err0) {} + CaptureParser* capture = nullptr; // nonowning; lifetime is the capture call void* yyscanner; const char* buf; unsigned int pos, length; diff --git a/gecode/flatzinc/parser.tab.cpp b/gecode/flatzinc/parser.tab.cpp index e2e818290b..b72ac91b18 100644 --- a/gecode/flatzinc/parser.tab.cpp +++ b/gecode/flatzinc/parser.tab.cpp @@ -67,7 +67,7 @@ /* First part of user prologue. */ -#line 37 "./gecode/flatzinc/parser.yxx" +#line 37 "gecode/flatzinc/parser.yxx" #define YYPARSE_PARAM parm #define YYLEX_PARAM static_cast(parm)->yyscanner @@ -183,6 +183,10 @@ AST::Node* getArrayElement(ParserState* pp, string id, int offset, } return new AST::FloatVar(pp->arrays[e.i+offset],n); } + case ST_BOOLVALARRAY: + if(!pp->capture) break; + if(offset>pp->arrays[e.i]) goto error; + return new AST::BoolLit(pp->arrays[e.i+offset]); case ST_INTVALARRAY: if (offset > pp->arrays[e.i]) goto error; @@ -233,18 +237,32 @@ AST::Node* getVarRefArg(ParserState* pp, string id, bool annotation = false) { void addDomainConstraint(ParserState* pp, std::string id, AST::Node* var, Option& dom) { - if (!dom()) + if (!dom()) { + if(pp->capture) delete var; return; + } + if(pp->capture && pp->domainConstraints.size()>=pp->capture->options.max_constraints) { + pp->capture->fail(*pp,Capture::Status::ResourceLimit,"declaration domain count exceeds capture limit"); + delete var;delete dom.some();dom=Option::none();return; + } AST::Array* args = new AST::Array(2); args->a[0] = var; args->a[1] = dom.some(); pp->domainConstraints.push_back(new ConExpr(id, args, NULL)); + if(pp->capture) dom=Option::none(); } void addDomainConstraint(ParserState* pp, AST::Node* var, Option* > dom) { - if (!dom()) + if (!dom()) { + if(pp->capture) delete var; return; + } + if(pp->capture && (pp->capture->options.max_constraints<2 || + pp->domainConstraints.size()>pp->capture->options.max_constraints-2)) { + pp->capture->fail(*pp,Capture::Status::ResourceLimit,"declaration domain count exceeds capture limit"); + delete var;delete dom.some();dom=Option*>::none();return; + } { AST::Array* args = new AST::Array(2); args->a[0] = new AST::FloatLit(dom.some()->first); @@ -259,6 +277,7 @@ void addDomainConstraint(ParserState* pp, AST::Node* var, pp->domainConstraints.push_back(new ConExpr("float_le", args, NULL)); } delete dom.some(); + if(pp->capture) dom=Option*>::none(); } int getBaseIntVar(ParserState* pp, int i) { @@ -304,6 +323,7 @@ int getBaseSetVar(ParserState* pp, int i) { */ void initfg(ParserState* pp) { + if (pp->capture) return; // capture never creates native variables/actors if (!pp->hadError) pp->fg->init(pp->intvars.size(), pp->boolvars.size(), @@ -528,13 +548,14 @@ void fillPrinter(ParserState& pp, Gecode::FlatZinc::Printer& p) { #endif } -AST::Node* arrayOutput(AST::Call* ann) { +AST::Node* arrayOutput(AST::Call* ann, bool capture=false) { + AST::Node* dimensions=capture ? ann->args->getArray()->a.at(0) : ann->args; AST::Array* a = NULL; - if (ann->args->isArray()) { - a = ann->args->getArray(); + if (dimensions->isArray()) { + a = dimensions->getArray(); } else { - a = new AST::Array(ann->args); + a = new AST::Array(dimensions); } std::ostringstream oss; @@ -557,7 +578,7 @@ AST::Node* arrayOutput(AST::Call* ann) { } } - if (!ann->args->isArray()) { + if (!dimensions->isArray()) { a->a[0] = NULL; delete a; } @@ -644,7 +665,7 @@ namespace Gecode { namespace FlatZinc { }} -#line 648 "gecode/flatzinc/parser.tab.cpp" +#line 669 "gecode/flatzinc/parser.tab.cpp" # ifndef YY_CAST # ifdef __cplusplus @@ -732,72 +753,73 @@ enum yysymbol_kind_t YYSYMBOL_57_ = 57, /* '}' */ YYSYMBOL_YYACCEPT = 58, /* $accept */ YYSYMBOL_model = 59, /* model */ - YYSYMBOL_preddecl_items = 60, /* preddecl_items */ - YYSYMBOL_preddecl_items_head = 61, /* preddecl_items_head */ - YYSYMBOL_vardecl_items = 62, /* vardecl_items */ - YYSYMBOL_vardecl_items_head = 63, /* vardecl_items_head */ - YYSYMBOL_constraint_items = 64, /* constraint_items */ - YYSYMBOL_constraint_items_head = 65, /* constraint_items_head */ - YYSYMBOL_preddecl_item = 66, /* preddecl_item */ - YYSYMBOL_pred_arg_list = 67, /* pred_arg_list */ - YYSYMBOL_pred_arg_list_head = 68, /* pred_arg_list_head */ - YYSYMBOL_pred_arg = 69, /* pred_arg */ - YYSYMBOL_pred_arg_type = 70, /* pred_arg_type */ - YYSYMBOL_pred_arg_simple_type = 71, /* pred_arg_simple_type */ - YYSYMBOL_pred_array_init = 72, /* pred_array_init */ - YYSYMBOL_pred_array_init_arg = 73, /* pred_array_init_arg */ - YYSYMBOL_var_par_id = 74, /* var_par_id */ - YYSYMBOL_vardecl_item = 75, /* vardecl_item */ - YYSYMBOL_int_init = 76, /* int_init */ - YYSYMBOL_int_init_list = 77, /* int_init_list */ - YYSYMBOL_int_init_list_head = 78, /* int_init_list_head */ - YYSYMBOL_list_tail = 79, /* list_tail */ - YYSYMBOL_int_var_array_literal = 80, /* int_var_array_literal */ - YYSYMBOL_float_init = 81, /* float_init */ - YYSYMBOL_float_init_list = 82, /* float_init_list */ - YYSYMBOL_float_init_list_head = 83, /* float_init_list_head */ - YYSYMBOL_float_var_array_literal = 84, /* float_var_array_literal */ - YYSYMBOL_bool_init = 85, /* bool_init */ - YYSYMBOL_bool_init_list = 86, /* bool_init_list */ - YYSYMBOL_bool_init_list_head = 87, /* bool_init_list_head */ - YYSYMBOL_bool_var_array_literal = 88, /* bool_var_array_literal */ - YYSYMBOL_set_init = 89, /* set_init */ - YYSYMBOL_set_init_list = 90, /* set_init_list */ - YYSYMBOL_set_init_list_head = 91, /* set_init_list_head */ - YYSYMBOL_set_var_array_literal = 92, /* set_var_array_literal */ - YYSYMBOL_vardecl_int_var_array_init = 93, /* vardecl_int_var_array_init */ - YYSYMBOL_vardecl_bool_var_array_init = 94, /* vardecl_bool_var_array_init */ - YYSYMBOL_vardecl_float_var_array_init = 95, /* vardecl_float_var_array_init */ - YYSYMBOL_vardecl_set_var_array_init = 96, /* vardecl_set_var_array_init */ - YYSYMBOL_constraint_item = 97, /* constraint_item */ - YYSYMBOL_solve_item = 98, /* solve_item */ - YYSYMBOL_int_ti_expr_tail = 99, /* int_ti_expr_tail */ - YYSYMBOL_bool_ti_expr_tail = 100, /* bool_ti_expr_tail */ - YYSYMBOL_float_ti_expr_tail = 101, /* float_ti_expr_tail */ - YYSYMBOL_set_literal = 102, /* set_literal */ - YYSYMBOL_int_list = 103, /* int_list */ - YYSYMBOL_int_list_head = 104, /* int_list_head */ - YYSYMBOL_bool_list = 105, /* bool_list */ - YYSYMBOL_bool_list_head = 106, /* bool_list_head */ - YYSYMBOL_float_list = 107, /* float_list */ - YYSYMBOL_float_list_head = 108, /* float_list_head */ - YYSYMBOL_set_literal_list = 109, /* set_literal_list */ - YYSYMBOL_set_literal_list_head = 110, /* set_literal_list_head */ - YYSYMBOL_flat_expr_list = 111, /* flat_expr_list */ - YYSYMBOL_flat_expr = 112, /* flat_expr */ - YYSYMBOL_non_array_expr_opt = 113, /* non_array_expr_opt */ - YYSYMBOL_non_array_expr = 114, /* non_array_expr */ - YYSYMBOL_non_array_expr_list = 115, /* non_array_expr_list */ - YYSYMBOL_non_array_expr_list_head = 116, /* non_array_expr_list_head */ - YYSYMBOL_solve_expr = 117, /* solve_expr */ - YYSYMBOL_minmax = 118, /* minmax */ - YYSYMBOL_annotations = 119, /* annotations */ - YYSYMBOL_annotations_head = 120, /* annotations_head */ - YYSYMBOL_annotation = 121, /* annotation */ - YYSYMBOL_annotation_list = 122, /* annotation_list */ - YYSYMBOL_annotation_expr = 123, /* annotation_expr */ - YYSYMBOL_annotation_list_tail = 124, /* annotation_list_tail */ - YYSYMBOL_ann_non_array_expr = 125 /* ann_non_array_expr */ + YYSYMBOL_60_1 = 60, /* $@1 */ + YYSYMBOL_preddecl_items = 61, /* preddecl_items */ + YYSYMBOL_preddecl_items_head = 62, /* preddecl_items_head */ + YYSYMBOL_vardecl_items = 63, /* vardecl_items */ + YYSYMBOL_vardecl_items_head = 64, /* vardecl_items_head */ + YYSYMBOL_constraint_items = 65, /* constraint_items */ + YYSYMBOL_constraint_items_head = 66, /* constraint_items_head */ + YYSYMBOL_preddecl_item = 67, /* preddecl_item */ + YYSYMBOL_pred_arg_list = 68, /* pred_arg_list */ + YYSYMBOL_pred_arg_list_head = 69, /* pred_arg_list_head */ + YYSYMBOL_pred_arg = 70, /* pred_arg */ + YYSYMBOL_pred_arg_type = 71, /* pred_arg_type */ + YYSYMBOL_pred_arg_simple_type = 72, /* pred_arg_simple_type */ + YYSYMBOL_pred_array_init = 73, /* pred_array_init */ + YYSYMBOL_pred_array_init_arg = 74, /* pred_array_init_arg */ + YYSYMBOL_var_par_id = 75, /* var_par_id */ + YYSYMBOL_vardecl_item = 76, /* vardecl_item */ + YYSYMBOL_int_init = 77, /* int_init */ + YYSYMBOL_int_init_list = 78, /* int_init_list */ + YYSYMBOL_int_init_list_head = 79, /* int_init_list_head */ + YYSYMBOL_list_tail = 80, /* list_tail */ + YYSYMBOL_int_var_array_literal = 81, /* int_var_array_literal */ + YYSYMBOL_float_init = 82, /* float_init */ + YYSYMBOL_float_init_list = 83, /* float_init_list */ + YYSYMBOL_float_init_list_head = 84, /* float_init_list_head */ + YYSYMBOL_float_var_array_literal = 85, /* float_var_array_literal */ + YYSYMBOL_bool_init = 86, /* bool_init */ + YYSYMBOL_bool_init_list = 87, /* bool_init_list */ + YYSYMBOL_bool_init_list_head = 88, /* bool_init_list_head */ + YYSYMBOL_bool_var_array_literal = 89, /* bool_var_array_literal */ + YYSYMBOL_set_init = 90, /* set_init */ + YYSYMBOL_set_init_list = 91, /* set_init_list */ + YYSYMBOL_set_init_list_head = 92, /* set_init_list_head */ + YYSYMBOL_set_var_array_literal = 93, /* set_var_array_literal */ + YYSYMBOL_vardecl_int_var_array_init = 94, /* vardecl_int_var_array_init */ + YYSYMBOL_vardecl_bool_var_array_init = 95, /* vardecl_bool_var_array_init */ + YYSYMBOL_vardecl_float_var_array_init = 96, /* vardecl_float_var_array_init */ + YYSYMBOL_vardecl_set_var_array_init = 97, /* vardecl_set_var_array_init */ + YYSYMBOL_constraint_item = 98, /* constraint_item */ + YYSYMBOL_solve_item = 99, /* solve_item */ + YYSYMBOL_int_ti_expr_tail = 100, /* int_ti_expr_tail */ + YYSYMBOL_bool_ti_expr_tail = 101, /* bool_ti_expr_tail */ + YYSYMBOL_float_ti_expr_tail = 102, /* float_ti_expr_tail */ + YYSYMBOL_set_literal = 103, /* set_literal */ + YYSYMBOL_int_list = 104, /* int_list */ + YYSYMBOL_int_list_head = 105, /* int_list_head */ + YYSYMBOL_bool_list = 106, /* bool_list */ + YYSYMBOL_bool_list_head = 107, /* bool_list_head */ + YYSYMBOL_float_list = 108, /* float_list */ + YYSYMBOL_float_list_head = 109, /* float_list_head */ + YYSYMBOL_set_literal_list = 110, /* set_literal_list */ + YYSYMBOL_set_literal_list_head = 111, /* set_literal_list_head */ + YYSYMBOL_flat_expr_list = 112, /* flat_expr_list */ + YYSYMBOL_flat_expr = 113, /* flat_expr */ + YYSYMBOL_non_array_expr_opt = 114, /* non_array_expr_opt */ + YYSYMBOL_non_array_expr = 115, /* non_array_expr */ + YYSYMBOL_non_array_expr_list = 116, /* non_array_expr_list */ + YYSYMBOL_non_array_expr_list_head = 117, /* non_array_expr_list_head */ + YYSYMBOL_solve_expr = 118, /* solve_expr */ + YYSYMBOL_minmax = 119, /* minmax */ + YYSYMBOL_annotations = 120, /* annotations */ + YYSYMBOL_annotations_head = 121, /* annotations_head */ + YYSYMBOL_annotation = 122, /* annotation */ + YYSYMBOL_annotation_list = 123, /* annotation_list */ + YYSYMBOL_annotation_expr = 124, /* annotation_expr */ + YYSYMBOL_annotation_list_tail = 125, /* annotation_list_tail */ + YYSYMBOL_ann_non_array_expr = 126 /* ann_non_array_expr */ }; typedef enum yysymbol_kind_t yysymbol_kind_t; @@ -1125,16 +1147,16 @@ union yyalloc /* YYFINAL -- State number of the termination state. */ #define YYFINAL 7 /* YYLAST -- Last index in YYTABLE. */ -#define YYLAST 360 +#define YYLAST 367 /* YYNTOKENS -- Number of terminals. */ #define YYNTOKENS 58 /* YYNNTS -- Number of nonterminals. */ -#define YYNNTS 68 +#define YYNNTS 69 /* YYNRULES -- Number of rules. */ -#define YYNRULES 162 +#define YYNRULES 163 /* YYNSTATES -- Number of states. */ -#define YYNSTATES 347 +#define YYNSTATES 348 /* YYMAXUTOK -- Last valid token kind. */ #define YYMAXUTOK 302 @@ -1188,23 +1210,23 @@ static const yytype_int8 yytranslate[] = /* YYRLINE[YYN] -- Source line where rule number YYN was defined. */ static const yytype_int16 yyrline[] = { - 0, 716, 716, 718, 720, 723, 724, 726, 728, 731, - 732, 734, 736, 739, 740, 747, 750, 752, 755, 756, - 759, 763, 764, 765, 766, 769, 771, 773, 774, 777, - 778, 781, 782, 788, 788, 791, 823, 855, 894, 927, - 936, 946, 955, 967, 1037, 1103, 1174, 1242, 1263, 1283, - 1303, 1326, 1330, 1345, 1369, 1370, 1374, 1376, 1379, 1379, - 1381, 1385, 1387, 1402, 1425, 1426, 1430, 1432, 1436, 1440, - 1442, 1457, 1480, 1481, 1485, 1487, 1490, 1493, 1495, 1510, - 1533, 1534, 1538, 1540, 1543, 1548, 1549, 1554, 1555, 1560, - 1561, 1566, 1567, 1571, 1737, 1751, 1776, 1778, 1780, 1786, - 1788, 1801, 1803, 1812, 1814, 1821, 1822, 1826, 1828, 1833, - 1834, 1838, 1840, 1845, 1846, 1850, 1852, 1857, 1858, 1862, - 1864, 1872, 1874, 1878, 1880, 1885, 1886, 1890, 1892, 1894, - 1896, 1898, 1994, 2009, 2010, 2014, 2016, 2024, 2058, 2065, - 2072, 2098, 2099, 2107, 2108, 2112, 2114, 2118, 2122, 2126, - 2128, 2132, 2134, 2136, 2139, 2139, 2142, 2144, 2146, 2148, - 2150, 2256, 2267 + 0, 745, 745, 744, 749, 751, 754, 755, 757, 759, + 762, 764, 767, 769, 772, 774, 782, 785, 787, 790, + 791, 794, 798, 799, 800, 801, 804, 806, 808, 809, + 812, 813, 816, 817, 823, 823, 826, 861, 896, 939, + 975, 988, 1002, 1015, 1030, 1112, 1190, 1273, 1353, 1376, + 1398, 1420, 1445, 1449, 1466, 1490, 1491, 1495, 1497, 1500, + 1500, 1502, 1506, 1508, 1525, 1548, 1549, 1553, 1555, 1559, + 1563, 1565, 1582, 1605, 1606, 1610, 1612, 1615, 1618, 1620, + 1637, 1660, 1661, 1665, 1667, 1670, 1675, 1676, 1681, 1682, + 1687, 1688, 1693, 1694, 1698, 1867, 1884, 1912, 1914, 1916, + 1922, 1924, 1937, 1939, 1948, 1950, 1957, 1958, 1962, 1964, + 1969, 1970, 1974, 1976, 1981, 1982, 1986, 1988, 1993, 1994, + 1998, 2000, 2008, 2010, 2014, 2016, 2021, 2022, 2026, 2028, + 2030, 2032, 2034, 2130, 2145, 2146, 2150, 2152, 2160, 2201, + 2208, 2215, 2250, 2251, 2259, 2260, 2264, 2266, 2270, 2274, + 2278, 2280, 2284, 2286, 2288, 2291, 2291, 2294, 2296, 2298, + 2300, 2302, 2408, 2420 }; #endif @@ -1230,7 +1252,7 @@ static const char *const yytname[] = "FZ_SET", "FZ_SHOW", "FZ_SHOWCOND", "FZ_SOLVE", "FZ_STRING", "FZ_TEST", "FZ_THEN", "FZ_TUPLE", "FZ_TYPE", "FZ_VARIANT_RECORD", "FZ_WHERE", "';'", "'('", "')'", "','", "':'", "'['", "']'", "'='", "'{'", "'}'", "$accept", - "model", "preddecl_items", "preddecl_items_head", "vardecl_items", + "model", "$@1", "preddecl_items", "preddecl_items_head", "vardecl_items", "vardecl_items_head", "constraint_items", "constraint_items_head", "preddecl_item", "pred_arg_list", "pred_arg_list_head", "pred_arg", "pred_arg_type", "pred_arg_simple_type", "pred_array_init", @@ -1260,7 +1282,7 @@ yysymbol_name (yysymbol_kind_t yysymbol) } #endif -#define YYPACT_NINF (-123) +#define YYPACT_NINF (-118) #define yypact_value_is_default(Yyn) \ ((Yyn) == YYPACT_NINF) @@ -1274,41 +1296,41 @@ yysymbol_name (yysymbol_kind_t yysymbol) STATE-NUM. */ static const yytype_int16 yypact[] = { - -25, 13, 30, 253, -25, -20, -13, -123, 102, -7, - 6, 18, 38, 87, 108, 253, 79, 81, -123, 84, - 116, 118, -123, -123, -123, 113, 111, 91, 95, 101, - 161, 126, 126, 126, 149, 173, 140, 108, 137, 138, - -123, -123, 217, 134, -123, -123, 157, 185, 142, 139, - -123, 146, -123, -123, 188, 194, 78, -123, -123, 147, - 152, 154, 126, 126, 126, 189, -123, -123, 191, 191, - 191, 158, 160, 191, 163, 170, -123, -123, -123, 56, - 78, -123, 84, -123, 211, -123, -123, 171, -123, 218, - -123, 220, 172, 191, 191, 191, 223, 35, 179, 216, - 181, 187, 126, 169, 119, -123, -123, 208, -123, 28, - -123, -123, -123, -123, 126, -123, -123, -123, 192, 192, - 192, 186, 224, -123, -123, 197, -123, 46, 185, 196, - -123, -123, -123, -123, 57, 35, 57, 57, 191, 224, - -123, -123, 57, 200, -123, 106, -123, -123, -123, -123, - -123, 156, 255, 56, 227, 191, 57, -123, -123, -123, - 228, 258, 35, -123, -123, 212, 214, 19, -123, -123, - -123, -123, 210, -123, 221, 231, 57, 191, 169, -123, - -123, 225, -123, -123, -123, 114, 192, -123, 20, -123, - 117, 35, 229, -123, 232, 57, -123, 57, -123, 233, - -123, -123, 271, 217, -123, -123, 141, 236, 237, 239, - 247, -123, 35, -123, -123, -123, -123, -123, -123, 238, - -123, 261, 242, 243, 244, 126, 126, 126, 257, -123, - 78, 126, 126, 126, 191, 191, 191, 245, 246, 191, - 191, 191, 248, 249, 250, 126, 126, 251, 254, 256, - 259, 260, 262, 191, 191, 263, -123, 264, -123, 265, - -123, 295, 296, 185, 266, 267, 62, -123, 88, -123, - 177, -123, 269, 154, -123, 270, 268, 272, 274, 275, - -123, -123, 276, -123, 277, 279, -123, 280, -123, 278, - 283, -123, 282, -123, 284, 285, -123, -123, -123, 297, - -123, -123, 17, 11, -123, 305, -123, 62, -123, 307, - -123, 88, -123, 311, -123, 177, -123, -123, 224, -123, - 286, 288, 289, -123, 287, 292, -123, 290, -123, 291, - -123, 293, -123, -123, 17, -123, 317, -123, 11, -123, - -123, -123, -123, -123, 294, -123, -123 + -10, 39, 27, 215, -10, -12, -2, -118, 104, -4, + 7, 18, 20, 44, -118, 215, 54, 60, -118, 102, + 59, 91, -118, -118, -118, 81, 28, 65, 73, 75, + 126, 85, 85, 85, 112, 128, 100, -118, -118, 207, + 97, -118, -118, 139, 171, 140, 138, -118, 143, -118, + -118, 193, 198, 9, -118, -118, 147, 155, 156, 85, + 85, 85, 189, -118, -118, 195, 195, 195, 157, 206, + 173, 128, 166, -118, -118, 23, 9, -118, 102, -118, + 216, -118, -118, 174, -118, 222, -118, 226, 177, 195, + 195, 195, 230, 15, 190, 231, 196, 199, 85, 201, + 195, 200, 209, -118, 236, -118, -11, -118, -118, -118, + -118, 85, -118, -118, -118, 203, 203, 203, 205, 242, + -118, -118, 213, -118, 50, 171, 211, -118, -118, -118, + -118, 163, 15, 163, 163, 195, 78, 30, -118, -118, + 262, 23, 234, 195, 163, -118, -118, -118, 235, 267, + 15, -118, -118, 220, 217, 149, 242, -118, -118, 223, + -118, -118, -118, -118, -118, 224, 163, 111, -118, -118, + -118, -118, -118, 83, -118, -118, 135, 203, -118, 232, + -118, 137, 15, 221, -118, 228, 163, 163, -118, 229, + 227, 195, 78, -118, -118, 233, -118, 207, -118, -118, + 109, 237, 238, 239, 248, -118, 15, -118, -118, -118, + 240, -118, -118, 163, -118, -118, -118, 281, -118, 255, + 241, 243, 244, 85, 85, 85, 260, -118, -118, 245, + 9, 85, 85, 85, 195, 195, 195, 246, -118, 249, + 195, 195, 195, 247, 250, 251, 85, 85, 252, 254, + 256, 257, 259, 261, 195, 195, 263, -118, 264, -118, + 265, -118, 288, 292, 171, 258, 266, 170, -118, 93, + -118, 176, -118, 268, 156, -118, 269, 253, 270, 272, + 273, -118, -118, 274, -118, 275, 277, -118, 278, -118, + 276, 282, -118, 279, -118, 280, 284, -118, -118, -118, + 295, -118, -118, 21, 10, -118, 300, -118, 170, -118, + 312, -118, 93, -118, 316, -118, 176, -118, -118, 242, + -118, 283, 285, 286, -118, 287, 289, -118, 290, -118, + 291, -118, 293, -118, -118, 21, -118, 317, -118, 10, + -118, -118, -118, -118, -118, 294, -118, -118 }; /* YYDEFACT[STATE-NUM] -- Default reduction number in state STATE-NUM. @@ -1316,65 +1338,65 @@ static const yytype_int16 yypact[] = means the default is an error. */ static const yytype_uint8 yydefact[] = { - 3, 0, 0, 7, 4, 0, 0, 1, 0, 0, - 0, 0, 0, 0, 11, 8, 0, 0, 5, 16, - 0, 0, 99, 101, 96, 0, 105, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, - 9, 6, 0, 0, 27, 28, 0, 105, 0, 58, - 18, 0, 24, 25, 0, 0, 0, 107, 111, 0, - 58, 58, 0, 0, 0, 0, 33, 34, 143, 143, - 143, 0, 0, 143, 0, 0, 13, 10, 23, 0, - 0, 15, 59, 17, 0, 98, 102, 0, 97, 59, - 106, 59, 0, 143, 143, 143, 0, 0, 0, 144, - 0, 0, 0, 0, 0, 2, 14, 0, 31, 0, - 29, 26, 19, 20, 0, 108, 112, 100, 125, 125, - 125, 0, 157, 156, 158, 33, 162, 0, 105, 160, - 159, 145, 148, 151, 0, 0, 0, 0, 143, 128, - 127, 129, 133, 131, 130, 0, 121, 123, 142, 141, - 94, 0, 0, 0, 0, 143, 0, 35, 36, 37, - 0, 0, 0, 152, 149, 154, 0, 0, 41, 146, - 40, 39, 0, 135, 0, 58, 0, 143, 0, 138, - 139, 137, 95, 32, 30, 0, 125, 126, 0, 104, - 0, 155, 0, 103, 0, 0, 124, 59, 134, 0, - 93, 122, 0, 0, 21, 38, 0, 0, 0, 0, - 0, 147, 0, 150, 153, 161, 42, 136, 132, 0, - 22, 0, 0, 0, 0, 0, 0, 0, 0, 140, - 0, 0, 0, 0, 143, 143, 143, 0, 0, 143, - 143, 143, 0, 0, 0, 0, 0, 85, 87, 89, - 0, 0, 0, 143, 143, 0, 43, 0, 44, 0, - 45, 109, 113, 105, 0, 91, 54, 86, 72, 88, - 64, 90, 0, 58, 115, 0, 58, 0, 0, 0, - 46, 51, 52, 56, 0, 58, 69, 70, 74, 0, - 58, 61, 62, 66, 0, 58, 48, 110, 49, 59, - 114, 47, 117, 80, 92, 0, 60, 59, 55, 0, - 76, 59, 73, 0, 68, 59, 65, 116, 0, 119, - 0, 58, 78, 82, 0, 58, 77, 0, 57, 0, - 75, 0, 67, 50, 59, 118, 0, 84, 59, 81, - 53, 71, 63, 120, 0, 83, 79 + 4, 0, 0, 8, 5, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 2, 9, 0, 0, 6, 17, + 0, 0, 100, 102, 97, 0, 106, 0, 0, 0, + 0, 0, 0, 0, 0, 12, 0, 10, 7, 0, + 0, 28, 29, 0, 106, 0, 59, 19, 0, 25, + 26, 0, 0, 0, 108, 112, 0, 59, 59, 0, + 0, 0, 0, 34, 35, 144, 144, 144, 0, 0, + 0, 13, 0, 11, 24, 0, 0, 16, 60, 18, + 0, 99, 103, 0, 98, 60, 107, 60, 0, 144, + 144, 144, 0, 0, 0, 145, 0, 0, 0, 0, + 144, 0, 0, 14, 0, 32, 0, 30, 27, 20, + 21, 0, 109, 113, 101, 126, 126, 126, 0, 158, + 157, 159, 34, 163, 0, 106, 161, 160, 146, 149, + 152, 0, 0, 0, 0, 144, 0, 0, 3, 15, + 0, 0, 0, 144, 0, 36, 37, 38, 0, 0, + 0, 153, 150, 155, 0, 0, 129, 128, 130, 132, + 131, 42, 147, 41, 40, 0, 134, 0, 122, 124, + 143, 142, 95, 0, 33, 31, 0, 126, 127, 0, + 105, 0, 156, 0, 104, 0, 0, 0, 136, 0, + 59, 144, 0, 139, 140, 138, 96, 0, 22, 39, + 0, 0, 0, 0, 0, 148, 0, 151, 154, 162, + 0, 43, 125, 60, 135, 94, 123, 0, 23, 0, + 0, 0, 0, 0, 0, 0, 0, 133, 137, 0, + 0, 0, 0, 0, 144, 144, 144, 0, 141, 0, + 144, 144, 144, 0, 0, 0, 0, 0, 86, 88, + 90, 0, 0, 0, 144, 144, 0, 44, 0, 45, + 0, 46, 110, 114, 106, 0, 92, 55, 87, 73, + 89, 65, 91, 0, 59, 116, 0, 59, 0, 0, + 0, 47, 52, 53, 57, 0, 59, 70, 71, 75, + 0, 59, 62, 63, 67, 0, 59, 49, 111, 50, + 60, 115, 48, 118, 81, 93, 0, 61, 60, 56, + 0, 77, 60, 74, 0, 69, 60, 66, 117, 0, + 120, 0, 59, 79, 83, 0, 59, 78, 0, 58, + 0, 76, 0, 68, 51, 60, 119, 0, 85, 60, + 82, 54, 72, 64, 121, 0, 84, 80 }; /* YYPGOTO[NTERM-NUM]. */ static const yytype_int16 yypgoto[] = { - -123, -123, -123, -123, -123, -123, -123, -123, 321, -123, - -123, 273, -123, -37, -123, 184, -31, 331, 42, -123, - -123, -57, -123, -15, -123, -123, -123, 39, -123, -123, - -123, 14, -123, -123, -123, -123, -123, -123, -123, 314, - -123, 0, 148, 150, -90, -122, -123, -123, 92, -123, - -123, -123, -123, -123, 180, -108, -121, -123, -123, -123, - -123, 16, -123, -88, 195, -123, -123, 193 + -118, -118, -118, -118, -118, -118, -118, -118, -118, 334, + -118, -118, 271, -118, -33, -118, 202, -31, 327, 38, + -118, -118, -54, -118, 34, -118, -118, -118, 40, -118, + -118, -118, 12, -118, -118, -118, -118, -118, -118, -118, + 296, -118, -3, 153, 154, -86, -117, -118, -118, 94, + -118, -118, -118, -118, -118, 165, -102, -92, -118, -118, + -118, -118, -56, -118, -84, 208, -118, -118, 204 }; /* YYDEFGOTO[NTERM-NUM]. */ static const yytype_int16 yydefgoto[] = { - 0, 2, 3, 4, 14, 15, 36, 37, 5, 48, - 49, 50, 51, 52, 109, 110, 143, 16, 283, 284, - 285, 83, 267, 293, 294, 295, 271, 288, 289, 290, - 269, 323, 324, 325, 304, 256, 258, 260, 280, 38, - 74, 53, 28, 29, 144, 59, 60, 272, 61, 275, - 276, 320, 321, 145, 146, 157, 147, 174, 175, 182, - 151, 98, 99, 164, 165, 132, 192, 133 + 0, 2, 35, 3, 4, 14, 15, 70, 71, 5, + 45, 46, 47, 48, 49, 106, 107, 159, 16, 284, + 285, 286, 79, 268, 294, 295, 296, 272, 289, 290, + 291, 270, 324, 325, 326, 305, 257, 259, 261, 281, + 72, 101, 50, 28, 29, 160, 56, 57, 273, 58, + 276, 277, 321, 322, 167, 168, 145, 169, 189, 190, + 196, 173, 94, 95, 152, 153, 129, 183, 130 }; /* YYTABLE[YYPACT[STATE-NUM]] -- What to do in state STATE-NUM. If @@ -1382,169 +1404,169 @@ static const yytype_int16 yydefgoto[] = number is the opposite. If YYTABLE_NINF, syntax error. */ static const yytype_int16 yytable[] = { - 68, 69, 70, 90, 92, 78, 166, 130, 27, 131, - 1, 158, 159, 168, 318, 170, 171, 66, 67, 6, - 318, 173, 122, 123, 124, 66, 67, 126, 18, 206, - 7, 93, 94, 95, 207, 187, 19, 130, 122, 123, - 124, 125, 67, 126, 208, 130, 30, 169, 209, 122, - 123, 124, 125, 67, 126, 199, 87, 210, 31, 107, - 139, 140, 141, 66, 67, 281, 129, 128, 66, 67, - 32, 138, 130, 128, 216, 128, 217, 130, 205, 153, - 111, 20, 154, 155, 108, 100, 101, 20, 127, 104, - 33, 128, 286, 42, 66, 67, 129, 43, 44, 127, - 163, 130, 128, 213, 129, 20, 24, 21, 45, 118, - 119, 120, 24, 128, 57, 58, 22, 20, 198, 34, - 181, 46, 130, 203, 213, 35, 23, 40, 44, 41, - 24, 129, 66, 67, 47, 54, 129, 55, 45, 25, - 47, 277, 24, 62, 20, 56, 21, 63, 204, 148, - 149, 46, 150, 64, 172, 22, 177, 178, 26, 179, - 129, 180, 66, 67, 65, 23, 220, 211, 212, 24, - 47, 186, 139, 140, 141, 66, 67, 71, 221, 72, - 73, 129, 291, 66, 67, 76, 77, 79, 57, 80, - 82, 85, 81, 200, 234, 235, 236, 26, 84, 86, - 239, 240, 241, 89, 88, 91, 222, 97, 96, 103, - 102, 105, 319, 326, 253, 254, 297, 113, 106, 300, - 20, 115, 142, 114, 116, 128, 121, 152, 308, 117, - 238, 44, 135, 312, 134, 282, 136, 287, 316, 292, - 160, 45, 137, 161, 343, 24, 162, 156, 326, 167, - 242, 243, 244, 176, 46, 247, 248, 249, 183, 185, - 188, 189, 8, 191, 335, 195, 9, 10, 339, 264, - 265, 193, 322, 47, 219, 196, 282, 11, 202, 228, - 287, 12, 197, 214, 292, 237, 215, 218, 225, 226, - 13, 227, 229, 230, 231, 232, 233, 245, 246, 58, - 332, 274, 317, 250, 251, 252, 255, 322, 327, 257, - 329, 259, 261, 262, 331, 263, 266, 268, 270, 299, - 344, 278, 279, 296, 298, 17, 301, 302, 303, 305, - 307, 306, 310, 309, 311, 313, 315, 184, 314, 334, - 333, 337, 336, 338, 340, 341, 39, 342, 346, 328, - 330, 75, 345, 273, 223, 112, 224, 190, 201, 0, - 194 + 65, 66, 67, 86, 88, 27, 74, 127, 154, 128, + 96, 97, 20, 319, 146, 147, 63, 64, 119, 120, + 121, 122, 64, 123, 319, 1, 104, 7, 89, 90, + 91, 54, 55, 115, 116, 117, 18, 24, 127, 161, + 141, 163, 164, 142, 137, 6, 127, 19, 162, 30, + 83, 105, 178, 119, 120, 121, 122, 64, 123, 31, + 170, 171, 126, 172, 127, 44, 125, 135, 124, 127, + 32, 125, 33, 108, 188, 199, 34, 125, 51, 165, + 143, 156, 157, 158, 63, 64, 193, 177, 194, 63, + 64, 63, 64, 126, 210, 211, 127, 287, 207, 63, + 64, 126, 37, 124, 151, 20, 125, 20, 38, 21, + 52, 39, 20, 53, 21, 40, 41, 59, 22, 126, + 127, 228, 207, 22, 126, 60, 42, 61, 23, 62, + 24, 166, 24, 23, 125, 215, 214, 24, 20, 43, + 68, 25, 195, 198, 197, 69, 219, 278, 73, 41, + 75, 126, 119, 120, 121, 63, 64, 123, 44, 42, + 26, 191, 192, 24, 218, 26, 156, 157, 158, 63, + 64, 76, 43, 282, 54, 126, 63, 64, 243, 244, + 245, 292, 63, 64, 248, 249, 250, 205, 206, 78, + 77, 44, 234, 235, 236, 80, 81, 220, 265, 266, + 240, 241, 242, 82, 84, 125, 85, 87, 92, 98, + 20, 93, 99, 100, 103, 254, 255, 320, 327, 125, + 298, 41, 110, 301, 8, 112, 111, 239, 9, 10, + 113, 42, 309, 118, 114, 24, 283, 313, 288, 11, + 293, 200, 317, 12, 43, 131, 201, 132, 138, 344, + 136, 133, 13, 327, 134, 140, 202, 139, 144, 148, + 203, 149, 150, 44, 155, 174, 176, 179, 336, 204, + 180, 182, 340, 323, 184, 208, 186, 283, 213, 187, + 226, 288, 209, 212, 229, 293, 217, 230, 237, 223, + 224, 225, 55, 231, 227, 232, 233, 275, 246, 238, + 318, 247, 251, 328, 300, 252, 253, 256, 323, 258, + 262, 260, 263, 279, 264, 330, 267, 269, 271, 332, + 345, 280, 297, 299, 302, 303, 304, 306, 308, 307, + 311, 310, 314, 312, 315, 316, 335, 334, 17, 337, + 339, 338, 36, 175, 341, 342, 329, 343, 347, 109, + 333, 346, 331, 221, 222, 0, 274, 216, 181, 185, + 0, 0, 0, 0, 0, 0, 0, 102 }; static const yytype_int16 yycheck[] = { - 31, 32, 33, 60, 61, 42, 128, 97, 8, 97, - 35, 119, 120, 134, 3, 136, 137, 6, 7, 6, - 3, 142, 3, 4, 5, 6, 7, 8, 48, 9, - 0, 62, 63, 64, 14, 156, 49, 127, 3, 4, - 5, 6, 7, 8, 24, 135, 53, 135, 28, 3, - 4, 5, 6, 7, 8, 176, 56, 37, 52, 3, - 3, 4, 5, 6, 7, 3, 97, 56, 6, 7, - 52, 102, 162, 56, 195, 56, 197, 167, 186, 51, - 80, 3, 54, 114, 28, 69, 70, 3, 53, 73, - 52, 56, 4, 9, 6, 7, 127, 13, 14, 53, - 54, 191, 56, 191, 135, 3, 28, 5, 24, 93, - 94, 95, 28, 56, 3, 4, 14, 3, 175, 32, - 151, 37, 212, 9, 212, 17, 24, 48, 14, 48, - 28, 162, 6, 7, 56, 19, 167, 19, 24, 37, - 56, 263, 28, 52, 3, 32, 5, 52, 185, 30, - 31, 37, 33, 52, 138, 14, 50, 51, 56, 3, - 191, 5, 6, 7, 3, 24, 203, 50, 51, 28, - 56, 155, 3, 4, 5, 6, 7, 28, 37, 6, - 40, 212, 5, 6, 7, 48, 48, 53, 3, 32, - 51, 3, 50, 177, 225, 226, 227, 56, 52, 5, - 231, 232, 233, 51, 57, 51, 206, 16, 19, 49, - 52, 48, 302, 303, 245, 246, 273, 6, 48, 276, - 3, 3, 53, 52, 4, 56, 3, 19, 285, 57, - 230, 14, 16, 290, 55, 266, 55, 268, 295, 270, - 54, 24, 55, 19, 334, 28, 49, 55, 338, 53, - 234, 235, 236, 53, 37, 239, 240, 241, 3, 32, - 32, 3, 9, 51, 321, 55, 13, 14, 325, 253, - 254, 57, 303, 56, 3, 54, 307, 24, 53, 32, - 311, 28, 51, 54, 315, 28, 54, 54, 52, 52, - 37, 52, 54, 32, 52, 52, 52, 52, 52, 4, - 315, 5, 5, 55, 55, 55, 55, 338, 3, 55, - 3, 55, 53, 53, 3, 53, 53, 53, 53, 51, - 3, 55, 55, 54, 54, 4, 54, 53, 53, 53, - 51, 54, 54, 53, 51, 53, 51, 153, 54, 51, - 54, 54, 53, 51, 54, 54, 15, 54, 54, 307, - 311, 37, 338, 261, 206, 82, 206, 162, 178, -1, - 167 + 31, 32, 33, 57, 58, 8, 39, 93, 125, 93, + 66, 67, 3, 3, 116, 117, 6, 7, 3, 4, + 5, 6, 7, 8, 3, 35, 3, 0, 59, 60, + 61, 3, 4, 89, 90, 91, 48, 28, 124, 131, + 51, 133, 134, 54, 100, 6, 132, 49, 132, 53, + 53, 28, 144, 3, 4, 5, 6, 7, 8, 52, + 30, 31, 93, 33, 150, 56, 56, 98, 53, 155, + 52, 56, 52, 76, 166, 177, 32, 56, 19, 135, + 111, 3, 4, 5, 6, 7, 3, 143, 5, 6, + 7, 6, 7, 124, 186, 187, 182, 4, 182, 6, + 7, 132, 48, 53, 54, 3, 56, 3, 48, 5, + 19, 9, 3, 32, 5, 13, 14, 52, 14, 150, + 206, 213, 206, 14, 155, 52, 24, 52, 24, 3, + 28, 53, 28, 24, 56, 191, 190, 28, 3, 37, + 28, 37, 173, 176, 9, 17, 37, 264, 48, 14, + 53, 182, 3, 4, 5, 6, 7, 8, 56, 24, + 56, 50, 51, 28, 197, 56, 3, 4, 5, 6, + 7, 32, 37, 3, 3, 206, 6, 7, 234, 235, + 236, 5, 6, 7, 240, 241, 242, 50, 51, 51, + 50, 56, 223, 224, 225, 52, 3, 200, 254, 255, + 231, 232, 233, 5, 57, 56, 51, 51, 19, 52, + 3, 16, 6, 40, 48, 246, 247, 303, 304, 56, + 274, 14, 6, 277, 9, 3, 52, 230, 13, 14, + 4, 24, 286, 3, 57, 28, 267, 291, 269, 24, + 271, 9, 296, 28, 37, 55, 14, 16, 48, 335, + 49, 55, 37, 339, 55, 19, 24, 48, 55, 54, + 28, 19, 49, 56, 53, 3, 32, 32, 322, 37, + 3, 51, 326, 304, 57, 54, 53, 308, 51, 55, + 32, 312, 54, 54, 3, 316, 53, 32, 28, 52, + 52, 52, 4, 52, 54, 52, 52, 5, 52, 54, + 5, 52, 55, 3, 51, 55, 55, 55, 339, 55, + 53, 55, 53, 55, 53, 3, 53, 53, 53, 3, + 3, 55, 54, 54, 54, 53, 53, 53, 51, 54, + 54, 53, 53, 51, 54, 51, 51, 54, 4, 53, + 51, 54, 15, 141, 54, 54, 308, 54, 54, 78, + 316, 339, 312, 200, 200, -1, 262, 192, 150, 155, + -1, -1, -1, -1, -1, -1, -1, 71 }; /* YYSTOS[STATE-NUM] -- The symbol kind of the accessing symbol of state STATE-NUM. */ static const yytype_int8 yystos[] = { - 0, 35, 59, 60, 61, 66, 6, 0, 9, 13, - 14, 24, 28, 37, 62, 63, 75, 66, 48, 49, - 3, 5, 14, 24, 28, 37, 56, 99, 100, 101, - 53, 52, 52, 52, 32, 17, 64, 65, 97, 75, - 48, 48, 9, 13, 14, 24, 37, 56, 67, 68, - 69, 70, 71, 99, 19, 19, 32, 3, 4, 103, - 104, 106, 52, 52, 52, 3, 6, 7, 74, 74, - 74, 28, 6, 40, 98, 97, 48, 48, 71, 53, - 32, 50, 51, 79, 52, 3, 5, 99, 57, 51, - 79, 51, 79, 74, 74, 74, 19, 16, 119, 120, - 119, 119, 52, 49, 119, 48, 48, 3, 28, 72, - 73, 99, 69, 6, 52, 3, 4, 57, 119, 119, - 119, 3, 3, 4, 5, 6, 8, 53, 56, 74, - 102, 121, 123, 125, 55, 16, 55, 55, 74, 3, - 4, 5, 53, 74, 102, 111, 112, 114, 30, 31, - 33, 118, 19, 51, 54, 74, 55, 113, 113, 113, - 54, 19, 49, 54, 121, 122, 103, 53, 114, 121, - 114, 114, 119, 114, 115, 116, 53, 50, 51, 3, - 5, 74, 117, 3, 73, 32, 119, 114, 32, 3, - 122, 51, 124, 57, 125, 55, 54, 51, 79, 114, - 119, 112, 53, 9, 71, 113, 9, 14, 24, 28, - 37, 50, 51, 121, 54, 54, 114, 114, 54, 3, - 71, 37, 99, 100, 101, 52, 52, 52, 32, 54, - 32, 52, 52, 52, 74, 74, 74, 28, 99, 74, - 74, 74, 119, 119, 119, 52, 52, 119, 119, 119, - 55, 55, 55, 74, 74, 55, 93, 55, 94, 55, - 95, 53, 53, 53, 119, 119, 53, 80, 53, 88, - 53, 84, 105, 106, 5, 107, 108, 103, 55, 55, - 96, 3, 74, 76, 77, 78, 4, 74, 85, 86, - 87, 5, 74, 81, 82, 83, 54, 79, 54, 51, - 79, 54, 53, 53, 92, 53, 54, 51, 79, 53, - 54, 51, 79, 53, 54, 51, 79, 5, 3, 102, - 109, 110, 74, 89, 90, 91, 102, 3, 76, 3, - 85, 3, 81, 54, 51, 79, 53, 54, 51, 79, - 54, 54, 54, 102, 3, 89, 54 + 0, 35, 59, 61, 62, 67, 6, 0, 9, 13, + 14, 24, 28, 37, 63, 64, 76, 67, 48, 49, + 3, 5, 14, 24, 28, 37, 56, 100, 101, 102, + 53, 52, 52, 52, 32, 60, 76, 48, 48, 9, + 13, 14, 24, 37, 56, 68, 69, 70, 71, 72, + 100, 19, 19, 32, 3, 4, 104, 105, 107, 52, + 52, 52, 3, 6, 7, 75, 75, 75, 28, 17, + 65, 66, 98, 48, 72, 53, 32, 50, 51, 80, + 52, 3, 5, 100, 57, 51, 80, 51, 80, 75, + 75, 75, 19, 16, 120, 121, 120, 120, 52, 6, + 40, 99, 98, 48, 3, 28, 73, 74, 100, 70, + 6, 52, 3, 4, 57, 120, 120, 120, 3, 3, + 4, 5, 6, 8, 53, 56, 75, 103, 122, 124, + 126, 55, 16, 55, 55, 75, 49, 120, 48, 48, + 19, 51, 54, 75, 55, 114, 114, 114, 54, 19, + 49, 54, 122, 123, 104, 53, 3, 4, 5, 75, + 103, 115, 122, 115, 115, 120, 53, 112, 113, 115, + 30, 31, 33, 119, 3, 74, 32, 120, 115, 32, + 3, 123, 51, 125, 57, 126, 53, 55, 115, 116, + 117, 50, 51, 3, 5, 75, 118, 9, 72, 114, + 9, 14, 24, 28, 37, 50, 51, 122, 54, 54, + 115, 115, 54, 51, 80, 120, 113, 53, 72, 37, + 100, 101, 102, 52, 52, 52, 32, 54, 115, 3, + 32, 52, 52, 52, 75, 75, 75, 28, 54, 100, + 75, 75, 75, 120, 120, 120, 52, 52, 120, 120, + 120, 55, 55, 55, 75, 75, 55, 94, 55, 95, + 55, 96, 53, 53, 53, 120, 120, 53, 81, 53, + 89, 53, 85, 106, 107, 5, 108, 109, 104, 55, + 55, 97, 3, 75, 77, 78, 79, 4, 75, 86, + 87, 88, 5, 75, 82, 83, 84, 54, 80, 54, + 51, 80, 54, 53, 53, 93, 53, 54, 51, 80, + 53, 54, 51, 80, 53, 54, 51, 80, 5, 3, + 103, 110, 111, 75, 90, 91, 92, 103, 3, 77, + 3, 86, 3, 82, 54, 51, 80, 53, 54, 51, + 80, 54, 54, 54, 103, 3, 90, 54 }; /* YYR1[RULE-NUM] -- Symbol kind of the left-hand side of rule RULE-NUM. */ static const yytype_int8 yyr1[] = { - 0, 58, 59, 60, 60, 61, 61, 62, 62, 63, - 63, 64, 64, 65, 65, 66, 67, 67, 68, 68, - 69, 70, 70, 70, 70, 71, 71, 71, 71, 72, - 72, 73, 73, 74, 74, 75, 75, 75, 75, 75, - 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, - 75, 76, 76, 76, 77, 77, 78, 78, 79, 79, - 80, 81, 81, 81, 82, 82, 83, 83, 84, 85, - 85, 85, 86, 86, 87, 87, 88, 89, 89, 89, - 90, 90, 91, 91, 92, 93, 93, 94, 94, 95, - 95, 96, 96, 97, 98, 98, 99, 99, 99, 100, - 100, 101, 101, 102, 102, 103, 103, 104, 104, 105, - 105, 106, 106, 107, 107, 108, 108, 109, 109, 110, - 110, 111, 111, 112, 112, 113, 113, 114, 114, 114, - 114, 114, 114, 115, 115, 116, 116, 117, 117, 117, - 117, 118, 118, 119, 119, 120, 120, 121, 121, 122, - 122, 123, 123, 123, 124, 124, 125, 125, 125, 125, - 125, 125, 125 + 0, 58, 60, 59, 61, 61, 62, 62, 63, 63, + 64, 64, 65, 65, 66, 66, 67, 68, 68, 69, + 69, 70, 71, 71, 71, 71, 72, 72, 72, 72, + 73, 73, 74, 74, 75, 75, 76, 76, 76, 76, + 76, 76, 76, 76, 76, 76, 76, 76, 76, 76, + 76, 76, 77, 77, 77, 78, 78, 79, 79, 80, + 80, 81, 82, 82, 82, 83, 83, 84, 84, 85, + 86, 86, 86, 87, 87, 88, 88, 89, 90, 90, + 90, 91, 91, 92, 92, 93, 94, 94, 95, 95, + 96, 96, 97, 97, 98, 99, 99, 100, 100, 100, + 101, 101, 102, 102, 103, 103, 104, 104, 105, 105, + 106, 106, 107, 107, 108, 108, 109, 109, 110, 110, + 111, 111, 112, 112, 113, 113, 114, 114, 115, 115, + 115, 115, 115, 115, 116, 116, 117, 117, 118, 118, + 118, 118, 119, 119, 120, 120, 121, 121, 122, 122, + 123, 123, 124, 124, 124, 125, 125, 126, 126, 126, + 126, 126, 126, 126 }; /* YYR2[RULE-NUM] -- Number of symbols on the right-hand side of rule RULE-NUM. */ static const yytype_int8 yyr2[] = { - 0, 2, 5, 0, 1, 2, 3, 0, 1, 2, - 3, 0, 1, 2, 3, 5, 0, 2, 1, 3, - 3, 6, 7, 2, 1, 1, 3, 1, 1, 1, - 3, 1, 3, 1, 1, 6, 6, 6, 8, 6, - 6, 6, 8, 13, 13, 13, 15, 15, 15, 15, - 17, 1, 1, 4, 0, 2, 1, 3, 0, 1, - 3, 1, 1, 4, 0, 2, 1, 3, 3, 1, - 1, 4, 0, 2, 1, 3, 3, 1, 1, 4, - 0, 2, 1, 3, 3, 0, 2, 0, 2, 0, - 2, 0, 2, 6, 3, 4, 1, 3, 3, 1, - 4, 1, 3, 3, 3, 0, 2, 1, 3, 0, - 2, 1, 3, 0, 2, 1, 3, 0, 2, 1, - 3, 1, 3, 1, 3, 0, 2, 1, 1, 1, - 1, 1, 4, 0, 2, 1, 3, 1, 1, 1, - 4, 1, 1, 0, 1, 2, 3, 4, 1, 1, - 3, 1, 2, 4, 0, 1, 1, 1, 1, 1, - 1, 4, 1 + 0, 2, 0, 6, 0, 1, 2, 3, 0, 1, + 2, 3, 0, 1, 2, 3, 5, 0, 2, 1, + 3, 3, 6, 7, 2, 1, 1, 3, 1, 1, + 1, 3, 1, 3, 1, 1, 6, 6, 6, 8, + 6, 6, 6, 8, 13, 13, 13, 15, 15, 15, + 15, 17, 1, 1, 4, 0, 2, 1, 3, 0, + 1, 3, 1, 1, 4, 0, 2, 1, 3, 3, + 1, 1, 4, 0, 2, 1, 3, 3, 1, 1, + 4, 0, 2, 1, 3, 3, 0, 2, 0, 2, + 0, 2, 0, 2, 6, 3, 4, 1, 3, 3, + 1, 4, 1, 3, 3, 3, 0, 2, 1, 3, + 0, 2, 1, 3, 0, 2, 1, 3, 0, 2, + 1, 3, 1, 3, 1, 3, 0, 2, 1, 1, + 1, 1, 1, 4, 0, 2, 1, 3, 1, 1, + 1, 4, 1, 1, 0, 1, 2, 3, 4, 1, + 1, 3, 1, 2, 4, 0, 1, 1, 1, 1, + 1, 1, 4, 1 }; @@ -2014,7 +2036,299 @@ yydestruct (const char *yymsg, YY_SYMBOL_PRINT (yymsg, yykind, yyvaluep, yylocationp); YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN - YY_USE (yykind); + switch (yykind) + { + case YYSYMBOL_FZ_ID: /* FZ_ID */ +#line 732 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) free(((*yyvaluep).sValue)); } +#line 2045 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_FZ_U_ID: /* FZ_U_ID */ +#line 732 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) free(((*yyvaluep).sValue)); } +#line 2051 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_FZ_STRING_LIT: /* FZ_STRING_LIT */ +#line 732 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) free(((*yyvaluep).sValue)); } +#line 2057 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_var_par_id: /* var_par_id */ +#line 732 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) free(((*yyvaluep).sValue)); } +#line 2063 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_int_init: /* int_init */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).varSpec); } +#line 2069 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_int_init_list: /* int_init_list */ +#line 735 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) { for(auto* p:*((*yyvaluep).varSpecVec)) delete p; delete ((*yyvaluep).varSpecVec); } } +#line 2075 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_int_init_list_head: /* int_init_list_head */ +#line 735 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) { for(auto* p:*((*yyvaluep).varSpecVec)) delete p; delete ((*yyvaluep).varSpecVec); } } +#line 2081 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_int_var_array_literal: /* int_var_array_literal */ +#line 735 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) { for(auto* p:*((*yyvaluep).varSpecVec)) delete p; delete ((*yyvaluep).varSpecVec); } } +#line 2087 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_float_init: /* float_init */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).varSpec); } +#line 2093 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_float_init_list: /* float_init_list */ +#line 735 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) { for(auto* p:*((*yyvaluep).varSpecVec)) delete p; delete ((*yyvaluep).varSpecVec); } } +#line 2099 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_float_init_list_head: /* float_init_list_head */ +#line 735 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) { for(auto* p:*((*yyvaluep).varSpecVec)) delete p; delete ((*yyvaluep).varSpecVec); } } +#line 2105 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_float_var_array_literal: /* float_var_array_literal */ +#line 735 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) { for(auto* p:*((*yyvaluep).varSpecVec)) delete p; delete ((*yyvaluep).varSpecVec); } } +#line 2111 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_bool_init: /* bool_init */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).varSpec); } +#line 2117 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_bool_init_list: /* bool_init_list */ +#line 735 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) { for(auto* p:*((*yyvaluep).varSpecVec)) delete p; delete ((*yyvaluep).varSpecVec); } } +#line 2123 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_bool_init_list_head: /* bool_init_list_head */ +#line 735 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) { for(auto* p:*((*yyvaluep).varSpecVec)) delete p; delete ((*yyvaluep).varSpecVec); } } +#line 2129 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_bool_var_array_literal: /* bool_var_array_literal */ +#line 735 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) { for(auto* p:*((*yyvaluep).varSpecVec)) delete p; delete ((*yyvaluep).varSpecVec); } } +#line 2135 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_set_init: /* set_init */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).varSpec); } +#line 2141 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_set_init_list: /* set_init_list */ +#line 735 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) { for(auto* p:*((*yyvaluep).varSpecVec)) delete p; delete ((*yyvaluep).varSpecVec); } } +#line 2147 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_set_init_list_head: /* set_init_list_head */ +#line 735 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) { for(auto* p:*((*yyvaluep).varSpecVec)) delete p; delete ((*yyvaluep).varSpecVec); } } +#line 2153 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_set_var_array_literal: /* set_var_array_literal */ +#line 735 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) { for(auto* p:*((*yyvaluep).varSpecVec)) delete p; delete ((*yyvaluep).varSpecVec); } } +#line 2159 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_vardecl_int_var_array_init: /* vardecl_int_var_array_init */ +#line 736 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture && ((*yyvaluep).oVarSpecVec)()) { for(auto* p:*((*yyvaluep).oVarSpecVec).some()) delete p; delete ((*yyvaluep).oVarSpecVec).some(); } } +#line 2165 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_vardecl_bool_var_array_init: /* vardecl_bool_var_array_init */ +#line 736 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture && ((*yyvaluep).oVarSpecVec)()) { for(auto* p:*((*yyvaluep).oVarSpecVec).some()) delete p; delete ((*yyvaluep).oVarSpecVec).some(); } } +#line 2171 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_vardecl_float_var_array_init: /* vardecl_float_var_array_init */ +#line 736 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture && ((*yyvaluep).oVarSpecVec)()) { for(auto* p:*((*yyvaluep).oVarSpecVec).some()) delete p; delete ((*yyvaluep).oVarSpecVec).some(); } } +#line 2177 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_vardecl_set_var_array_init: /* vardecl_set_var_array_init */ +#line 736 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture && ((*yyvaluep).oVarSpecVec)()) { for(auto* p:*((*yyvaluep).oVarSpecVec).some()) delete p; delete ((*yyvaluep).oVarSpecVec).some(); } } +#line 2183 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_int_ti_expr_tail: /* int_ti_expr_tail */ +#line 734 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture && ((*yyvaluep).oSet)()) delete ((*yyvaluep).oSet).some(); } +#line 2189 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_bool_ti_expr_tail: /* bool_ti_expr_tail */ +#line 734 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture && ((*yyvaluep).oSet)()) delete ((*yyvaluep).oSet).some(); } +#line 2195 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_float_ti_expr_tail: /* float_ti_expr_tail */ +#line 734 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture && ((*yyvaluep).oPFloat)()) delete ((*yyvaluep).oPFloat).some(); } +#line 2201 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_set_literal: /* set_literal */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).setLit); } +#line 2207 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_int_list: /* int_list */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).setValue); } +#line 2213 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_int_list_head: /* int_list_head */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).setValue); } +#line 2219 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_bool_list: /* bool_list */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).setValue); } +#line 2225 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_bool_list_head: /* bool_list_head */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).setValue); } +#line 2231 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_float_list: /* float_list */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).floatSetValue); } +#line 2237 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_float_list_head: /* float_list_head */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).floatSetValue); } +#line 2243 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_set_literal_list: /* set_literal_list */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).setValueList); } +#line 2249 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_set_literal_list_head: /* set_literal_list_head */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).setValueList); } +#line 2255 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_flat_expr_list: /* flat_expr_list */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).argVec); } +#line 2261 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_flat_expr: /* flat_expr */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).arg); } +#line 2267 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_non_array_expr_opt: /* non_array_expr_opt */ +#line 734 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture && ((*yyvaluep).oArg)()) delete ((*yyvaluep).oArg).some(); } +#line 2273 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_non_array_expr: /* non_array_expr */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).arg); } +#line 2279 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_non_array_expr_list: /* non_array_expr_list */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).argVec); } +#line 2285 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_non_array_expr_list_head: /* non_array_expr_list_head */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).argVec); } +#line 2291 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_annotations: /* annotations */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).argVec); } +#line 2297 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_annotations_head: /* annotations_head */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).argVec); } +#line 2303 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_annotation: /* annotation */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).arg); } +#line 2309 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_annotation_list: /* annotation_list */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).arg); } +#line 2315 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_annotation_expr: /* annotation_expr */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).arg); } +#line 2321 "gecode/flatzinc/parser.tab.cpp" + break; + + case YYSYMBOL_ann_non_array_expr: /* ann_non_array_expr */ +#line 733 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture) delete ((*yyvaluep).arg); } +#line 2327 "gecode/flatzinc/parser.tab.cpp" + break; + + default: + break; + } YY_IGNORE_MAYBE_UNINITIALIZED_END } @@ -2285,35 +2599,68 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); YY_REDUCE_PRINT (yyn); switch (yyn) { - case 15: /* preddecl_item: FZ_PREDICATE FZ_ID '(' pred_arg_list ')' */ -#line 748 "./gecode/flatzinc/parser.yxx" + case 2: /* $@1: %empty */ +#line 745 "gecode/flatzinc/parser.yxx" + { ParserState* pp=static_cast(parm); + if(pp->capture && !pp->hadError) pp->capture->begin(*pp); } +#line 2607 "gecode/flatzinc/parser.tab.cpp" + break; + + case 10: /* vardecl_items_head: vardecl_item ';' */ +#line 763 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture && static_cast(parm)->hadError) YYABORT; } +#line 2613 "gecode/flatzinc/parser.tab.cpp" + break; + + case 11: /* vardecl_items_head: vardecl_items_head vardecl_item ';' */ +#line 765 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture && static_cast(parm)->hadError) YYABORT; } +#line 2619 "gecode/flatzinc/parser.tab.cpp" + break; + + case 14: /* constraint_items_head: constraint_item ';' */ +#line 773 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture && static_cast(parm)->hadError) YYABORT; } +#line 2625 "gecode/flatzinc/parser.tab.cpp" + break; + + case 15: /* constraint_items_head: constraint_items_head constraint_item ';' */ +#line 775 "gecode/flatzinc/parser.yxx" + { if(static_cast(parm)->capture && static_cast(parm)->hadError) YYABORT; } +#line 2631 "gecode/flatzinc/parser.tab.cpp" + break; + + case 16: /* preddecl_item: FZ_PREDICATE FZ_ID '(' pred_arg_list ')' */ +#line 783 "gecode/flatzinc/parser.yxx" { free((yyvsp[-3].sValue)); } -#line 2292 "gecode/flatzinc/parser.tab.cpp" +#line 2637 "gecode/flatzinc/parser.tab.cpp" break; - case 20: /* pred_arg: pred_arg_type ':' FZ_ID */ -#line 760 "./gecode/flatzinc/parser.yxx" + case 21: /* pred_arg: pred_arg_type ':' FZ_ID */ +#line 795 "gecode/flatzinc/parser.yxx" { free((yyvsp[0].sValue)); } -#line 2298 "gecode/flatzinc/parser.tab.cpp" +#line 2643 "gecode/flatzinc/parser.tab.cpp" break; - case 25: /* pred_arg_simple_type: int_ti_expr_tail */ -#line 770 "./gecode/flatzinc/parser.yxx" + case 26: /* pred_arg_simple_type: int_ti_expr_tail */ +#line 805 "gecode/flatzinc/parser.yxx" { if ((yyvsp[0].oSet)()) delete (yyvsp[0].oSet).some(); } -#line 2304 "gecode/flatzinc/parser.tab.cpp" +#line 2649 "gecode/flatzinc/parser.tab.cpp" break; - case 26: /* pred_arg_simple_type: FZ_SET FZ_OF int_ti_expr_tail */ -#line 772 "./gecode/flatzinc/parser.yxx" + case 27: /* pred_arg_simple_type: FZ_SET FZ_OF int_ti_expr_tail */ +#line 807 "gecode/flatzinc/parser.yxx" { if ((yyvsp[0].oSet)()) delete (yyvsp[0].oSet).some(); } -#line 2310 "gecode/flatzinc/parser.tab.cpp" +#line 2655 "gecode/flatzinc/parser.tab.cpp" break; - case 35: /* vardecl_item: FZ_VAR int_ti_expr_tail ':' var_par_id annotations non_array_expr_opt */ -#line 792 "./gecode/flatzinc/parser.yxx" + case 36: /* vardecl_item: FZ_VAR int_ti_expr_tail ':' var_par_id annotations non_array_expr_opt */ +#line 827 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); - bool print = (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasAtom("output_var"); + if(pp->capture) pp->capture->declaration(*pp,(yyvsp[-2].sValue),(yyvsp[-1].argVec)); + if(pp->capture) pp->capture->variable_count(*pp,1); + bool print = (!pp->capture || !pp->hadError) && (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasAtom("output_var"); bool funcDep = (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasAtom("is_defined_var"); yyassert(pp, pp->symbols.put((yyvsp[-2].sValue), se_iv(pp->intvars.size())), @@ -2340,16 +2687,19 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); pp->intvars.push_back(varspec((yyvsp[-2].sValue), new IntVarSpec((yyvsp[-4].oSet),!print,funcDep))); } + if(pp->capture && pp->hadError && (yyvsp[0].oArg)() && (yyvsp[-4].oSet)()) delete (yyvsp[-4].oSet).some(); delete (yyvsp[-1].argVec); free((yyvsp[-2].sValue)); } -#line 2346 "gecode/flatzinc/parser.tab.cpp" +#line 2694 "gecode/flatzinc/parser.tab.cpp" break; - case 36: /* vardecl_item: FZ_VAR bool_ti_expr_tail ':' var_par_id annotations non_array_expr_opt */ -#line 824 "./gecode/flatzinc/parser.yxx" + case 37: /* vardecl_item: FZ_VAR bool_ti_expr_tail ':' var_par_id annotations non_array_expr_opt */ +#line 862 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); - bool print = (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasAtom("output_var"); + if(pp->capture) pp->capture->declaration(*pp,(yyvsp[-2].sValue),(yyvsp[-1].argVec)); + if(pp->capture) pp->capture->variable_count(*pp,1); + bool print = (!pp->capture || !pp->hadError) && (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasAtom("output_var"); bool funcDep = (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasAtom("is_defined_var"); yyassert(pp, pp->symbols.put((yyvsp[-2].sValue), se_bv(pp->boolvars.size())), @@ -2376,16 +2726,19 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); pp->boolvars.push_back(varspec((yyvsp[-2].sValue), new BoolVarSpec((yyvsp[-4].oSet),!print,funcDep))); } + if(pp->capture && pp->hadError && (yyvsp[0].oArg)() && (yyvsp[-4].oSet)()) delete (yyvsp[-4].oSet).some(); delete (yyvsp[-1].argVec); free((yyvsp[-2].sValue)); } -#line 2382 "gecode/flatzinc/parser.tab.cpp" +#line 2733 "gecode/flatzinc/parser.tab.cpp" break; - case 37: /* vardecl_item: FZ_VAR float_ti_expr_tail ':' var_par_id annotations non_array_expr_opt */ -#line 856 "./gecode/flatzinc/parser.yxx" + case 38: /* vardecl_item: FZ_VAR float_ti_expr_tail ':' var_par_id annotations non_array_expr_opt */ +#line 897 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); - bool print = (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasAtom("output_var"); + if(pp->capture) pp->capture->declaration(*pp,(yyvsp[-2].sValue),(yyvsp[-1].argVec)); + if(pp->capture) pp->capture->variable_count(*pp,1); + bool print = (!pp->capture || !pp->hadError) && (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasAtom("output_var"); bool funcDep = (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasAtom("is_defined_var"); yyassert(pp, pp->symbols.put((yyvsp[-2].sValue), se_fv(pp->floatvars.size())), @@ -2409,6 +2762,7 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); if (!pp->hadError && (yyvsp[-4].oPFloat)()) { AST::FloatVar* fv = new AST::FloatVar(pp->floatvars.size()-1); addDomainConstraint(pp, fv, (yyvsp[-4].oPFloat)); + if(pp->capture) (yyvsp[-4].oPFloat)=Option*>::none(); } delete arg; } else { @@ -2419,16 +2773,19 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); pp->floatvars.push_back(varspec((yyvsp[-2].sValue), new FloatVarSpec(dom,!print,funcDep))); } + if(pp->capture && pp->hadError && (yyvsp[0].oArg)() && (yyvsp[-4].oPFloat)()) delete (yyvsp[-4].oPFloat).some(); delete (yyvsp[-1].argVec); free((yyvsp[-2].sValue)); } -#line 2425 "gecode/flatzinc/parser.tab.cpp" +#line 2780 "gecode/flatzinc/parser.tab.cpp" break; - case 38: /* vardecl_item: FZ_VAR FZ_SET FZ_OF int_ti_expr_tail ':' var_par_id annotations non_array_expr_opt */ -#line 895 "./gecode/flatzinc/parser.yxx" + case 39: /* vardecl_item: FZ_VAR FZ_SET FZ_OF int_ti_expr_tail ':' var_par_id annotations non_array_expr_opt */ +#line 940 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); - bool print = (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasAtom("output_var"); + if(pp->capture) pp->capture->declaration(*pp,(yyvsp[-2].sValue),(yyvsp[-1].argVec)); + if(pp->capture) pp->capture->variable_count(*pp,1); + bool print = (!pp->capture || !pp->hadError) && (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasAtom("output_var"); bool funcDep = (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasAtom("is_defined_var"); yyassert(pp, pp->symbols.put((yyvsp[-2].sValue), se_sv(pp->setvars.size())), @@ -2456,74 +2813,94 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); pp->setvars.push_back(varspec((yyvsp[-2].sValue), new SetVarSpec((yyvsp[-4].oSet),!print,funcDep))); } + if(pp->capture && pp->hadError && (yyvsp[0].oArg)() && (yyvsp[-4].oSet)()) delete (yyvsp[-4].oSet).some(); delete (yyvsp[-1].argVec); free((yyvsp[-2].sValue)); } -#line 2462 "gecode/flatzinc/parser.tab.cpp" +#line 2820 "gecode/flatzinc/parser.tab.cpp" break; - case 39: /* vardecl_item: FZ_INT ':' var_par_id annotations '=' non_array_expr */ -#line 928 "./gecode/flatzinc/parser.yxx" + case 40: /* vardecl_item: FZ_INT ':' var_par_id annotations '=' non_array_expr */ +#line 976 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); + if(pp->capture && (yyvsp[-2].argVec)) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, (yyvsp[0].arg)->isInt(), "Invalid int initializer"); + if (!pp->capture || !pp->hadError) { yyassert(pp, pp->symbols.put((yyvsp[-3].sValue), se_i((yyvsp[0].arg)->getInt())), "Duplicate symbol"); + } + if(pp->capture) delete (yyvsp[0].arg); delete (yyvsp[-2].argVec); free((yyvsp[-3].sValue)); } -#line 2475 "gecode/flatzinc/parser.tab.cpp" +#line 2837 "gecode/flatzinc/parser.tab.cpp" break; - case 40: /* vardecl_item: FZ_FLOAT ':' var_par_id annotations '=' non_array_expr */ -#line 937 "./gecode/flatzinc/parser.yxx" + case 41: /* vardecl_item: FZ_FLOAT ':' var_par_id annotations '=' non_array_expr */ +#line 989 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); + if(pp->capture && (yyvsp[-2].argVec)) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, (yyvsp[0].arg)->isFloat(), "Invalid float initializer"); + if (!pp->capture || !pp->hadError) { pp->floatvals.push_back((yyvsp[0].arg)->getFloat()); yyassert(pp, pp->symbols.put((yyvsp[-3].sValue), se_f(pp->floatvals.size()-1)), "Duplicate symbol"); + } + if(pp->capture) delete (yyvsp[0].arg); delete (yyvsp[-2].argVec); free((yyvsp[-3].sValue)); } -#line 2489 "gecode/flatzinc/parser.tab.cpp" +#line 2855 "gecode/flatzinc/parser.tab.cpp" break; - case 41: /* vardecl_item: FZ_BOOL ':' var_par_id annotations '=' non_array_expr */ -#line 947 "./gecode/flatzinc/parser.yxx" + case 42: /* vardecl_item: FZ_BOOL ':' var_par_id annotations '=' non_array_expr */ +#line 1003 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); + if(pp->capture && (yyvsp[-2].argVec)) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, (yyvsp[0].arg)->isBool(), "Invalid bool initializer"); + if (!pp->capture || !pp->hadError) { yyassert(pp, pp->symbols.put((yyvsp[-3].sValue), se_b((yyvsp[0].arg)->getBool())), "Duplicate symbol"); + } + if(pp->capture) delete (yyvsp[0].arg); delete (yyvsp[-2].argVec); free((yyvsp[-3].sValue)); } -#line 2502 "gecode/flatzinc/parser.tab.cpp" +#line 2872 "gecode/flatzinc/parser.tab.cpp" break; - case 42: /* vardecl_item: FZ_SET FZ_OF FZ_INT ':' var_par_id annotations '=' non_array_expr */ -#line 956 "./gecode/flatzinc/parser.yxx" + case 43: /* vardecl_item: FZ_SET FZ_OF FZ_INT ':' var_par_id annotations '=' non_array_expr */ +#line 1016 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); + if(pp->capture && (yyvsp[-2].argVec)) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, (yyvsp[0].arg)->isSet(), "Invalid set initializer"); + if (!pp->capture || !pp->hadError) { AST::SetLit* set = (yyvsp[0].arg)->getSet(); pp->setvals.push_back(*set); yyassert(pp, pp->symbols.put((yyvsp[-3].sValue), se_s(pp->setvals.size()-1)), "Duplicate symbol"); delete set; + } else delete (yyvsp[0].arg); delete (yyvsp[-2].argVec); free((yyvsp[-3].sValue)); } -#line 2518 "gecode/flatzinc/parser.tab.cpp" +#line 2891 "gecode/flatzinc/parser.tab.cpp" break; - case 43: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_VAR int_ti_expr_tail ':' var_par_id annotations vardecl_int_var_array_init */ -#line 969 "./gecode/flatzinc/parser.yxx" + case 44: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_VAR int_ti_expr_tail ':' var_par_id annotations vardecl_int_var_array_init */ +#line 1032 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); yyassert(pp, (yyvsp[-10].iValue)==1, "Arrays must start at 1"); + if(pp->capture) { pp->capture->array_size(*pp,(yyvsp[-8].iValue)); + pp->capture->variable_count(*pp,(yyvsp[-8].iValue),(yyvsp[0].oVarSpecVec)()); + } if (!pp->hadError) { - bool print = (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasCall("output_array"); + if(pp->capture) pp->capture->declaration(*pp,(yyvsp[-2].sValue),(yyvsp[-1].argVec),(yyvsp[-8].iValue)); + bool print = (!pp->capture || !pp->hadError) && (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasCall("output_array"); vector vars((yyvsp[-8].iValue)); if (!pp->hadError) { if ((yyvsp[0].oVarSpecVec)()) { @@ -2532,6 +2909,7 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); "Initializer size does not match array dimension"); if (!pp->hadError) { for (int i=0; i<(yyvsp[-8].iValue); i++) { + if(pp->capture && pp->hadError) break; IntVarSpec* ivsv = static_cast((*vsv)[i]); if (ivsv->alias) { if (print) @@ -2542,6 +2920,7 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); ivsv->introduced = false; vars[i] = pp->intvars.size(); pp->intvars.push_back(varspec((yyvsp[-2].sValue), ivsv)); + if(pp->capture) (*vsv)[i]=nullptr; } if (!pp->hadError && (yyvsp[-4].oSet)()) { Option opt = @@ -2552,10 +2931,12 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } } } + if(pp->capture) { for(auto* child:*vsv) delete child; (yyvsp[0].oVarSpecVec)=Option*>::none(); } delete vsv; } else { if ((yyvsp[-8].iValue)>0) { for (int i=0; i<(yyvsp[-8].iValue); i++) { + if(pp->capture && pp->hadError) break; Option dom = (yyvsp[-4].oSet)() ? Option::some(new AST::SetLit((yyvsp[-4].oSet).some())) : Option::none(); @@ -2564,12 +2945,12 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); pp->intvars.push_back(varspec((yyvsp[-2].sValue), ispec)); } } - if ((yyvsp[-4].oSet)()) delete (yyvsp[-4].oSet).some(); + if ((yyvsp[-4].oSet)()) { delete (yyvsp[-4].oSet).some(); if(pp->capture) (yyvsp[-4].oSet)=Option::none(); } } } - if (print) { + if (print && (!pp->capture || !pp->hadError)) { AST::Array* a = new AST::Array(); - a->a.push_back(arrayOutput((yyvsp[-1].argVec)->getCall("output_array"))); + a->a.push_back(arrayOutput((yyvsp[-1].argVec)->getCall("output_array"),pp->capture!=nullptr)); AST::Array* output = new AST::Array(); for (int i=0; i<(yyvsp[-8].iValue); i++) output->a.push_back(new AST::IntVar(vars[i])); @@ -2585,17 +2966,25 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); pp->symbols.put((yyvsp[-2].sValue), se_iva(iva)), "Duplicate symbol"); } + if(pp->capture) { + if((yyvsp[0].oVarSpecVec)()) { for(auto* child:*(yyvsp[0].oVarSpecVec).some()) delete child; delete (yyvsp[0].oVarSpecVec).some(); } + if((yyvsp[-4].oSet)()) delete (yyvsp[-4].oSet).some(); + } delete (yyvsp[-1].argVec); free((yyvsp[-2].sValue)); } -#line 2591 "gecode/flatzinc/parser.tab.cpp" +#line 2976 "gecode/flatzinc/parser.tab.cpp" break; - case 44: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_VAR bool_ti_expr_tail ':' var_par_id annotations vardecl_bool_var_array_init */ -#line 1039 "./gecode/flatzinc/parser.yxx" + case 45: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_VAR bool_ti_expr_tail ':' var_par_id annotations vardecl_bool_var_array_init */ +#line 1114 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); - bool print = (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasCall("output_array"); + if(pp->capture) pp->capture->declaration(*pp,(yyvsp[-2].sValue),(yyvsp[-1].argVec),(yyvsp[-8].iValue)); + bool print = (!pp->capture || !pp->hadError) && (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasCall("output_array"); yyassert(pp, (yyvsp[-10].iValue)==1, "Arrays must start at 1"); + if(pp->capture) { pp->capture->array_size(*pp,(yyvsp[-8].iValue)); + pp->capture->variable_count(*pp,(yyvsp[-8].iValue),(yyvsp[0].oVarSpecVec)()); + } if (!pp->hadError) { vector vars((yyvsp[-8].iValue)); if ((yyvsp[0].oVarSpecVec)()) { @@ -2604,6 +2993,7 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); "Initializer size does not match array dimension"); if (!pp->hadError) { for (int i=0; i<(yyvsp[-8].iValue); i++) { + if(pp->capture && pp->hadError) break; BoolVarSpec* bvsv = static_cast((*vsv)[i]); if (bvsv->alias) { if (print) @@ -2614,6 +3004,7 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); bvsv->introduced = false; vars[i] = pp->boolvars.size(); pp->boolvars.push_back(varspec((yyvsp[-2].sValue), (*vsv)[i])); + if(pp->capture) (*vsv)[i]=nullptr; } if (!pp->hadError && (yyvsp[-4].oSet)()) { Option opt = @@ -2624,9 +3015,11 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } } } - delete vsv; + if(pp->capture) { for(auto* child:*vsv) delete child; (yyvsp[0].oVarSpecVec)=Option*>::none(); } + delete vsv; } else { for (int i=0; i<(yyvsp[-8].iValue); i++) { + if(pp->capture && pp->hadError) break; Option dom = (yyvsp[-4].oSet)() ? Option::some(new AST::SetLit((yyvsp[-4].oSet).some())) : Option::none(); @@ -2634,11 +3027,11 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); pp->boolvars.push_back(varspec((yyvsp[-2].sValue), new BoolVarSpec(dom,!print,false))); } - if ((yyvsp[-4].oSet)()) delete (yyvsp[-4].oSet).some(); + if ((yyvsp[-4].oSet)()) { delete (yyvsp[-4].oSet).some(); if(pp->capture) (yyvsp[-4].oSet)=Option::none(); } } - if (print) { + if (print && (!pp->capture || !pp->hadError)) { AST::Array* a = new AST::Array(); - a->a.push_back(arrayOutput((yyvsp[-1].argVec)->getCall("output_array"))); + a->a.push_back(arrayOutput((yyvsp[-1].argVec)->getCall("output_array"),pp->capture!=nullptr)); AST::Array* output = new AST::Array(); for (int i=0; i<(yyvsp[-8].iValue); i++) output->a.push_back(new AST::BoolVar(vars[i])); @@ -2654,18 +3047,26 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); pp->symbols.put((yyvsp[-2].sValue), se_bva(bva)), "Duplicate symbol"); } + if(pp->capture) { + if((yyvsp[0].oVarSpecVec)()) { for(auto* child:*(yyvsp[0].oVarSpecVec).some()) delete child; delete (yyvsp[0].oVarSpecVec).some(); } + if((yyvsp[-4].oSet)()) delete (yyvsp[-4].oSet).some(); + } delete (yyvsp[-1].argVec); free((yyvsp[-2].sValue)); } -#line 2660 "gecode/flatzinc/parser.tab.cpp" +#line 3057 "gecode/flatzinc/parser.tab.cpp" break; - case 45: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_VAR float_ti_expr_tail ':' var_par_id annotations vardecl_float_var_array_init */ -#line 1106 "./gecode/flatzinc/parser.yxx" + case 46: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_VAR float_ti_expr_tail ':' var_par_id annotations vardecl_float_var_array_init */ +#line 1193 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); yyassert(pp, (yyvsp[-10].iValue)==1, "Arrays must start at 1"); + if(pp->capture) { pp->capture->array_size(*pp,(yyvsp[-8].iValue)); + pp->capture->variable_count(*pp,(yyvsp[-8].iValue),(yyvsp[0].oVarSpecVec)()); + } if (!pp->hadError) { - bool print = (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasCall("output_array"); + if(pp->capture) pp->capture->declaration(*pp,(yyvsp[-2].sValue),(yyvsp[-1].argVec),(yyvsp[-8].iValue)); + bool print = (!pp->capture || !pp->hadError) && (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasCall("output_array"); vector vars((yyvsp[-8].iValue)); if (!pp->hadError) { if ((yyvsp[0].oVarSpecVec)()) { @@ -2674,6 +3075,7 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); "Initializer size does not match array dimension"); if (!pp->hadError) { for (int i=0; i<(yyvsp[-8].iValue); i++) { + if(pp->capture && pp->hadError) break; FloatVarSpec* ivsv = static_cast((*vsv)[i]); if (ivsv->alias) { if (print) @@ -2684,6 +3086,7 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); ivsv->introduced = false; vars[i] = pp->floatvars.size(); pp->floatvars.push_back(varspec((yyvsp[-2].sValue), ivsv)); + if(pp->capture) (*vsv)[i]=nullptr; } if (!pp->hadError && (yyvsp[-4].oPFloat)()) { Option*> opt = @@ -2694,6 +3097,7 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } } } + if(pp->capture) { for(auto* child:*vsv) delete child; (yyvsp[0].oVarSpecVec)=Option*>::none(); } delete vsv; } else { if ((yyvsp[-8].iValue)>0) { @@ -2701,6 +3105,7 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); (yyvsp[-4].oPFloat)() ? Option >::some(*(yyvsp[-4].oPFloat).some()) : Option >::none(); for (int i=0; i<(yyvsp[-8].iValue); i++) { + if(pp->capture && pp->hadError) break; FloatVarSpec* ispec = new FloatVarSpec(dom,!print,false); vars[i] = pp->floatvars.size(); pp->floatvars.push_back(varspec((yyvsp[-2].sValue), ispec)); @@ -2708,9 +3113,9 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } } } - if (print) { + if (print && (!pp->capture || !pp->hadError)) { AST::Array* a = new AST::Array(); - a->a.push_back(arrayOutput((yyvsp[-1].argVec)->getCall("output_array"))); + a->a.push_back(arrayOutput((yyvsp[-1].argVec)->getCall("output_array"),pp->capture!=nullptr)); AST::Array* output = new AST::Array(); for (int i=0; i<(yyvsp[-8].iValue); i++) output->a.push_back(new AST::FloatVar(vars[i])); @@ -2726,18 +3131,26 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); pp->symbols.put((yyvsp[-2].sValue), se_fva(fva)), "Duplicate symbol"); } - if ((yyvsp[-4].oPFloat)()) delete (yyvsp[-4].oPFloat).some(); + if ((yyvsp[-4].oPFloat)()) { delete (yyvsp[-4].oPFloat).some(); if(pp->capture) (yyvsp[-4].oPFloat)=Option*>::none(); } + if(pp->capture) { + if((yyvsp[0].oVarSpecVec)()) { for(auto* child:*(yyvsp[0].oVarSpecVec).some()) delete child; delete (yyvsp[0].oVarSpecVec).some(); } + if((yyvsp[-4].oPFloat)()) delete (yyvsp[-4].oPFloat).some(); + } delete (yyvsp[-1].argVec); free((yyvsp[-2].sValue)); } -#line 2733 "gecode/flatzinc/parser.tab.cpp" +#line 3142 "gecode/flatzinc/parser.tab.cpp" break; - case 46: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_VAR FZ_SET FZ_OF int_ti_expr_tail ':' var_par_id annotations vardecl_set_var_array_init */ -#line 1176 "./gecode/flatzinc/parser.yxx" + case 47: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_VAR FZ_SET FZ_OF int_ti_expr_tail ':' var_par_id annotations vardecl_set_var_array_init */ +#line 1275 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); - bool print = (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasCall("output_array"); + if(pp->capture) pp->capture->declaration(*pp,(yyvsp[-2].sValue),(yyvsp[-1].argVec),(yyvsp[-10].iValue)); + bool print = (!pp->capture || !pp->hadError) && (yyvsp[-1].argVec) != NULL && (yyvsp[-1].argVec)->hasCall("output_array"); yyassert(pp, (yyvsp[-12].iValue)==1, "Arrays must start at 1"); + if(pp->capture) { pp->capture->array_size(*pp,(yyvsp[-10].iValue)); + pp->capture->variable_count(*pp,(yyvsp[-10].iValue),(yyvsp[0].oVarSpecVec)()); + } if (!pp->hadError) { vector vars((yyvsp[-10].iValue)); if ((yyvsp[0].oVarSpecVec)()) { @@ -2746,6 +3159,7 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); "Initializer size does not match array dimension"); if (!pp->hadError) { for (int i=0; i<(yyvsp[-10].iValue); i++) { + if(pp->capture && pp->hadError) break; SetVarSpec* svsv = static_cast((*vsv)[i]); if (svsv->alias) { if (print) @@ -2756,6 +3170,7 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); svsv->introduced = false; vars[i] = pp->setvars.size(); pp->setvars.push_back(varspec((yyvsp[-2].sValue), (*vsv)[i])); + if(pp->capture) (*vsv)[i]=nullptr; } if (!pp->hadError && (yyvsp[-4].oSet)()) { Option opt = @@ -2766,10 +3181,12 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } } } - delete vsv; + if(pp->capture) { for(auto* child:*vsv) delete child; (yyvsp[0].oVarSpecVec)=Option*>::none(); } + delete vsv; } else { if ((yyvsp[-10].iValue)>0) { for (int i=0; i<(yyvsp[-10].iValue); i++) { + if(pp->capture && pp->hadError) break; Option dom = (yyvsp[-4].oSet)() ? Option::some(new AST::SetLit((yyvsp[-4].oSet).some())) : Option::none(); @@ -2777,12 +3194,12 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); vars[i] = pp->setvars.size(); pp->setvars.push_back(varspec((yyvsp[-2].sValue), ispec)); } - if ((yyvsp[-4].oSet)()) delete (yyvsp[-4].oSet).some(); + if ((yyvsp[-4].oSet)()) { delete (yyvsp[-4].oSet).some(); if(pp->capture) (yyvsp[-4].oSet)=Option::none(); } } } - if (print) { + if (print && (!pp->capture || !pp->hadError)) { AST::Array* a = new AST::Array(); - a->a.push_back(arrayOutput((yyvsp[-1].argVec)->getCall("output_array"))); + a->a.push_back(arrayOutput((yyvsp[-1].argVec)->getCall("output_array"),pp->capture!=nullptr)); AST::Array* output = new AST::Array(); for (int i=0; i<(yyvsp[-10].iValue); i++) output->a.push_back(new AST::SetVar(vars[i])); @@ -2798,16 +3215,22 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); pp->symbols.put((yyvsp[-2].sValue), se_sva(sva)), "Duplicate symbol"); } + if(pp->capture) { + if((yyvsp[0].oVarSpecVec)()) { for(auto* child:*(yyvsp[0].oVarSpecVec).some()) delete child; delete (yyvsp[0].oVarSpecVec).some(); } + if((yyvsp[-4].oSet)()) delete (yyvsp[-4].oSet).some(); + } delete (yyvsp[-1].argVec); free((yyvsp[-2].sValue)); } -#line 2804 "gecode/flatzinc/parser.tab.cpp" +#line 3225 "gecode/flatzinc/parser.tab.cpp" break; - case 47: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_INT ':' var_par_id annotations '=' '[' int_list ']' */ -#line 1244 "./gecode/flatzinc/parser.yxx" + case 48: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_INT ':' var_par_id annotations '=' '[' int_list ']' */ +#line 1355 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); + if(pp->capture && (yyvsp[-4].argVec)) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, (yyvsp[-12].iValue)==1, "Arrays must start at 1"); + if(pp->capture) pp->capture->array_size(*pp,(yyvsp[-10].iValue)); yyassert(pp, (yyvsp[-1].setValue)->size() == static_cast((yyvsp[-10].iValue)), "Initializer size does not match array dimension"); @@ -2824,14 +3247,16 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); free((yyvsp[-5].sValue)); delete (yyvsp[-4].argVec); } -#line 2828 "gecode/flatzinc/parser.tab.cpp" +#line 3251 "gecode/flatzinc/parser.tab.cpp" break; - case 48: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_BOOL ':' var_par_id annotations '=' '[' bool_list ']' */ -#line 1265 "./gecode/flatzinc/parser.yxx" + case 49: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_BOOL ':' var_par_id annotations '=' '[' bool_list ']' */ +#line 1378 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); + if(pp->capture && (yyvsp[-4].argVec)) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, (yyvsp[-12].iValue)==1, "Arrays must start at 1"); + if(pp->capture) pp->capture->array_size(*pp,(yyvsp[-10].iValue)); yyassert(pp, (yyvsp[-1].setValue)->size() == static_cast((yyvsp[-10].iValue)), "Initializer size does not match array dimension"); if (!pp->hadError) { @@ -2847,14 +3272,16 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); free((yyvsp[-5].sValue)); delete (yyvsp[-4].argVec); } -#line 2851 "gecode/flatzinc/parser.tab.cpp" +#line 3276 "gecode/flatzinc/parser.tab.cpp" break; - case 49: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_FLOAT ':' var_par_id annotations '=' '[' float_list ']' */ -#line 1285 "./gecode/flatzinc/parser.yxx" + case 50: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_FLOAT ':' var_par_id annotations '=' '[' float_list ']' */ +#line 1400 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); + if(pp->capture && (yyvsp[-4].argVec)) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, (yyvsp[-12].iValue)==1, "Arrays must start at 1"); + if(pp->capture) pp->capture->array_size(*pp,(yyvsp[-10].iValue)); yyassert(pp, (yyvsp[-1].floatSetValue)->size() == static_cast((yyvsp[-10].iValue)), "Initializer size does not match array dimension"); if (!pp->hadError) { @@ -2870,14 +3297,16 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); delete (yyvsp[-1].floatSetValue); delete (yyvsp[-4].argVec); free((yyvsp[-5].sValue)); } -#line 2874 "gecode/flatzinc/parser.tab.cpp" +#line 3301 "gecode/flatzinc/parser.tab.cpp" break; - case 50: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_SET FZ_OF FZ_INT ':' var_par_id annotations '=' '[' set_literal_list ']' */ -#line 1305 "./gecode/flatzinc/parser.yxx" + case 51: /* vardecl_item: FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_SET FZ_OF FZ_INT ':' var_par_id annotations '=' '[' set_literal_list ']' */ +#line 1422 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); + if(pp->capture && (yyvsp[-4].argVec)) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, (yyvsp[-14].iValue)==1, "Arrays must start at 1"); + if(pp->capture) pp->capture->array_size(*pp,(yyvsp[-12].iValue)); yyassert(pp, (yyvsp[-1].setValueList)->size() == static_cast((yyvsp[-12].iValue)), "Initializer size does not match array dimension"); if (!pp->hadError) { @@ -2894,24 +3323,26 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); delete (yyvsp[-1].setValueList); delete (yyvsp[-4].argVec); free((yyvsp[-5].sValue)); } -#line 2898 "gecode/flatzinc/parser.tab.cpp" +#line 3327 "gecode/flatzinc/parser.tab.cpp" break; - case 51: /* int_init: FZ_INT_LIT */ -#line 1327 "./gecode/flatzinc/parser.yxx" + case 52: /* int_init: FZ_INT_LIT */ +#line 1446 "gecode/flatzinc/parser.yxx" { (yyval.varSpec) = new IntVarSpec((yyvsp[0].iValue),false,false); } -#line 2906 "gecode/flatzinc/parser.tab.cpp" +#line 3335 "gecode/flatzinc/parser.tab.cpp" break; - case 52: /* int_init: var_par_id */ -#line 1331 "./gecode/flatzinc/parser.yxx" + case 53: /* int_init: var_par_id */ +#line 1450 "gecode/flatzinc/parser.yxx" { SymbolEntry e; ParserState* pp = static_cast(parm); - if (pp->symbols.get((yyvsp[0].sValue), e) && (e.t == ST_INTVAR || e.t == ST_INT)) - (yyval.varSpec) = new IntVarSpec(Alias(e.i),false,false); + if (pp->symbols.get((yyvsp[0].sValue), e) && (e.t == ST_INTVAR || e.t == ST_INT)) { + if(pp->capture && e.t == ST_INT) (yyval.varSpec) = new IntVarSpec(e.i,false,false); + else (yyval.varSpec) = new IntVarSpec(Alias(e.i),false,false); + } else { pp->err << "Error: undefined identifier for type int " << (yyvsp[0].sValue) << " in line no. " @@ -2921,11 +3352,11 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } free((yyvsp[0].sValue)); } -#line 2925 "gecode/flatzinc/parser.tab.cpp" +#line 3356 "gecode/flatzinc/parser.tab.cpp" break; - case 53: /* int_init: var_par_id '[' FZ_INT_LIT ']' */ -#line 1346 "./gecode/flatzinc/parser.yxx" + case 54: /* int_init: var_par_id '[' FZ_INT_LIT ']' */ +#line 1467 "gecode/flatzinc/parser.yxx" { vector v; SymbolEntry e; @@ -2946,52 +3377,54 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } free((yyvsp[-3].sValue)); } -#line 2950 "gecode/flatzinc/parser.tab.cpp" +#line 3381 "gecode/flatzinc/parser.tab.cpp" break; - case 54: /* int_init_list: %empty */ -#line 1369 "./gecode/flatzinc/parser.yxx" + case 55: /* int_init_list: %empty */ +#line 1490 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = new vector(0); } -#line 2956 "gecode/flatzinc/parser.tab.cpp" +#line 3387 "gecode/flatzinc/parser.tab.cpp" break; - case 55: /* int_init_list: int_init_list_head list_tail */ -#line 1371 "./gecode/flatzinc/parser.yxx" + case 56: /* int_init_list: int_init_list_head list_tail */ +#line 1492 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = (yyvsp[-1].varSpecVec); } -#line 2962 "gecode/flatzinc/parser.tab.cpp" +#line 3393 "gecode/flatzinc/parser.tab.cpp" break; - case 56: /* int_init_list_head: int_init */ -#line 1375 "./gecode/flatzinc/parser.yxx" + case 57: /* int_init_list_head: int_init */ +#line 1496 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = new vector(1); (*(yyval.varSpecVec))[0] = (yyvsp[0].varSpec); } -#line 2968 "gecode/flatzinc/parser.tab.cpp" +#line 3399 "gecode/flatzinc/parser.tab.cpp" break; - case 57: /* int_init_list_head: int_init_list_head ',' int_init */ -#line 1377 "./gecode/flatzinc/parser.yxx" + case 58: /* int_init_list_head: int_init_list_head ',' int_init */ +#line 1498 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = (yyvsp[-2].varSpecVec); (yyval.varSpecVec)->push_back((yyvsp[0].varSpec)); } -#line 2974 "gecode/flatzinc/parser.tab.cpp" +#line 3405 "gecode/flatzinc/parser.tab.cpp" break; - case 60: /* int_var_array_literal: '[' int_init_list ']' */ -#line 1382 "./gecode/flatzinc/parser.yxx" + case 61: /* int_var_array_literal: '[' int_init_list ']' */ +#line 1503 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = (yyvsp[-1].varSpecVec); } -#line 2980 "gecode/flatzinc/parser.tab.cpp" +#line 3411 "gecode/flatzinc/parser.tab.cpp" break; - case 61: /* float_init: FZ_FLOAT_LIT */ -#line 1386 "./gecode/flatzinc/parser.yxx" + case 62: /* float_init: FZ_FLOAT_LIT */ +#line 1507 "gecode/flatzinc/parser.yxx" { (yyval.varSpec) = new FloatVarSpec((yyvsp[0].dValue),false,false); } -#line 2986 "gecode/flatzinc/parser.tab.cpp" +#line 3417 "gecode/flatzinc/parser.tab.cpp" break; - case 62: /* float_init: var_par_id */ -#line 1388 "./gecode/flatzinc/parser.yxx" + case 63: /* float_init: var_par_id */ +#line 1509 "gecode/flatzinc/parser.yxx" { SymbolEntry e; ParserState* pp = static_cast(parm); - if (pp->symbols.get((yyvsp[0].sValue), e) && (e.t == ST_FLOATVAR || e.t == ST_FLOAT)) - (yyval.varSpec) = new FloatVarSpec(Alias(e.i),false,false); + if (pp->symbols.get((yyvsp[0].sValue), e) && (e.t == ST_FLOATVAR || e.t == ST_FLOAT)) { + if(pp->capture && e.t == ST_FLOAT) (yyval.varSpec) = new FloatVarSpec(pp->floatvals.at(e.i),false,false); + else (yyval.varSpec) = new FloatVarSpec(Alias(e.i),false,false); + } else { pp->err << "Error: undefined identifier for type float " << (yyvsp[0].sValue) << " in line no. " @@ -3001,11 +3434,11 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } free((yyvsp[0].sValue)); } -#line 3005 "gecode/flatzinc/parser.tab.cpp" +#line 3438 "gecode/flatzinc/parser.tab.cpp" break; - case 63: /* float_init: var_par_id '[' FZ_INT_LIT ']' */ -#line 1403 "./gecode/flatzinc/parser.yxx" + case 64: /* float_init: var_par_id '[' FZ_INT_LIT ']' */ +#line 1526 "gecode/flatzinc/parser.yxx" { SymbolEntry e; ParserState* pp = static_cast(parm); @@ -3025,52 +3458,54 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } free((yyvsp[-3].sValue)); } -#line 3029 "gecode/flatzinc/parser.tab.cpp" +#line 3462 "gecode/flatzinc/parser.tab.cpp" break; - case 64: /* float_init_list: %empty */ -#line 1425 "./gecode/flatzinc/parser.yxx" + case 65: /* float_init_list: %empty */ +#line 1548 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = new vector(0); } -#line 3035 "gecode/flatzinc/parser.tab.cpp" +#line 3468 "gecode/flatzinc/parser.tab.cpp" break; - case 65: /* float_init_list: float_init_list_head list_tail */ -#line 1427 "./gecode/flatzinc/parser.yxx" + case 66: /* float_init_list: float_init_list_head list_tail */ +#line 1550 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = (yyvsp[-1].varSpecVec); } -#line 3041 "gecode/flatzinc/parser.tab.cpp" +#line 3474 "gecode/flatzinc/parser.tab.cpp" break; - case 66: /* float_init_list_head: float_init */ -#line 1431 "./gecode/flatzinc/parser.yxx" + case 67: /* float_init_list_head: float_init */ +#line 1554 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = new vector(1); (*(yyval.varSpecVec))[0] = (yyvsp[0].varSpec); } -#line 3047 "gecode/flatzinc/parser.tab.cpp" +#line 3480 "gecode/flatzinc/parser.tab.cpp" break; - case 67: /* float_init_list_head: float_init_list_head ',' float_init */ -#line 1433 "./gecode/flatzinc/parser.yxx" + case 68: /* float_init_list_head: float_init_list_head ',' float_init */ +#line 1556 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = (yyvsp[-2].varSpecVec); (yyval.varSpecVec)->push_back((yyvsp[0].varSpec)); } -#line 3053 "gecode/flatzinc/parser.tab.cpp" +#line 3486 "gecode/flatzinc/parser.tab.cpp" break; - case 68: /* float_var_array_literal: '[' float_init_list ']' */ -#line 1437 "./gecode/flatzinc/parser.yxx" + case 69: /* float_var_array_literal: '[' float_init_list ']' */ +#line 1560 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = (yyvsp[-1].varSpecVec); } -#line 3059 "gecode/flatzinc/parser.tab.cpp" +#line 3492 "gecode/flatzinc/parser.tab.cpp" break; - case 69: /* bool_init: FZ_BOOL_LIT */ -#line 1441 "./gecode/flatzinc/parser.yxx" + case 70: /* bool_init: FZ_BOOL_LIT */ +#line 1564 "gecode/flatzinc/parser.yxx" { (yyval.varSpec) = new BoolVarSpec((yyvsp[0].iValue),false,false); } -#line 3065 "gecode/flatzinc/parser.tab.cpp" +#line 3498 "gecode/flatzinc/parser.tab.cpp" break; - case 70: /* bool_init: var_par_id */ -#line 1443 "./gecode/flatzinc/parser.yxx" + case 71: /* bool_init: var_par_id */ +#line 1566 "gecode/flatzinc/parser.yxx" { SymbolEntry e; ParserState* pp = static_cast(parm); - if (pp->symbols.get((yyvsp[0].sValue), e) && (e.t == ST_BOOLVAR || e.t == ST_BOOL)) - (yyval.varSpec) = new BoolVarSpec(Alias(e.i),false,false); + if (pp->symbols.get((yyvsp[0].sValue), e) && (e.t == ST_BOOLVAR || e.t == ST_BOOL)) { + if(pp->capture && e.t == ST_BOOL) (yyval.varSpec) = new BoolVarSpec(e.i != 0,false,false); + else (yyval.varSpec) = new BoolVarSpec(Alias(e.i),false,false); + } else { pp->err << "Error: undefined identifier for type bool " << (yyvsp[0].sValue) << " in line no. " @@ -3080,11 +3515,11 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } free((yyvsp[0].sValue)); } -#line 3084 "gecode/flatzinc/parser.tab.cpp" +#line 3519 "gecode/flatzinc/parser.tab.cpp" break; - case 71: /* bool_init: var_par_id '[' FZ_INT_LIT ']' */ -#line 1458 "./gecode/flatzinc/parser.yxx" + case 72: /* bool_init: var_par_id '[' FZ_INT_LIT ']' */ +#line 1583 "gecode/flatzinc/parser.yxx" { SymbolEntry e; ParserState* pp = static_cast(parm); @@ -3104,52 +3539,54 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } free((yyvsp[-3].sValue)); } -#line 3108 "gecode/flatzinc/parser.tab.cpp" +#line 3543 "gecode/flatzinc/parser.tab.cpp" break; - case 72: /* bool_init_list: %empty */ -#line 1480 "./gecode/flatzinc/parser.yxx" + case 73: /* bool_init_list: %empty */ +#line 1605 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = new vector(0); } -#line 3114 "gecode/flatzinc/parser.tab.cpp" +#line 3549 "gecode/flatzinc/parser.tab.cpp" break; - case 73: /* bool_init_list: bool_init_list_head list_tail */ -#line 1482 "./gecode/flatzinc/parser.yxx" + case 74: /* bool_init_list: bool_init_list_head list_tail */ +#line 1607 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = (yyvsp[-1].varSpecVec); } -#line 3120 "gecode/flatzinc/parser.tab.cpp" +#line 3555 "gecode/flatzinc/parser.tab.cpp" break; - case 74: /* bool_init_list_head: bool_init */ -#line 1486 "./gecode/flatzinc/parser.yxx" + case 75: /* bool_init_list_head: bool_init */ +#line 1611 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = new vector(1); (*(yyval.varSpecVec))[0] = (yyvsp[0].varSpec); } -#line 3126 "gecode/flatzinc/parser.tab.cpp" +#line 3561 "gecode/flatzinc/parser.tab.cpp" break; - case 75: /* bool_init_list_head: bool_init_list_head ',' bool_init */ -#line 1488 "./gecode/flatzinc/parser.yxx" + case 76: /* bool_init_list_head: bool_init_list_head ',' bool_init */ +#line 1613 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = (yyvsp[-2].varSpecVec); (yyval.varSpecVec)->push_back((yyvsp[0].varSpec)); } -#line 3132 "gecode/flatzinc/parser.tab.cpp" +#line 3567 "gecode/flatzinc/parser.tab.cpp" break; - case 76: /* bool_var_array_literal: '[' bool_init_list ']' */ -#line 1490 "./gecode/flatzinc/parser.yxx" + case 77: /* bool_var_array_literal: '[' bool_init_list ']' */ +#line 1615 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = (yyvsp[-1].varSpecVec); } -#line 3138 "gecode/flatzinc/parser.tab.cpp" +#line 3573 "gecode/flatzinc/parser.tab.cpp" break; - case 77: /* set_init: set_literal */ -#line 1494 "./gecode/flatzinc/parser.yxx" + case 78: /* set_init: set_literal */ +#line 1619 "gecode/flatzinc/parser.yxx" { (yyval.varSpec) = new SetVarSpec((yyvsp[0].setLit),false,false); } -#line 3144 "gecode/flatzinc/parser.tab.cpp" +#line 3579 "gecode/flatzinc/parser.tab.cpp" break; - case 78: /* set_init: var_par_id */ -#line 1496 "./gecode/flatzinc/parser.yxx" + case 79: /* set_init: var_par_id */ +#line 1621 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); SymbolEntry e; - if (pp->symbols.get((yyvsp[0].sValue), e) && (e.t == ST_SETVAR || e.t == ST_SET)) - (yyval.varSpec) = new SetVarSpec(Alias(e.i),false,false); + if (pp->symbols.get((yyvsp[0].sValue), e) && (e.t == ST_SETVAR || e.t == ST_SET)) { + if(pp->capture && e.t == ST_SET) (yyval.varSpec) = new SetVarSpec(new AST::SetLit(pp->setvals.at(e.i)),false,false); + else (yyval.varSpec) = new SetVarSpec(Alias(e.i),false,false); + } else { pp->err << "Error: undefined identifier for type set " << (yyvsp[0].sValue) << " in line no. " @@ -3159,11 +3596,11 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } free((yyvsp[0].sValue)); } -#line 3163 "gecode/flatzinc/parser.tab.cpp" +#line 3600 "gecode/flatzinc/parser.tab.cpp" break; - case 79: /* set_init: var_par_id '[' FZ_INT_LIT ']' */ -#line 1511 "./gecode/flatzinc/parser.yxx" + case 80: /* set_init: var_par_id '[' FZ_INT_LIT ']' */ +#line 1638 "gecode/flatzinc/parser.yxx" { SymbolEntry e; ParserState* pp = static_cast(parm); @@ -3183,92 +3620,95 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } free((yyvsp[-3].sValue)); } -#line 3187 "gecode/flatzinc/parser.tab.cpp" +#line 3624 "gecode/flatzinc/parser.tab.cpp" break; - case 80: /* set_init_list: %empty */ -#line 1533 "./gecode/flatzinc/parser.yxx" + case 81: /* set_init_list: %empty */ +#line 1660 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = new vector(0); } -#line 3193 "gecode/flatzinc/parser.tab.cpp" +#line 3630 "gecode/flatzinc/parser.tab.cpp" break; - case 81: /* set_init_list: set_init_list_head list_tail */ -#line 1535 "./gecode/flatzinc/parser.yxx" + case 82: /* set_init_list: set_init_list_head list_tail */ +#line 1662 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = (yyvsp[-1].varSpecVec); } -#line 3199 "gecode/flatzinc/parser.tab.cpp" +#line 3636 "gecode/flatzinc/parser.tab.cpp" break; - case 82: /* set_init_list_head: set_init */ -#line 1539 "./gecode/flatzinc/parser.yxx" + case 83: /* set_init_list_head: set_init */ +#line 1666 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = new vector(1); (*(yyval.varSpecVec))[0] = (yyvsp[0].varSpec); } -#line 3205 "gecode/flatzinc/parser.tab.cpp" +#line 3642 "gecode/flatzinc/parser.tab.cpp" break; - case 83: /* set_init_list_head: set_init_list_head ',' set_init */ -#line 1541 "./gecode/flatzinc/parser.yxx" + case 84: /* set_init_list_head: set_init_list_head ',' set_init */ +#line 1668 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = (yyvsp[-2].varSpecVec); (yyval.varSpecVec)->push_back((yyvsp[0].varSpec)); } -#line 3211 "gecode/flatzinc/parser.tab.cpp" +#line 3648 "gecode/flatzinc/parser.tab.cpp" break; - case 84: /* set_var_array_literal: '[' set_init_list ']' */ -#line 1544 "./gecode/flatzinc/parser.yxx" + case 85: /* set_var_array_literal: '[' set_init_list ']' */ +#line 1671 "gecode/flatzinc/parser.yxx" { (yyval.varSpecVec) = (yyvsp[-1].varSpecVec); } -#line 3217 "gecode/flatzinc/parser.tab.cpp" +#line 3654 "gecode/flatzinc/parser.tab.cpp" break; - case 85: /* vardecl_int_var_array_init: %empty */ -#line 1548 "./gecode/flatzinc/parser.yxx" + case 86: /* vardecl_int_var_array_init: %empty */ +#line 1675 "gecode/flatzinc/parser.yxx" { (yyval.oVarSpecVec) = Option* >::none(); } -#line 3223 "gecode/flatzinc/parser.tab.cpp" +#line 3660 "gecode/flatzinc/parser.tab.cpp" break; - case 86: /* vardecl_int_var_array_init: '=' int_var_array_literal */ -#line 1550 "./gecode/flatzinc/parser.yxx" + case 87: /* vardecl_int_var_array_init: '=' int_var_array_literal */ +#line 1677 "gecode/flatzinc/parser.yxx" { (yyval.oVarSpecVec) = Option* >::some((yyvsp[0].varSpecVec)); } -#line 3229 "gecode/flatzinc/parser.tab.cpp" +#line 3666 "gecode/flatzinc/parser.tab.cpp" break; - case 87: /* vardecl_bool_var_array_init: %empty */ -#line 1554 "./gecode/flatzinc/parser.yxx" + case 88: /* vardecl_bool_var_array_init: %empty */ +#line 1681 "gecode/flatzinc/parser.yxx" { (yyval.oVarSpecVec) = Option* >::none(); } -#line 3235 "gecode/flatzinc/parser.tab.cpp" +#line 3672 "gecode/flatzinc/parser.tab.cpp" break; - case 88: /* vardecl_bool_var_array_init: '=' bool_var_array_literal */ -#line 1556 "./gecode/flatzinc/parser.yxx" + case 89: /* vardecl_bool_var_array_init: '=' bool_var_array_literal */ +#line 1683 "gecode/flatzinc/parser.yxx" { (yyval.oVarSpecVec) = Option* >::some((yyvsp[0].varSpecVec)); } -#line 3241 "gecode/flatzinc/parser.tab.cpp" +#line 3678 "gecode/flatzinc/parser.tab.cpp" break; - case 89: /* vardecl_float_var_array_init: %empty */ -#line 1560 "./gecode/flatzinc/parser.yxx" + case 90: /* vardecl_float_var_array_init: %empty */ +#line 1687 "gecode/flatzinc/parser.yxx" { (yyval.oVarSpecVec) = Option* >::none(); } -#line 3247 "gecode/flatzinc/parser.tab.cpp" +#line 3684 "gecode/flatzinc/parser.tab.cpp" break; - case 90: /* vardecl_float_var_array_init: '=' float_var_array_literal */ -#line 1562 "./gecode/flatzinc/parser.yxx" + case 91: /* vardecl_float_var_array_init: '=' float_var_array_literal */ +#line 1689 "gecode/flatzinc/parser.yxx" { (yyval.oVarSpecVec) = Option* >::some((yyvsp[0].varSpecVec)); } -#line 3253 "gecode/flatzinc/parser.tab.cpp" +#line 3690 "gecode/flatzinc/parser.tab.cpp" break; - case 91: /* vardecl_set_var_array_init: %empty */ -#line 1566 "./gecode/flatzinc/parser.yxx" + case 92: /* vardecl_set_var_array_init: %empty */ +#line 1693 "gecode/flatzinc/parser.yxx" { (yyval.oVarSpecVec) = Option* >::none(); } -#line 3259 "gecode/flatzinc/parser.tab.cpp" +#line 3696 "gecode/flatzinc/parser.tab.cpp" break; - case 92: /* vardecl_set_var_array_init: '=' set_var_array_literal */ -#line 1568 "./gecode/flatzinc/parser.yxx" + case 93: /* vardecl_set_var_array_init: '=' set_var_array_literal */ +#line 1695 "gecode/flatzinc/parser.yxx" { (yyval.oVarSpecVec) = Option* >::some((yyvsp[0].varSpecVec)); } -#line 3265 "gecode/flatzinc/parser.tab.cpp" +#line 3702 "gecode/flatzinc/parser.tab.cpp" break; - case 93: /* constraint_item: FZ_CONSTRAINT FZ_ID '(' flat_expr_list ')' annotations */ -#line 1572 "./gecode/flatzinc/parser.yxx" + case 94: /* constraint_item: FZ_CONSTRAINT FZ_ID '(' flat_expr_list ')' annotations */ +#line 1699 "gecode/flatzinc/parser.yxx" { ParserState *pp = static_cast(parm); - if (!pp->hadError) { + if (pp->capture) { + if (!pp->hadError) pp->capture->constraint(*pp,(yyvsp[-4].sValue),(yyvsp[-2].argVec),(yyvsp[0].argVec)); + delete (yyvsp[-2].argVec); delete (yyvsp[0].argVec); + } else if (!pp->hadError) { std::string cid((yyvsp[-4].sValue)); if (cid=="gecode_on_restart_status" && (yyvsp[-2].argVec)->a[0]->isIntVar()) { pp->status_idx = getBaseIntVar(pp,(yyvsp[-2].argVec)->a[0]->getIntVar()); @@ -3430,15 +3870,18 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } free((yyvsp[-4].sValue)); } -#line 3434 "gecode/flatzinc/parser.tab.cpp" +#line 3874 "gecode/flatzinc/parser.tab.cpp" break; - case 94: /* solve_item: FZ_SOLVE annotations FZ_SATISFY */ -#line 1738 "./gecode/flatzinc/parser.yxx" + case 95: /* solve_item: FZ_SOLVE annotations FZ_SATISFY */ +#line 1868 "gecode/flatzinc/parser.yxx" { ParserState *pp = static_cast(parm); initfg(pp); - if (!pp->hadError) { + if (pp->capture) { + if (!pp->hadError) pp->capture->solve(*pp,Capture::Method::Satisfy,(yyvsp[-1].argVec)); + delete (yyvsp[-1].argVec); + } else if (!pp->hadError) { try { pp->fg->solve((yyvsp[-1].argVec)); } catch (Gecode::FlatZinc::Error& e) { @@ -3448,15 +3891,18 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); delete (yyvsp[-1].argVec); } } -#line 3452 "gecode/flatzinc/parser.tab.cpp" +#line 3895 "gecode/flatzinc/parser.tab.cpp" break; - case 95: /* solve_item: FZ_SOLVE annotations minmax solve_expr */ -#line 1752 "./gecode/flatzinc/parser.yxx" + case 96: /* solve_item: FZ_SOLVE annotations minmax solve_expr */ +#line 1885 "gecode/flatzinc/parser.yxx" { ParserState *pp = static_cast(parm); initfg(pp); - if (!pp->hadError) { + if (pp->capture) { + if (!pp->hadError) pp->capture->solve(*pp,(yyvsp[-1].bValue)?Capture::Method::Minimize:Capture::Method::Maximize,(yyvsp[-2].argVec)); + delete (yyvsp[-2].argVec); + } else if (!pp->hadError) { try { int v = (yyvsp[0].iValue) < 0 ? (-(yyvsp[0].iValue)-1) : (yyvsp[0].iValue); bool vi = (yyvsp[0].iValue) >= 0; @@ -3471,37 +3917,37 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); delete (yyvsp[-2].argVec); } } -#line 3475 "gecode/flatzinc/parser.tab.cpp" +#line 3921 "gecode/flatzinc/parser.tab.cpp" break; - case 96: /* int_ti_expr_tail: FZ_INT */ -#line 1777 "./gecode/flatzinc/parser.yxx" + case 97: /* int_ti_expr_tail: FZ_INT */ +#line 1913 "gecode/flatzinc/parser.yxx" { (yyval.oSet) = Option::none(); } -#line 3481 "gecode/flatzinc/parser.tab.cpp" +#line 3927 "gecode/flatzinc/parser.tab.cpp" break; - case 97: /* int_ti_expr_tail: '{' int_list '}' */ -#line 1779 "./gecode/flatzinc/parser.yxx" - { (yyval.oSet) = Option::some(new AST::SetLit(*(yyvsp[-1].setValue))); } -#line 3487 "gecode/flatzinc/parser.tab.cpp" + case 98: /* int_ti_expr_tail: '{' int_list '}' */ +#line 1915 "gecode/flatzinc/parser.yxx" + { (yyval.oSet) = Option::some(new AST::SetLit(*(yyvsp[-1].setValue))); if(static_cast(parm)->capture) delete (yyvsp[-1].setValue); } +#line 3933 "gecode/flatzinc/parser.tab.cpp" break; - case 98: /* int_ti_expr_tail: FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT */ -#line 1781 "./gecode/flatzinc/parser.yxx" + case 99: /* int_ti_expr_tail: FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT */ +#line 1917 "gecode/flatzinc/parser.yxx" { (yyval.oSet) = Option::some(new AST::SetLit((yyvsp[-2].iValue), (yyvsp[0].iValue))); } -#line 3495 "gecode/flatzinc/parser.tab.cpp" +#line 3941 "gecode/flatzinc/parser.tab.cpp" break; - case 99: /* bool_ti_expr_tail: FZ_BOOL */ -#line 1787 "./gecode/flatzinc/parser.yxx" + case 100: /* bool_ti_expr_tail: FZ_BOOL */ +#line 1923 "gecode/flatzinc/parser.yxx" { (yyval.oSet) = Option::none(); } -#line 3501 "gecode/flatzinc/parser.tab.cpp" +#line 3947 "gecode/flatzinc/parser.tab.cpp" break; - case 100: /* bool_ti_expr_tail: '{' bool_list_head list_tail '}' */ -#line 1789 "./gecode/flatzinc/parser.yxx" + case 101: /* bool_ti_expr_tail: '{' bool_list_head list_tail '}' */ +#line 1925 "gecode/flatzinc/parser.yxx" { bool haveTrue = false; bool haveFalse = false; for (int i=(yyvsp[-2].setValue)->size(); i--;) { @@ -3512,192 +3958,192 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); (yyval.oSet) = Option::some( new AST::SetLit(!haveFalse,haveTrue)); } -#line 3516 "gecode/flatzinc/parser.tab.cpp" +#line 3962 "gecode/flatzinc/parser.tab.cpp" break; - case 101: /* float_ti_expr_tail: FZ_FLOAT */ -#line 1802 "./gecode/flatzinc/parser.yxx" + case 102: /* float_ti_expr_tail: FZ_FLOAT */ +#line 1938 "gecode/flatzinc/parser.yxx" { (yyval.oPFloat) = Option* >::none(); } -#line 3522 "gecode/flatzinc/parser.tab.cpp" +#line 3968 "gecode/flatzinc/parser.tab.cpp" break; - case 102: /* float_ti_expr_tail: FZ_FLOAT_LIT FZ_DOTDOT FZ_FLOAT_LIT */ -#line 1804 "./gecode/flatzinc/parser.yxx" + case 103: /* float_ti_expr_tail: FZ_FLOAT_LIT FZ_DOTDOT FZ_FLOAT_LIT */ +#line 1940 "gecode/flatzinc/parser.yxx" { std::pair* dom = new std::pair((yyvsp[-2].dValue),(yyvsp[0].dValue)); (yyval.oPFloat) = Option* >::some(dom); } -#line 3529 "gecode/flatzinc/parser.tab.cpp" +#line 3975 "gecode/flatzinc/parser.tab.cpp" break; - case 103: /* set_literal: '{' int_list '}' */ -#line 1813 "./gecode/flatzinc/parser.yxx" - { (yyval.setLit) = new AST::SetLit(*(yyvsp[-1].setValue)); } -#line 3535 "gecode/flatzinc/parser.tab.cpp" + case 104: /* set_literal: '{' int_list '}' */ +#line 1949 "gecode/flatzinc/parser.yxx" + { (yyval.setLit) = new AST::SetLit(*(yyvsp[-1].setValue)); if(static_cast(parm)->capture) delete (yyvsp[-1].setValue); } +#line 3981 "gecode/flatzinc/parser.tab.cpp" break; - case 104: /* set_literal: FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT */ -#line 1815 "./gecode/flatzinc/parser.yxx" + case 105: /* set_literal: FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT */ +#line 1951 "gecode/flatzinc/parser.yxx" { (yyval.setLit) = new AST::SetLit((yyvsp[-2].iValue), (yyvsp[0].iValue)); } -#line 3541 "gecode/flatzinc/parser.tab.cpp" +#line 3987 "gecode/flatzinc/parser.tab.cpp" break; - case 105: /* int_list: %empty */ -#line 1821 "./gecode/flatzinc/parser.yxx" + case 106: /* int_list: %empty */ +#line 1957 "gecode/flatzinc/parser.yxx" { (yyval.setValue) = new vector(0); } -#line 3547 "gecode/flatzinc/parser.tab.cpp" +#line 3993 "gecode/flatzinc/parser.tab.cpp" break; - case 106: /* int_list: int_list_head list_tail */ -#line 1823 "./gecode/flatzinc/parser.yxx" + case 107: /* int_list: int_list_head list_tail */ +#line 1959 "gecode/flatzinc/parser.yxx" { (yyval.setValue) = (yyvsp[-1].setValue); } -#line 3553 "gecode/flatzinc/parser.tab.cpp" +#line 3999 "gecode/flatzinc/parser.tab.cpp" break; - case 107: /* int_list_head: FZ_INT_LIT */ -#line 1827 "./gecode/flatzinc/parser.yxx" + case 108: /* int_list_head: FZ_INT_LIT */ +#line 1963 "gecode/flatzinc/parser.yxx" { (yyval.setValue) = new vector(1); (*(yyval.setValue))[0] = (yyvsp[0].iValue); } -#line 3559 "gecode/flatzinc/parser.tab.cpp" +#line 4005 "gecode/flatzinc/parser.tab.cpp" break; - case 108: /* int_list_head: int_list_head ',' FZ_INT_LIT */ -#line 1829 "./gecode/flatzinc/parser.yxx" + case 109: /* int_list_head: int_list_head ',' FZ_INT_LIT */ +#line 1965 "gecode/flatzinc/parser.yxx" { (yyval.setValue) = (yyvsp[-2].setValue); (yyval.setValue)->push_back((yyvsp[0].iValue)); } -#line 3565 "gecode/flatzinc/parser.tab.cpp" +#line 4011 "gecode/flatzinc/parser.tab.cpp" break; - case 109: /* bool_list: %empty */ -#line 1833 "./gecode/flatzinc/parser.yxx" + case 110: /* bool_list: %empty */ +#line 1969 "gecode/flatzinc/parser.yxx" { (yyval.setValue) = new vector(0); } -#line 3571 "gecode/flatzinc/parser.tab.cpp" +#line 4017 "gecode/flatzinc/parser.tab.cpp" break; - case 110: /* bool_list: bool_list_head list_tail */ -#line 1835 "./gecode/flatzinc/parser.yxx" + case 111: /* bool_list: bool_list_head list_tail */ +#line 1971 "gecode/flatzinc/parser.yxx" { (yyval.setValue) = (yyvsp[-1].setValue); } -#line 3577 "gecode/flatzinc/parser.tab.cpp" +#line 4023 "gecode/flatzinc/parser.tab.cpp" break; - case 111: /* bool_list_head: FZ_BOOL_LIT */ -#line 1839 "./gecode/flatzinc/parser.yxx" + case 112: /* bool_list_head: FZ_BOOL_LIT */ +#line 1975 "gecode/flatzinc/parser.yxx" { (yyval.setValue) = new vector(1); (*(yyval.setValue))[0] = (yyvsp[0].iValue); } -#line 3583 "gecode/flatzinc/parser.tab.cpp" +#line 4029 "gecode/flatzinc/parser.tab.cpp" break; - case 112: /* bool_list_head: bool_list_head ',' FZ_BOOL_LIT */ -#line 1841 "./gecode/flatzinc/parser.yxx" + case 113: /* bool_list_head: bool_list_head ',' FZ_BOOL_LIT */ +#line 1977 "gecode/flatzinc/parser.yxx" { (yyval.setValue) = (yyvsp[-2].setValue); (yyval.setValue)->push_back((yyvsp[0].iValue)); } -#line 3589 "gecode/flatzinc/parser.tab.cpp" +#line 4035 "gecode/flatzinc/parser.tab.cpp" break; - case 113: /* float_list: %empty */ -#line 1845 "./gecode/flatzinc/parser.yxx" + case 114: /* float_list: %empty */ +#line 1981 "gecode/flatzinc/parser.yxx" { (yyval.floatSetValue) = new vector(0); } -#line 3595 "gecode/flatzinc/parser.tab.cpp" +#line 4041 "gecode/flatzinc/parser.tab.cpp" break; - case 114: /* float_list: float_list_head list_tail */ -#line 1847 "./gecode/flatzinc/parser.yxx" + case 115: /* float_list: float_list_head list_tail */ +#line 1983 "gecode/flatzinc/parser.yxx" { (yyval.floatSetValue) = (yyvsp[-1].floatSetValue); } -#line 3601 "gecode/flatzinc/parser.tab.cpp" +#line 4047 "gecode/flatzinc/parser.tab.cpp" break; - case 115: /* float_list_head: FZ_FLOAT_LIT */ -#line 1851 "./gecode/flatzinc/parser.yxx" + case 116: /* float_list_head: FZ_FLOAT_LIT */ +#line 1987 "gecode/flatzinc/parser.yxx" { (yyval.floatSetValue) = new vector(1); (*(yyval.floatSetValue))[0] = (yyvsp[0].dValue); } -#line 3607 "gecode/flatzinc/parser.tab.cpp" +#line 4053 "gecode/flatzinc/parser.tab.cpp" break; - case 116: /* float_list_head: float_list_head ',' FZ_FLOAT_LIT */ -#line 1853 "./gecode/flatzinc/parser.yxx" + case 117: /* float_list_head: float_list_head ',' FZ_FLOAT_LIT */ +#line 1989 "gecode/flatzinc/parser.yxx" { (yyval.floatSetValue) = (yyvsp[-2].floatSetValue); (yyval.floatSetValue)->push_back((yyvsp[0].dValue)); } -#line 3613 "gecode/flatzinc/parser.tab.cpp" +#line 4059 "gecode/flatzinc/parser.tab.cpp" break; - case 117: /* set_literal_list: %empty */ -#line 1857 "./gecode/flatzinc/parser.yxx" + case 118: /* set_literal_list: %empty */ +#line 1993 "gecode/flatzinc/parser.yxx" { (yyval.setValueList) = new vector(0); } -#line 3619 "gecode/flatzinc/parser.tab.cpp" +#line 4065 "gecode/flatzinc/parser.tab.cpp" break; - case 118: /* set_literal_list: set_literal_list_head list_tail */ -#line 1859 "./gecode/flatzinc/parser.yxx" + case 119: /* set_literal_list: set_literal_list_head list_tail */ +#line 1995 "gecode/flatzinc/parser.yxx" { (yyval.setValueList) = (yyvsp[-1].setValueList); } -#line 3625 "gecode/flatzinc/parser.tab.cpp" +#line 4071 "gecode/flatzinc/parser.tab.cpp" break; - case 119: /* set_literal_list_head: set_literal */ -#line 1863 "./gecode/flatzinc/parser.yxx" + case 120: /* set_literal_list_head: set_literal */ +#line 1999 "gecode/flatzinc/parser.yxx" { (yyval.setValueList) = new vector(1); (*(yyval.setValueList))[0] = *(yyvsp[0].setLit); delete (yyvsp[0].setLit); } -#line 3631 "gecode/flatzinc/parser.tab.cpp" +#line 4077 "gecode/flatzinc/parser.tab.cpp" break; - case 120: /* set_literal_list_head: set_literal_list_head ',' set_literal */ -#line 1865 "./gecode/flatzinc/parser.yxx" + case 121: /* set_literal_list_head: set_literal_list_head ',' set_literal */ +#line 2001 "gecode/flatzinc/parser.yxx" { (yyval.setValueList) = (yyvsp[-2].setValueList); (yyval.setValueList)->push_back(*(yyvsp[0].setLit)); delete (yyvsp[0].setLit); } -#line 3637 "gecode/flatzinc/parser.tab.cpp" +#line 4083 "gecode/flatzinc/parser.tab.cpp" break; - case 121: /* flat_expr_list: flat_expr */ -#line 1873 "./gecode/flatzinc/parser.yxx" + case 122: /* flat_expr_list: flat_expr */ +#line 2009 "gecode/flatzinc/parser.yxx" { (yyval.argVec) = new AST::Array((yyvsp[0].arg)); } -#line 3643 "gecode/flatzinc/parser.tab.cpp" +#line 4089 "gecode/flatzinc/parser.tab.cpp" break; - case 122: /* flat_expr_list: flat_expr_list ',' flat_expr */ -#line 1875 "./gecode/flatzinc/parser.yxx" + case 123: /* flat_expr_list: flat_expr_list ',' flat_expr */ +#line 2011 "gecode/flatzinc/parser.yxx" { (yyval.argVec) = (yyvsp[-2].argVec); (yyval.argVec)->append((yyvsp[0].arg)); } -#line 3649 "gecode/flatzinc/parser.tab.cpp" +#line 4095 "gecode/flatzinc/parser.tab.cpp" break; - case 123: /* flat_expr: non_array_expr */ -#line 1879 "./gecode/flatzinc/parser.yxx" + case 124: /* flat_expr: non_array_expr */ +#line 2015 "gecode/flatzinc/parser.yxx" { (yyval.arg) = (yyvsp[0].arg); } -#line 3655 "gecode/flatzinc/parser.tab.cpp" +#line 4101 "gecode/flatzinc/parser.tab.cpp" break; - case 124: /* flat_expr: '[' non_array_expr_list ']' */ -#line 1881 "./gecode/flatzinc/parser.yxx" + case 125: /* flat_expr: '[' non_array_expr_list ']' */ +#line 2017 "gecode/flatzinc/parser.yxx" { (yyval.arg) = (yyvsp[-1].argVec); } -#line 3661 "gecode/flatzinc/parser.tab.cpp" +#line 4107 "gecode/flatzinc/parser.tab.cpp" break; - case 125: /* non_array_expr_opt: %empty */ -#line 1885 "./gecode/flatzinc/parser.yxx" + case 126: /* non_array_expr_opt: %empty */ +#line 2021 "gecode/flatzinc/parser.yxx" { (yyval.oArg) = Option::none(); } -#line 3667 "gecode/flatzinc/parser.tab.cpp" +#line 4113 "gecode/flatzinc/parser.tab.cpp" break; - case 126: /* non_array_expr_opt: '=' non_array_expr */ -#line 1887 "./gecode/flatzinc/parser.yxx" + case 127: /* non_array_expr_opt: '=' non_array_expr */ +#line 2023 "gecode/flatzinc/parser.yxx" { (yyval.oArg) = Option::some((yyvsp[0].arg)); } -#line 3673 "gecode/flatzinc/parser.tab.cpp" +#line 4119 "gecode/flatzinc/parser.tab.cpp" break; - case 127: /* non_array_expr: FZ_BOOL_LIT */ -#line 1891 "./gecode/flatzinc/parser.yxx" + case 128: /* non_array_expr: FZ_BOOL_LIT */ +#line 2027 "gecode/flatzinc/parser.yxx" { (yyval.arg) = new AST::BoolLit((yyvsp[0].iValue)); } -#line 3679 "gecode/flatzinc/parser.tab.cpp" +#line 4125 "gecode/flatzinc/parser.tab.cpp" break; - case 128: /* non_array_expr: FZ_INT_LIT */ -#line 1893 "./gecode/flatzinc/parser.yxx" + case 129: /* non_array_expr: FZ_INT_LIT */ +#line 2029 "gecode/flatzinc/parser.yxx" { (yyval.arg) = new AST::IntLit((yyvsp[0].iValue)); } -#line 3685 "gecode/flatzinc/parser.tab.cpp" +#line 4131 "gecode/flatzinc/parser.tab.cpp" break; - case 129: /* non_array_expr: FZ_FLOAT_LIT */ -#line 1895 "./gecode/flatzinc/parser.yxx" + case 130: /* non_array_expr: FZ_FLOAT_LIT */ +#line 2031 "gecode/flatzinc/parser.yxx" { (yyval.arg) = new AST::FloatLit((yyvsp[0].dValue)); } -#line 3691 "gecode/flatzinc/parser.tab.cpp" +#line 4137 "gecode/flatzinc/parser.tab.cpp" break; - case 130: /* non_array_expr: set_literal */ -#line 1897 "./gecode/flatzinc/parser.yxx" + case 131: /* non_array_expr: set_literal */ +#line 2033 "gecode/flatzinc/parser.yxx" { (yyval.arg) = (yyvsp[0].setLit); } -#line 3697 "gecode/flatzinc/parser.tab.cpp" +#line 4143 "gecode/flatzinc/parser.tab.cpp" break; - case 131: /* non_array_expr: var_par_id */ -#line 1899 "./gecode/flatzinc/parser.yxx" + case 132: /* non_array_expr: var_par_id */ +#line 2035 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); SymbolEntry e; @@ -3789,15 +4235,15 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); << " in line no. " << yyget_lineno(pp->yyscanner) << std::endl; pp->hadError = true; - (yyval.arg) = NULL; + (yyval.arg) = pp->capture ? new AST::IntLit(0) : NULL; } free((yyvsp[0].sValue)); } -#line 3797 "gecode/flatzinc/parser.tab.cpp" +#line 4243 "gecode/flatzinc/parser.tab.cpp" break; - case 132: /* non_array_expr: var_par_id '[' non_array_expr ']' */ -#line 1995 "./gecode/flatzinc/parser.yxx" + case 133: /* non_array_expr: var_par_id '[' non_array_expr ']' */ +#line 2131 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); int i = -1; @@ -3809,35 +4255,35 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); delete (yyvsp[-1].arg); free((yyvsp[-3].sValue)); } -#line 3813 "gecode/flatzinc/parser.tab.cpp" +#line 4259 "gecode/flatzinc/parser.tab.cpp" break; - case 133: /* non_array_expr_list: %empty */ -#line 2009 "./gecode/flatzinc/parser.yxx" + case 134: /* non_array_expr_list: %empty */ +#line 2145 "gecode/flatzinc/parser.yxx" { (yyval.argVec) = new AST::Array(0); } -#line 3819 "gecode/flatzinc/parser.tab.cpp" +#line 4265 "gecode/flatzinc/parser.tab.cpp" break; - case 134: /* non_array_expr_list: non_array_expr_list_head list_tail */ -#line 2011 "./gecode/flatzinc/parser.yxx" + case 135: /* non_array_expr_list: non_array_expr_list_head list_tail */ +#line 2147 "gecode/flatzinc/parser.yxx" { (yyval.argVec) = (yyvsp[-1].argVec); } -#line 3825 "gecode/flatzinc/parser.tab.cpp" +#line 4271 "gecode/flatzinc/parser.tab.cpp" break; - case 135: /* non_array_expr_list_head: non_array_expr */ -#line 2015 "./gecode/flatzinc/parser.yxx" + case 136: /* non_array_expr_list_head: non_array_expr */ +#line 2151 "gecode/flatzinc/parser.yxx" { (yyval.argVec) = new AST::Array((yyvsp[0].arg)); } -#line 3831 "gecode/flatzinc/parser.tab.cpp" +#line 4277 "gecode/flatzinc/parser.tab.cpp" break; - case 136: /* non_array_expr_list_head: non_array_expr_list_head ',' non_array_expr */ -#line 2017 "./gecode/flatzinc/parser.yxx" + case 137: /* non_array_expr_list_head: non_array_expr_list_head ',' non_array_expr */ +#line 2153 "gecode/flatzinc/parser.yxx" { (yyval.argVec) = (yyvsp[-2].argVec); (yyval.argVec)->append((yyvsp[0].arg)); } -#line 3837 "gecode/flatzinc/parser.tab.cpp" +#line 4283 "gecode/flatzinc/parser.tab.cpp" break; - case 137: /* solve_expr: var_par_id */ -#line 2025 "./gecode/flatzinc/parser.yxx" + case 138: /* solve_expr: var_par_id */ +#line 2161 "gecode/flatzinc/parser.yxx" { ParserState *pp = static_cast(parm); SymbolEntry e; @@ -3845,13 +4291,20 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); if (haveSym) { switch (e.t) { case ST_INTVAR: + if(pp->capture){AST::IntVar value(e.i);pp->capture->objective(*pp,&value);} (yyval.iValue) = e.i; break; case ST_FLOATVAR: + if(pp->capture){AST::FloatVar value(e.i);pp->capture->objective(*pp,&value);} (yyval.iValue) = -e.i-1; break; case ST_INT: case ST_FLOAT: + if(pp->capture){ + if(e.t==ST_INT){AST::IntLit value(e.i);pp->capture->objective(*pp,&value);} + else {AST::FloatLit value(pp->floatvals.at(e.i));pp->capture->objective(*pp,&value);} + (yyval.iValue)=0;break; + } pp->intvars.push_back(varspec("OBJ_CONST_INTRODUCED", new IntVarSpec(0,true,false))); (yyval.iValue) = pp->intvars.size()-1; @@ -3871,49 +4324,58 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } free((yyvsp[0].sValue)); } -#line 3875 "gecode/flatzinc/parser.tab.cpp" +#line 4328 "gecode/flatzinc/parser.tab.cpp" break; - case 138: /* solve_expr: FZ_INT_LIT */ -#line 2059 "./gecode/flatzinc/parser.yxx" + case 139: /* solve_expr: FZ_INT_LIT */ +#line 2202 "gecode/flatzinc/parser.yxx" { ParserState *pp = static_cast(parm); - pp->intvars.push_back(varspec("OBJ_CONST_INTRODUCED", - new IntVarSpec(0,true,false))); - (yyval.iValue) = pp->intvars.size()-1; + if(pp->capture){AST::IntLit value((yyvsp[0].iValue));pp->capture->objective(*pp,&value);(yyval.iValue)=0;} + else {pp->intvars.push_back(varspec("OBJ_CONST_INTRODUCED", + new IntVarSpec(0,true,false)));(yyval.iValue) = pp->intvars.size()-1;} } -#line 3886 "gecode/flatzinc/parser.tab.cpp" +#line 4339 "gecode/flatzinc/parser.tab.cpp" break; - case 139: /* solve_expr: FZ_FLOAT_LIT */ -#line 2066 "./gecode/flatzinc/parser.yxx" + case 140: /* solve_expr: FZ_FLOAT_LIT */ +#line 2209 "gecode/flatzinc/parser.yxx" { ParserState *pp = static_cast(parm); - pp->intvars.push_back(varspec("OBJ_CONST_INTRODUCED", - new IntVarSpec(0,true,false))); - (yyval.iValue) = pp->intvars.size()-1; + if(pp->capture){AST::FloatLit value((yyvsp[0].dValue));pp->capture->objective(*pp,&value);(yyval.iValue)=0;} + else {pp->intvars.push_back(varspec("OBJ_CONST_INTRODUCED", + new IntVarSpec(0,true,false)));(yyval.iValue) = pp->intvars.size()-1;} } -#line 3897 "gecode/flatzinc/parser.tab.cpp" +#line 4350 "gecode/flatzinc/parser.tab.cpp" break; - case 140: /* solve_expr: var_par_id '[' FZ_INT_LIT ']' */ -#line 2073 "./gecode/flatzinc/parser.yxx" + case 141: /* solve_expr: var_par_id '[' FZ_INT_LIT ']' */ +#line 2216 "gecode/flatzinc/parser.yxx" { SymbolEntry e; ParserState *pp = static_cast(parm); - if ( (!pp->symbols.get((yyvsp[-3].sValue), e)) || + if(pp->capture){ + if(!pp->symbols.get((yyvsp[-3].sValue),e)||(e.t!=ST_INTVARARRAY&&e.t!=ST_FLOATVARARRAY)|| + e.i<0||static_cast(e.i)>=pp->arrays.size()||(yyvsp[-1].iValue)<1||(yyvsp[-1].iValue)>pp->arrays[e.i]) { + pp->capture->fail(*pp,Capture::Status::InvalidInput,"invalid objective array reference");(yyval.iValue)=0; + } else { + const int slot=pp->arrays.at(static_cast(e.i)+(yyvsp[-1].iValue)); + if(e.t==ST_INTVARARRAY){AST::IntVar value(slot);pp->capture->objective(*pp,&value);(yyval.iValue)=slot;} + else{AST::FloatVar value(slot);pp->capture->objective(*pp,&value);(yyval.iValue)=-slot-1;} + } + } else if ( (!pp->symbols.get((yyvsp[-3].sValue), e)) || (e.t != ST_INTVARARRAY && e.t != ST_FLOATVARARRAY)) { pp->err << "Error: unknown int or float variable array " << (yyvsp[-3].sValue) << " in line no. " << yyget_lineno(pp->yyscanner) << std::endl; pp->hadError = true; } - if ((yyvsp[-1].iValue) == 0 || (yyvsp[-1].iValue) > pp->arrays[e.i]) { + if (!pp->capture && ((yyvsp[-1].iValue) == 0 || (yyvsp[-1].iValue) > pp->arrays[e.i])) { pp->err << "Error: array index out of bounds for array " << (yyvsp[-3].sValue) << " in line no. " << yyget_lineno(pp->yyscanner) << std::endl; pp->hadError = true; - } else { + } else if (!pp->capture) { if (e.t == ST_INTVARARRAY) (yyval.iValue) = pp->arrays[e.i+(yyvsp[-1].iValue)]; else @@ -3921,103 +4383,103 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); } free((yyvsp[-3].sValue)); } -#line 3925 "gecode/flatzinc/parser.tab.cpp" +#line 4387 "gecode/flatzinc/parser.tab.cpp" break; - case 143: /* annotations: %empty */ -#line 2107 "./gecode/flatzinc/parser.yxx" + case 144: /* annotations: %empty */ +#line 2259 "gecode/flatzinc/parser.yxx" { (yyval.argVec) = NULL; } -#line 3931 "gecode/flatzinc/parser.tab.cpp" +#line 4393 "gecode/flatzinc/parser.tab.cpp" break; - case 144: /* annotations: annotations_head */ -#line 2109 "./gecode/flatzinc/parser.yxx" + case 145: /* annotations: annotations_head */ +#line 2261 "gecode/flatzinc/parser.yxx" { (yyval.argVec) = (yyvsp[0].argVec); } -#line 3937 "gecode/flatzinc/parser.tab.cpp" +#line 4399 "gecode/flatzinc/parser.tab.cpp" break; - case 145: /* annotations_head: FZ_COLONCOLON annotation */ -#line 2113 "./gecode/flatzinc/parser.yxx" + case 146: /* annotations_head: FZ_COLONCOLON annotation */ +#line 2265 "gecode/flatzinc/parser.yxx" { (yyval.argVec) = new AST::Array((yyvsp[0].arg)); } -#line 3943 "gecode/flatzinc/parser.tab.cpp" +#line 4405 "gecode/flatzinc/parser.tab.cpp" break; - case 146: /* annotations_head: annotations_head FZ_COLONCOLON annotation */ -#line 2115 "./gecode/flatzinc/parser.yxx" + case 147: /* annotations_head: annotations_head FZ_COLONCOLON annotation */ +#line 2267 "gecode/flatzinc/parser.yxx" { (yyval.argVec) = (yyvsp[-2].argVec); (yyval.argVec)->append((yyvsp[0].arg)); } -#line 3949 "gecode/flatzinc/parser.tab.cpp" +#line 4411 "gecode/flatzinc/parser.tab.cpp" break; - case 147: /* annotation: FZ_ID '(' annotation_list ')' */ -#line 2119 "./gecode/flatzinc/parser.yxx" + case 148: /* annotation: FZ_ID '(' annotation_list ')' */ +#line 2271 "gecode/flatzinc/parser.yxx" { - (yyval.arg) = new AST::Call((yyvsp[-3].sValue), AST::extractSingleton((yyvsp[-1].arg))); free((yyvsp[-3].sValue)); + (yyval.arg) = new AST::Call((yyvsp[-3].sValue), static_cast(parm)->capture ? (yyvsp[-1].arg) : AST::extractSingleton((yyvsp[-1].arg))); free((yyvsp[-3].sValue)); } -#line 3957 "gecode/flatzinc/parser.tab.cpp" +#line 4419 "gecode/flatzinc/parser.tab.cpp" break; - case 148: /* annotation: annotation_expr */ -#line 2123 "./gecode/flatzinc/parser.yxx" + case 149: /* annotation: annotation_expr */ +#line 2275 "gecode/flatzinc/parser.yxx" { (yyval.arg) = (yyvsp[0].arg); } -#line 3963 "gecode/flatzinc/parser.tab.cpp" +#line 4425 "gecode/flatzinc/parser.tab.cpp" break; - case 149: /* annotation_list: annotation */ -#line 2127 "./gecode/flatzinc/parser.yxx" + case 150: /* annotation_list: annotation */ +#line 2279 "gecode/flatzinc/parser.yxx" { (yyval.arg) = new AST::Array((yyvsp[0].arg)); } -#line 3969 "gecode/flatzinc/parser.tab.cpp" +#line 4431 "gecode/flatzinc/parser.tab.cpp" break; - case 150: /* annotation_list: annotation_list ',' annotation */ -#line 2129 "./gecode/flatzinc/parser.yxx" + case 151: /* annotation_list: annotation_list ',' annotation */ +#line 2281 "gecode/flatzinc/parser.yxx" { (yyval.arg) = (yyvsp[-2].arg); (yyval.arg)->append((yyvsp[0].arg)); } -#line 3975 "gecode/flatzinc/parser.tab.cpp" +#line 4437 "gecode/flatzinc/parser.tab.cpp" break; - case 151: /* annotation_expr: ann_non_array_expr */ -#line 2133 "./gecode/flatzinc/parser.yxx" + case 152: /* annotation_expr: ann_non_array_expr */ +#line 2285 "gecode/flatzinc/parser.yxx" { (yyval.arg) = (yyvsp[0].arg); } -#line 3981 "gecode/flatzinc/parser.tab.cpp" +#line 4443 "gecode/flatzinc/parser.tab.cpp" break; - case 152: /* annotation_expr: '[' ']' */ -#line 2135 "./gecode/flatzinc/parser.yxx" + case 153: /* annotation_expr: '[' ']' */ +#line 2287 "gecode/flatzinc/parser.yxx" { (yyval.arg) = new AST::Array(); } -#line 3987 "gecode/flatzinc/parser.tab.cpp" +#line 4449 "gecode/flatzinc/parser.tab.cpp" break; - case 153: /* annotation_expr: '[' annotation_list annotation_list_tail ']' */ -#line 2137 "./gecode/flatzinc/parser.yxx" + case 154: /* annotation_expr: '[' annotation_list annotation_list_tail ']' */ +#line 2289 "gecode/flatzinc/parser.yxx" { (yyval.arg) = (yyvsp[-2].arg); } -#line 3993 "gecode/flatzinc/parser.tab.cpp" +#line 4455 "gecode/flatzinc/parser.tab.cpp" break; - case 156: /* ann_non_array_expr: FZ_BOOL_LIT */ -#line 2143 "./gecode/flatzinc/parser.yxx" + case 157: /* ann_non_array_expr: FZ_BOOL_LIT */ +#line 2295 "gecode/flatzinc/parser.yxx" { (yyval.arg) = new AST::BoolLit((yyvsp[0].iValue)); } -#line 3999 "gecode/flatzinc/parser.tab.cpp" +#line 4461 "gecode/flatzinc/parser.tab.cpp" break; - case 157: /* ann_non_array_expr: FZ_INT_LIT */ -#line 2145 "./gecode/flatzinc/parser.yxx" + case 158: /* ann_non_array_expr: FZ_INT_LIT */ +#line 2297 "gecode/flatzinc/parser.yxx" { (yyval.arg) = new AST::IntLit((yyvsp[0].iValue)); } -#line 4005 "gecode/flatzinc/parser.tab.cpp" +#line 4467 "gecode/flatzinc/parser.tab.cpp" break; - case 158: /* ann_non_array_expr: FZ_FLOAT_LIT */ -#line 2147 "./gecode/flatzinc/parser.yxx" + case 159: /* ann_non_array_expr: FZ_FLOAT_LIT */ +#line 2299 "gecode/flatzinc/parser.yxx" { (yyval.arg) = new AST::FloatLit((yyvsp[0].dValue)); } -#line 4011 "gecode/flatzinc/parser.tab.cpp" +#line 4473 "gecode/flatzinc/parser.tab.cpp" break; - case 159: /* ann_non_array_expr: set_literal */ -#line 2149 "./gecode/flatzinc/parser.yxx" + case 160: /* ann_non_array_expr: set_literal */ +#line 2301 "gecode/flatzinc/parser.yxx" { (yyval.arg) = (yyvsp[0].setLit); } -#line 4017 "gecode/flatzinc/parser.tab.cpp" +#line 4479 "gecode/flatzinc/parser.tab.cpp" break; - case 160: /* ann_non_array_expr: var_par_id */ -#line 2151 "./gecode/flatzinc/parser.yxx" + case 161: /* ann_non_array_expr: var_par_id */ +#line 2303 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); SymbolEntry e; @@ -4123,11 +4585,11 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); (yyval.arg) = getVarRefArg(pp,(yyvsp[0].sValue),true); free((yyvsp[0].sValue)); } -#line 4127 "gecode/flatzinc/parser.tab.cpp" +#line 4589 "gecode/flatzinc/parser.tab.cpp" break; - case 161: /* ann_non_array_expr: var_par_id '[' ann_non_array_expr ']' */ -#line 2257 "./gecode/flatzinc/parser.yxx" + case 162: /* ann_non_array_expr: var_par_id '[' ann_non_array_expr ']' */ +#line 2409 "gecode/flatzinc/parser.yxx" { ParserState* pp = static_cast(parm); int i = -1; @@ -4136,22 +4598,23 @@ YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default); (yyval.arg) = getArrayElement(static_cast(parm),(yyvsp[-3].sValue),i,true); else (yyval.arg) = new AST::IntLit(0); // keep things consistent + if(pp->capture) delete (yyvsp[-1].arg); free((yyvsp[-3].sValue)); } -#line 4142 "gecode/flatzinc/parser.tab.cpp" +#line 4605 "gecode/flatzinc/parser.tab.cpp" break; - case 162: /* ann_non_array_expr: FZ_STRING_LIT */ -#line 2268 "./gecode/flatzinc/parser.yxx" + case 163: /* ann_non_array_expr: FZ_STRING_LIT */ +#line 2421 "gecode/flatzinc/parser.yxx" { (yyval.arg) = new AST::String((yyvsp[0].sValue)); free((yyvsp[0].sValue)); } -#line 4151 "gecode/flatzinc/parser.tab.cpp" +#line 4614 "gecode/flatzinc/parser.tab.cpp" break; -#line 4155 "gecode/flatzinc/parser.tab.cpp" +#line 4618 "gecode/flatzinc/parser.tab.cpp" default: break; } diff --git a/gecode/flatzinc/parser.tab.hpp b/gecode/flatzinc/parser.tab.hpp index 094a72380a..6d8fdbf211 100644 --- a/gecode/flatzinc/parser.tab.hpp +++ b/gecode/flatzinc/parser.tab.hpp @@ -107,7 +107,7 @@ extern int yydebug; #if ! defined YYSTYPE && ! defined YYSTYPE_IS_DECLARED union YYSTYPE { -#line 614 "./gecode/flatzinc/parser.yxx" +#line 635 "gecode/flatzinc/parser.yxx" int iValue; char* sValue; bool bValue; double dValue; std::vector* setValue; Gecode::FlatZinc::AST::SetLit* setLit; @@ -122,8 +122,7 @@ union YYSTYPE Gecode::FlatZinc::AST::Node* arg; Gecode::FlatZinc::AST::Array* argVec; - -#line 127 "gecode/flatzinc/parser.tab.hpp" +#line 126 "gecode/flatzinc/parser.tab.hpp" }; typedef union YYSTYPE YYSTYPE; diff --git a/gecode/flatzinc/parser.yxx b/gecode/flatzinc/parser.yxx index 40d0aa2312..e5bda309df 100755 --- a/gecode/flatzinc/parser.yxx +++ b/gecode/flatzinc/parser.yxx @@ -149,6 +149,10 @@ AST::Node* getArrayElement(ParserState* pp, string id, int offset, } return new AST::FloatVar(pp->arrays[e.i+offset],n); } + case ST_BOOLVALARRAY: + if(!pp->capture) break; + if(offset>pp->arrays[e.i]) goto error; + return new AST::BoolLit(pp->arrays[e.i+offset]); case ST_INTVALARRAY: if (offset > pp->arrays[e.i]) goto error; @@ -199,18 +203,32 @@ AST::Node* getVarRefArg(ParserState* pp, string id, bool annotation = false) { void addDomainConstraint(ParserState* pp, std::string id, AST::Node* var, Option& dom) { - if (!dom()) + if (!dom()) { + if(pp->capture) delete var; return; + } + if(pp->capture && pp->domainConstraints.size()>=pp->capture->options.max_constraints) { + pp->capture->fail(*pp,Capture::Status::ResourceLimit,"declaration domain count exceeds capture limit"); + delete var;delete dom.some();dom=Option::none();return; + } AST::Array* args = new AST::Array(2); args->a[0] = var; args->a[1] = dom.some(); pp->domainConstraints.push_back(new ConExpr(id, args, NULL)); + if(pp->capture) dom=Option::none(); } void addDomainConstraint(ParserState* pp, AST::Node* var, Option* > dom) { - if (!dom()) + if (!dom()) { + if(pp->capture) delete var; return; + } + if(pp->capture && (pp->capture->options.max_constraints<2 || + pp->domainConstraints.size()>pp->capture->options.max_constraints-2)) { + pp->capture->fail(*pp,Capture::Status::ResourceLimit,"declaration domain count exceeds capture limit"); + delete var;delete dom.some();dom=Option*>::none();return; + } { AST::Array* args = new AST::Array(2); args->a[0] = new AST::FloatLit(dom.some()->first); @@ -225,6 +243,7 @@ void addDomainConstraint(ParserState* pp, AST::Node* var, pp->domainConstraints.push_back(new ConExpr("float_le", args, NULL)); } delete dom.some(); + if(pp->capture) dom=Option*>::none(); } int getBaseIntVar(ParserState* pp, int i) { @@ -270,6 +289,7 @@ int getBaseSetVar(ParserState* pp, int i) { */ void initfg(ParserState* pp) { + if (pp->capture) return; // capture never creates native variables/actors if (!pp->hadError) pp->fg->init(pp->intvars.size(), pp->boolvars.size(), @@ -494,13 +514,14 @@ void fillPrinter(ParserState& pp, Gecode::FlatZinc::Printer& p) { #endif } -AST::Node* arrayOutput(AST::Call* ann) { +AST::Node* arrayOutput(AST::Call* ann, bool capture=false) { + AST::Node* dimensions=capture ? ann->args->getArray()->a.at(0) : ann->args; AST::Array* a = NULL; - if (ann->args->isArray()) { - a = ann->args->getArray(); + if (dimensions->isArray()) { + a = dimensions->getArray(); } else { - a = new AST::Array(ann->args); + a = new AST::Array(dimensions); } std::ostringstream oss; @@ -523,7 +544,7 @@ AST::Node* arrayOutput(AST::Call* ann) { } } - if (!ann->args->isArray()) { + if (!dimensions->isArray()) { a->a[0] = NULL; delete a; } @@ -624,7 +645,7 @@ namespace Gecode { namespace FlatZinc { Gecode::FlatZinc::Option* > oVarSpecVec; Gecode::FlatZinc::AST::Node* arg; Gecode::FlatZinc::AST::Array* argVec; - } +} %define parse.error verbose @@ -707,13 +728,23 @@ namespace Gecode { namespace FlatZinc { %type annotations annotations_head %type annotation annotation_list +/* Only the new capture entry enables error-path semantic-value ownership. */ +%destructor { if(static_cast(parm)->capture) free($$); } +%destructor { if(static_cast(parm)->capture) delete $$; } +%destructor { if(static_cast(parm)->capture && $$()) delete $$.some(); } +%destructor { if(static_cast(parm)->capture) { for(auto* p:*$$) delete p; delete $$; } } +%destructor { if(static_cast(parm)->capture && $$()) { for(auto* p:*$$.some()) delete p; delete $$.some(); } } + %% /********************************/ /* main goal and item lists */ /********************************/ -model : preddecl_items vardecl_items constraint_items solve_item ';' +model : preddecl_items vardecl_items + { ParserState* pp=static_cast(parm); + if(pp->capture && !pp->hadError) pp->capture->begin(*pp); } + constraint_items solve_item ';' preddecl_items: /* empty */ @@ -729,7 +760,9 @@ vardecl_items: vardecl_items_head: vardecl_item ';' + { if(static_cast(parm)->capture && static_cast(parm)->hadError) YYABORT; } | vardecl_items_head vardecl_item ';' + { if(static_cast(parm)->capture && static_cast(parm)->hadError) YYABORT; } constraint_items: /* emtpy */ @@ -737,7 +770,9 @@ constraint_items: constraint_items_head: constraint_item ';' + { if(static_cast(parm)->capture && static_cast(parm)->hadError) YYABORT; } | constraint_items_head constraint_item ';' + { if(static_cast(parm)->capture && static_cast(parm)->hadError) YYABORT; } /********************************/ /* predicate declarations */ @@ -791,7 +826,9 @@ vardecl_item: FZ_VAR int_ti_expr_tail ':' var_par_id annotations non_array_expr_opt { ParserState* pp = static_cast(parm); - bool print = $5 != NULL && $5->hasAtom("output_var"); + if(pp->capture) pp->capture->declaration(*pp,$4,$5); + if(pp->capture) pp->capture->variable_count(*pp,1); + bool print = (!pp->capture || !pp->hadError) && $5 != NULL && $5->hasAtom("output_var"); bool funcDep = $5 != NULL && $5->hasAtom("is_defined_var"); yyassert(pp, pp->symbols.put($4, se_iv(pp->intvars.size())), @@ -818,12 +855,15 @@ vardecl_item: pp->intvars.push_back(varspec($4, new IntVarSpec($2,!print,funcDep))); } + if(pp->capture && pp->hadError && $6() && $2()) delete $2.some(); delete $5; free($4); } | FZ_VAR bool_ti_expr_tail ':' var_par_id annotations non_array_expr_opt { ParserState* pp = static_cast(parm); - bool print = $5 != NULL && $5->hasAtom("output_var"); + if(pp->capture) pp->capture->declaration(*pp,$4,$5); + if(pp->capture) pp->capture->variable_count(*pp,1); + bool print = (!pp->capture || !pp->hadError) && $5 != NULL && $5->hasAtom("output_var"); bool funcDep = $5 != NULL && $5->hasAtom("is_defined_var"); yyassert(pp, pp->symbols.put($4, se_bv(pp->boolvars.size())), @@ -850,12 +890,15 @@ vardecl_item: pp->boolvars.push_back(varspec($4, new BoolVarSpec($2,!print,funcDep))); } + if(pp->capture && pp->hadError && $6() && $2()) delete $2.some(); delete $5; free($4); } | FZ_VAR float_ti_expr_tail ':' var_par_id annotations non_array_expr_opt { ParserState* pp = static_cast(parm); - bool print = $5 != NULL && $5->hasAtom("output_var"); + if(pp->capture) pp->capture->declaration(*pp,$4,$5); + if(pp->capture) pp->capture->variable_count(*pp,1); + bool print = (!pp->capture || !pp->hadError) && $5 != NULL && $5->hasAtom("output_var"); bool funcDep = $5 != NULL && $5->hasAtom("is_defined_var"); yyassert(pp, pp->symbols.put($4, se_fv(pp->floatvars.size())), @@ -879,6 +922,7 @@ vardecl_item: if (!pp->hadError && $2()) { AST::FloatVar* fv = new AST::FloatVar(pp->floatvars.size()-1); addDomainConstraint(pp, fv, $2); + if(pp->capture) $2=Option*>::none(); } delete arg; } else { @@ -889,12 +933,15 @@ vardecl_item: pp->floatvars.push_back(varspec($4, new FloatVarSpec(dom,!print,funcDep))); } + if(pp->capture && pp->hadError && $6() && $2()) delete $2.some(); delete $5; free($4); } | FZ_VAR FZ_SET FZ_OF int_ti_expr_tail ':' var_par_id annotations non_array_expr_opt { ParserState* pp = static_cast(parm); - bool print = $7 != NULL && $7->hasAtom("output_var"); + if(pp->capture) pp->capture->declaration(*pp,$6,$7); + if(pp->capture) pp->capture->variable_count(*pp,1); + bool print = (!pp->capture || !pp->hadError) && $7 != NULL && $7->hasAtom("output_var"); bool funcDep = $7 != NULL && $7->hasAtom("is_defined_var"); yyassert(pp, pp->symbols.put($6, se_sv(pp->setvars.size())), @@ -922,46 +969,62 @@ vardecl_item: pp->setvars.push_back(varspec($6, new SetVarSpec($4,!print,funcDep))); } + if(pp->capture && pp->hadError && $8() && $4()) delete $4.some(); delete $7; free($6); } | FZ_INT ':' var_par_id annotations '=' non_array_expr { ParserState* pp = static_cast(parm); + if(pp->capture && $4) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, $6->isInt(), "Invalid int initializer"); + if (!pp->capture || !pp->hadError) { yyassert(pp, pp->symbols.put($3, se_i($6->getInt())), "Duplicate symbol"); + } + if(pp->capture) delete $6; delete $4; free($3); } | FZ_FLOAT ':' var_par_id annotations '=' non_array_expr { ParserState* pp = static_cast(parm); + if(pp->capture && $4) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, $6->isFloat(), "Invalid float initializer"); + if (!pp->capture || !pp->hadError) { pp->floatvals.push_back($6->getFloat()); yyassert(pp, pp->symbols.put($3, se_f(pp->floatvals.size()-1)), "Duplicate symbol"); + } + if(pp->capture) delete $6; delete $4; free($3); } | FZ_BOOL ':' var_par_id annotations '=' non_array_expr { ParserState* pp = static_cast(parm); + if(pp->capture && $4) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, $6->isBool(), "Invalid bool initializer"); + if (!pp->capture || !pp->hadError) { yyassert(pp, pp->symbols.put($3, se_b($6->getBool())), "Duplicate symbol"); + } + if(pp->capture) delete $6; delete $4; free($3); } | FZ_SET FZ_OF FZ_INT ':' var_par_id annotations '=' non_array_expr { ParserState* pp = static_cast(parm); + if(pp->capture && $6) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, $8->isSet(), "Invalid set initializer"); + if (!pp->capture || !pp->hadError) { AST::SetLit* set = $8->getSet(); pp->setvals.push_back(*set); yyassert(pp, pp->symbols.put($5, se_s(pp->setvals.size()-1)), "Duplicate symbol"); delete set; + } else delete $8; delete $6; free($5); } | FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_VAR int_ti_expr_tail ':' @@ -969,8 +1032,12 @@ vardecl_item: { ParserState* pp = static_cast(parm); yyassert(pp, $3==1, "Arrays must start at 1"); + if(pp->capture) { pp->capture->array_size(*pp,$5); + pp->capture->variable_count(*pp,$5,$13()); + } if (!pp->hadError) { - bool print = $12 != NULL && $12->hasCall("output_array"); + if(pp->capture) pp->capture->declaration(*pp,$11,$12,$5); + bool print = (!pp->capture || !pp->hadError) && $12 != NULL && $12->hasCall("output_array"); vector vars($5); if (!pp->hadError) { if ($13()) { @@ -979,6 +1046,7 @@ vardecl_item: "Initializer size does not match array dimension"); if (!pp->hadError) { for (int i=0; i<$5; i++) { + if(pp->capture && pp->hadError) break; IntVarSpec* ivsv = static_cast((*vsv)[i]); if (ivsv->alias) { if (print) @@ -989,6 +1057,7 @@ vardecl_item: ivsv->introduced = false; vars[i] = pp->intvars.size(); pp->intvars.push_back(varspec($11, ivsv)); + if(pp->capture) (*vsv)[i]=nullptr; } if (!pp->hadError && $9()) { Option opt = @@ -999,10 +1068,12 @@ vardecl_item: } } } + if(pp->capture) { for(auto* child:*vsv) delete child; $13=Option*>::none(); } delete vsv; } else { if ($5>0) { for (int i=0; i<$5; i++) { + if(pp->capture && pp->hadError) break; Option dom = $9() ? Option::some(new AST::SetLit($9.some())) : Option::none(); @@ -1011,12 +1082,12 @@ vardecl_item: pp->intvars.push_back(varspec($11, ispec)); } } - if ($9()) delete $9.some(); + if ($9()) { delete $9.some(); if(pp->capture) $9=Option::none(); } } } - if (print) { + if (print && (!pp->capture || !pp->hadError)) { AST::Array* a = new AST::Array(); - a->a.push_back(arrayOutput($12->getCall("output_array"))); + a->a.push_back(arrayOutput($12->getCall("output_array"),pp->capture!=nullptr)); AST::Array* output = new AST::Array(); for (int i=0; i<$5; i++) output->a.push_back(new AST::IntVar(vars[i])); @@ -1032,14 +1103,22 @@ vardecl_item: pp->symbols.put($11, se_iva(iva)), "Duplicate symbol"); } + if(pp->capture) { + if($13()) { for(auto* child:*$13.some()) delete child; delete $13.some(); } + if($9()) delete $9.some(); + } delete $12; free($11); } | FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_VAR bool_ti_expr_tail ':' var_par_id annotations vardecl_bool_var_array_init { ParserState* pp = static_cast(parm); - bool print = $12 != NULL && $12->hasCall("output_array"); + if(pp->capture) pp->capture->declaration(*pp,$11,$12,$5); + bool print = (!pp->capture || !pp->hadError) && $12 != NULL && $12->hasCall("output_array"); yyassert(pp, $3==1, "Arrays must start at 1"); + if(pp->capture) { pp->capture->array_size(*pp,$5); + pp->capture->variable_count(*pp,$5,$13()); + } if (!pp->hadError) { vector vars($5); if ($13()) { @@ -1048,6 +1127,7 @@ vardecl_item: "Initializer size does not match array dimension"); if (!pp->hadError) { for (int i=0; i<$5; i++) { + if(pp->capture && pp->hadError) break; BoolVarSpec* bvsv = static_cast((*vsv)[i]); if (bvsv->alias) { if (print) @@ -1058,6 +1138,7 @@ vardecl_item: bvsv->introduced = false; vars[i] = pp->boolvars.size(); pp->boolvars.push_back(varspec($11, (*vsv)[i])); + if(pp->capture) (*vsv)[i]=nullptr; } if (!pp->hadError && $9()) { Option opt = @@ -1068,9 +1149,11 @@ vardecl_item: } } } - delete vsv; + if(pp->capture) { for(auto* child:*vsv) delete child; $13=Option*>::none(); } + delete vsv; } else { for (int i=0; i<$5; i++) { + if(pp->capture && pp->hadError) break; Option dom = $9() ? Option::some(new AST::SetLit($9.some())) : Option::none(); @@ -1078,11 +1161,11 @@ vardecl_item: pp->boolvars.push_back(varspec($11, new BoolVarSpec(dom,!print,false))); } - if ($9()) delete $9.some(); + if ($9()) { delete $9.some(); if(pp->capture) $9=Option::none(); } } - if (print) { + if (print && (!pp->capture || !pp->hadError)) { AST::Array* a = new AST::Array(); - a->a.push_back(arrayOutput($12->getCall("output_array"))); + a->a.push_back(arrayOutput($12->getCall("output_array"),pp->capture!=nullptr)); AST::Array* output = new AST::Array(); for (int i=0; i<$5; i++) output->a.push_back(new AST::BoolVar(vars[i])); @@ -1098,6 +1181,10 @@ vardecl_item: pp->symbols.put($11, se_bva(bva)), "Duplicate symbol"); } + if(pp->capture) { + if($13()) { for(auto* child:*$13.some()) delete child; delete $13.some(); } + if($9()) delete $9.some(); + } delete $12; free($11); } | FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_VAR @@ -1106,8 +1193,12 @@ vardecl_item: { ParserState* pp = static_cast(parm); yyassert(pp, $3==1, "Arrays must start at 1"); + if(pp->capture) { pp->capture->array_size(*pp,$5); + pp->capture->variable_count(*pp,$5,$13()); + } if (!pp->hadError) { - bool print = $12 != NULL && $12->hasCall("output_array"); + if(pp->capture) pp->capture->declaration(*pp,$11,$12,$5); + bool print = (!pp->capture || !pp->hadError) && $12 != NULL && $12->hasCall("output_array"); vector vars($5); if (!pp->hadError) { if ($13()) { @@ -1116,6 +1207,7 @@ vardecl_item: "Initializer size does not match array dimension"); if (!pp->hadError) { for (int i=0; i<$5; i++) { + if(pp->capture && pp->hadError) break; FloatVarSpec* ivsv = static_cast((*vsv)[i]); if (ivsv->alias) { if (print) @@ -1126,6 +1218,7 @@ vardecl_item: ivsv->introduced = false; vars[i] = pp->floatvars.size(); pp->floatvars.push_back(varspec($11, ivsv)); + if(pp->capture) (*vsv)[i]=nullptr; } if (!pp->hadError && $9()) { Option*> opt = @@ -1136,6 +1229,7 @@ vardecl_item: } } } + if(pp->capture) { for(auto* child:*vsv) delete child; $13=Option*>::none(); } delete vsv; } else { if ($5>0) { @@ -1143,6 +1237,7 @@ vardecl_item: $9() ? Option >::some(*$9.some()) : Option >::none(); for (int i=0; i<$5; i++) { + if(pp->capture && pp->hadError) break; FloatVarSpec* ispec = new FloatVarSpec(dom,!print,false); vars[i] = pp->floatvars.size(); pp->floatvars.push_back(varspec($11, ispec)); @@ -1150,9 +1245,9 @@ vardecl_item: } } } - if (print) { + if (print && (!pp->capture || !pp->hadError)) { AST::Array* a = new AST::Array(); - a->a.push_back(arrayOutput($12->getCall("output_array"))); + a->a.push_back(arrayOutput($12->getCall("output_array"),pp->capture!=nullptr)); AST::Array* output = new AST::Array(); for (int i=0; i<$5; i++) output->a.push_back(new AST::FloatVar(vars[i])); @@ -1168,15 +1263,23 @@ vardecl_item: pp->symbols.put($11, se_fva(fva)), "Duplicate symbol"); } - if ($9()) delete $9.some(); + if ($9()) { delete $9.some(); if(pp->capture) $9=Option*>::none(); } + if(pp->capture) { + if($13()) { for(auto* child:*$13.some()) delete child; delete $13.some(); } + if($9()) delete $9.some(); + } delete $12; free($11); } | FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_VAR FZ_SET FZ_OF int_ti_expr_tail ':' var_par_id annotations vardecl_set_var_array_init { ParserState* pp = static_cast(parm); - bool print = $14 != NULL && $14->hasCall("output_array"); + if(pp->capture) pp->capture->declaration(*pp,$13,$14,$5); + bool print = (!pp->capture || !pp->hadError) && $14 != NULL && $14->hasCall("output_array"); yyassert(pp, $3==1, "Arrays must start at 1"); + if(pp->capture) { pp->capture->array_size(*pp,$5); + pp->capture->variable_count(*pp,$5,$15()); + } if (!pp->hadError) { vector vars($5); if ($15()) { @@ -1185,6 +1288,7 @@ vardecl_item: "Initializer size does not match array dimension"); if (!pp->hadError) { for (int i=0; i<$5; i++) { + if(pp->capture && pp->hadError) break; SetVarSpec* svsv = static_cast((*vsv)[i]); if (svsv->alias) { if (print) @@ -1195,6 +1299,7 @@ vardecl_item: svsv->introduced = false; vars[i] = pp->setvars.size(); pp->setvars.push_back(varspec($13, (*vsv)[i])); + if(pp->capture) (*vsv)[i]=nullptr; } if (!pp->hadError && $11()) { Option opt = @@ -1205,10 +1310,12 @@ vardecl_item: } } } - delete vsv; + if(pp->capture) { for(auto* child:*vsv) delete child; $15=Option*>::none(); } + delete vsv; } else { if ($5>0) { for (int i=0; i<$5; i++) { + if(pp->capture && pp->hadError) break; Option dom = $11() ? Option::some(new AST::SetLit($11.some())) : Option::none(); @@ -1216,12 +1323,12 @@ vardecl_item: vars[i] = pp->setvars.size(); pp->setvars.push_back(varspec($13, ispec)); } - if ($11()) delete $11.some(); + if ($11()) { delete $11.some(); if(pp->capture) $11=Option::none(); } } } - if (print) { + if (print && (!pp->capture || !pp->hadError)) { AST::Array* a = new AST::Array(); - a->a.push_back(arrayOutput($14->getCall("output_array"))); + a->a.push_back(arrayOutput($14->getCall("output_array"),pp->capture!=nullptr)); AST::Array* output = new AST::Array(); for (int i=0; i<$5; i++) output->a.push_back(new AST::SetVar(vars[i])); @@ -1237,13 +1344,19 @@ vardecl_item: pp->symbols.put($13, se_sva(sva)), "Duplicate symbol"); } + if(pp->capture) { + if($15()) { for(auto* child:*$15.some()) delete child; delete $15.some(); } + if($11()) delete $11.some(); + } delete $14; free($13); } | FZ_ARRAY '[' FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT ']' FZ_OF FZ_INT ':' var_par_id annotations '=' '[' int_list ']' { ParserState* pp = static_cast(parm); + if(pp->capture && $11) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, $3==1, "Arrays must start at 1"); + if(pp->capture) pp->capture->array_size(*pp,$5); yyassert(pp, $14->size() == static_cast($5), "Initializer size does not match array dimension"); @@ -1264,7 +1377,9 @@ vardecl_item: var_par_id annotations '=' '[' bool_list ']' { ParserState* pp = static_cast(parm); + if(pp->capture && $11) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, $3==1, "Arrays must start at 1"); + if(pp->capture) pp->capture->array_size(*pp,$5); yyassert(pp, $14->size() == static_cast($5), "Initializer size does not match array dimension"); if (!pp->hadError) { @@ -1284,7 +1399,9 @@ vardecl_item: var_par_id annotations '=' '[' float_list ']' { ParserState* pp = static_cast(parm); + if(pp->capture && $11) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, $3==1, "Arrays must start at 1"); + if(pp->capture) pp->capture->array_size(*pp,$5); yyassert(pp, $14->size() == static_cast($5), "Initializer size does not match array dimension"); if (!pp->hadError) { @@ -1304,7 +1421,9 @@ vardecl_item: var_par_id annotations '=' '[' set_literal_list ']' { ParserState* pp = static_cast(parm); + if(pp->capture && $13) pp->capture->fail(*pp,Capture::Status::InvalidInput,"parameter declarations cannot have annotations"); yyassert(pp, $3==1, "Arrays must start at 1"); + if(pp->capture) pp->capture->array_size(*pp,$5); yyassert(pp, $16->size() == static_cast($5), "Initializer size does not match array dimension"); if (!pp->hadError) { @@ -1331,8 +1450,10 @@ int_init : { SymbolEntry e; ParserState* pp = static_cast(parm); - if (pp->symbols.get($1, e) && (e.t == ST_INTVAR || e.t == ST_INT)) - $$ = new IntVarSpec(Alias(e.i),false,false); + if (pp->symbols.get($1, e) && (e.t == ST_INTVAR || e.t == ST_INT)) { + if(pp->capture && e.t == ST_INT) $$ = new IntVarSpec(e.i,false,false); + else $$ = new IntVarSpec(Alias(e.i),false,false); + } else { pp->err << "Error: undefined identifier for type int " << $1 << " in line no. " @@ -1388,8 +1509,10 @@ float_init : { SymbolEntry e; ParserState* pp = static_cast(parm); - if (pp->symbols.get($1, e) && (e.t == ST_FLOATVAR || e.t == ST_FLOAT)) - $$ = new FloatVarSpec(Alias(e.i),false,false); + if (pp->symbols.get($1, e) && (e.t == ST_FLOATVAR || e.t == ST_FLOAT)) { + if(pp->capture && e.t == ST_FLOAT) $$ = new FloatVarSpec(pp->floatvals.at(e.i),false,false); + else $$ = new FloatVarSpec(Alias(e.i),false,false); + } else { pp->err << "Error: undefined identifier for type float " << $1 << " in line no. " @@ -1443,8 +1566,10 @@ bool_init : { SymbolEntry e; ParserState* pp = static_cast(parm); - if (pp->symbols.get($1, e) && (e.t == ST_BOOLVAR || e.t == ST_BOOL)) - $$ = new BoolVarSpec(Alias(e.i),false,false); + if (pp->symbols.get($1, e) && (e.t == ST_BOOLVAR || e.t == ST_BOOL)) { + if(pp->capture && e.t == ST_BOOL) $$ = new BoolVarSpec(e.i != 0,false,false); + else $$ = new BoolVarSpec(Alias(e.i),false,false); + } else { pp->err << "Error: undefined identifier for type bool " << $1 << " in line no. " @@ -1496,8 +1621,10 @@ set_init : { ParserState* pp = static_cast(parm); SymbolEntry e; - if (pp->symbols.get($1, e) && (e.t == ST_SETVAR || e.t == ST_SET)) - $$ = new SetVarSpec(Alias(e.i),false,false); + if (pp->symbols.get($1, e) && (e.t == ST_SETVAR || e.t == ST_SET)) { + if(pp->capture && e.t == ST_SET) $$ = new SetVarSpec(new AST::SetLit(pp->setvals.at(e.i)),false,false); + else $$ = new SetVarSpec(Alias(e.i),false,false); + } else { pp->err << "Error: undefined identifier for type set " << $1 << " in line no. " @@ -1571,7 +1698,10 @@ constraint_item : FZ_CONSTRAINT FZ_ID '(' flat_expr_list ')' annotations { ParserState *pp = static_cast(parm); - if (!pp->hadError) { + if (pp->capture) { + if (!pp->hadError) pp->capture->constraint(*pp,$2,$4,$6); + delete $4; delete $6; + } else if (!pp->hadError) { std::string cid($2); if (cid=="gecode_on_restart_status" && $4->a[0]->isIntVar()) { pp->status_idx = getBaseIntVar(pp,$4->a[0]->getIntVar()); @@ -1738,7 +1868,10 @@ solve_item : { ParserState *pp = static_cast(parm); initfg(pp); - if (!pp->hadError) { + if (pp->capture) { + if (!pp->hadError) pp->capture->solve(*pp,Capture::Method::Satisfy,$2); + delete $2; + } else if (!pp->hadError) { try { pp->fg->solve($2); } catch (Gecode::FlatZinc::Error& e) { @@ -1752,7 +1885,10 @@ solve_item : { ParserState *pp = static_cast(parm); initfg(pp); - if (!pp->hadError) { + if (pp->capture) { + if (!pp->hadError) pp->capture->solve(*pp,$3?Capture::Method::Minimize:Capture::Method::Maximize,$2); + delete $2; + } else if (!pp->hadError) { try { int v = $4 < 0 ? (-$4-1) : $4; bool vi = $4 >= 0; @@ -1776,7 +1912,7 @@ int_ti_expr_tail : FZ_INT { $$ = Option::none(); } | '{' int_list '}' - { $$ = Option::some(new AST::SetLit(*$2)); } + { $$ = Option::some(new AST::SetLit(*$2)); if(static_cast(parm)->capture) delete $2; } | FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT { $$ = Option::some(new AST::SetLit($1, $3)); @@ -1810,7 +1946,7 @@ float_ti_expr_tail : set_literal : '{' int_list '}' - { $$ = new AST::SetLit(*$2); } + { $$ = new AST::SetLit(*$2); if(static_cast(parm)->capture) delete $2; } | FZ_INT_LIT FZ_DOTDOT FZ_INT_LIT { $$ = new AST::SetLit($1, $3); } @@ -1987,7 +2123,7 @@ non_array_expr : << " in line no. " << yyget_lineno(pp->yyscanner) << std::endl; pp->hadError = true; - $$ = NULL; + $$ = pp->capture ? new AST::IntLit(0) : NULL; } free($1); } @@ -2029,13 +2165,20 @@ solve_expr: if (haveSym) { switch (e.t) { case ST_INTVAR: + if(pp->capture){AST::IntVar value(e.i);pp->capture->objective(*pp,&value);} $$ = e.i; break; case ST_FLOATVAR: + if(pp->capture){AST::FloatVar value(e.i);pp->capture->objective(*pp,&value);} $$ = -e.i-1; break; case ST_INT: case ST_FLOAT: + if(pp->capture){ + if(e.t==ST_INT){AST::IntLit value(e.i);pp->capture->objective(*pp,&value);} + else {AST::FloatLit value(pp->floatvals.at(e.i));pp->capture->objective(*pp,&value);} + $$=0;break; + } pp->intvars.push_back(varspec("OBJ_CONST_INTRODUCED", new IntVarSpec(0,true,false))); $$ = pp->intvars.size()-1; @@ -2058,34 +2201,43 @@ solve_expr: | FZ_INT_LIT { ParserState *pp = static_cast(parm); - pp->intvars.push_back(varspec("OBJ_CONST_INTRODUCED", - new IntVarSpec(0,true,false))); - $$ = pp->intvars.size()-1; + if(pp->capture){AST::IntLit value($1);pp->capture->objective(*pp,&value);$$=0;} + else {pp->intvars.push_back(varspec("OBJ_CONST_INTRODUCED", + new IntVarSpec(0,true,false)));$$ = pp->intvars.size()-1;} } | FZ_FLOAT_LIT { ParserState *pp = static_cast(parm); - pp->intvars.push_back(varspec("OBJ_CONST_INTRODUCED", - new IntVarSpec(0,true,false))); - $$ = pp->intvars.size()-1; + if(pp->capture){AST::FloatLit value($1);pp->capture->objective(*pp,&value);$$=0;} + else {pp->intvars.push_back(varspec("OBJ_CONST_INTRODUCED", + new IntVarSpec(0,true,false)));$$ = pp->intvars.size()-1;} } | var_par_id '[' FZ_INT_LIT ']' { SymbolEntry e; ParserState *pp = static_cast(parm); - if ( (!pp->symbols.get($1, e)) || + if(pp->capture){ + if(!pp->symbols.get($1,e)||(e.t!=ST_INTVARARRAY&&e.t!=ST_FLOATVARARRAY)|| + e.i<0||static_cast(e.i)>=pp->arrays.size()||$3<1||$3>pp->arrays[e.i]) { + pp->capture->fail(*pp,Capture::Status::InvalidInput,"invalid objective array reference");$$=0; + } else { + const int slot=pp->arrays.at(static_cast(e.i)+$3); + if(e.t==ST_INTVARARRAY){AST::IntVar value(slot);pp->capture->objective(*pp,&value);$$=slot;} + else{AST::FloatVar value(slot);pp->capture->objective(*pp,&value);$$=-slot-1;} + } + } else if ( (!pp->symbols.get($1, e)) || (e.t != ST_INTVARARRAY && e.t != ST_FLOATVARARRAY)) { pp->err << "Error: unknown int or float variable array " << $1 << " in line no. " << yyget_lineno(pp->yyscanner) << std::endl; pp->hadError = true; } - if ($3 == 0 || $3 > pp->arrays[e.i]) { + if (!pp->capture && ($3 == 0 || $3 > pp->arrays[e.i])) { pp->err << "Error: array index out of bounds for array " << $1 << " in line no. " << yyget_lineno(pp->yyscanner) << std::endl; pp->hadError = true; - } else { + } else if (!pp->capture) { if (e.t == ST_INTVARARRAY) $$ = pp->arrays[e.i+$3]; else @@ -2117,7 +2269,7 @@ annotations_head : annotation : FZ_ID '(' annotation_list ')' { - $$ = new AST::Call($1, AST::extractSingleton($3)); free($1); + $$ = new AST::Call($1, static_cast(parm)->capture ? $3 : AST::extractSingleton($3)); free($1); } | annotation_expr { $$ = $1; } @@ -2262,6 +2414,7 @@ ann_non_array_expr : $$ = getArrayElement(static_cast(parm),$1,i,true); else $$ = new AST::IntLit(0); // keep things consistent + if(pp->capture) delete $3; free($1); } | FZ_STRING_LIT diff --git a/gecode/kernel/core.cpp b/gecode/kernel/core.cpp index 4293cfe6a8..1f94ec0415 100644 --- a/gecode/kernel/core.cpp +++ b/gecode/kernel/core.cpp @@ -129,7 +129,7 @@ namespace Gecode { // Initialize propagator and brancher links pl.init(); bl.init(); - b_status = b_commit = Brancher::cast(&bl); + b_status = b_commit = &bl; // Initialize array for forced deletion to be empty d_fst = d_cur = d_lst = nullptr; // Initialize space as stable but not failed @@ -526,13 +526,13 @@ namespace Gecode { * can be used for commit an exhausted brancher can actually be deleted. * This becomes known when choice is called. */ - while (b_status != Brancher::cast(&bl)) - if (b_status->status(*this)) { + while (b_status != &bl) + if (Brancher::cast(b_status)->status(*this)) { // Brancher still has choices to generate return SS_BRANCH; } else { // Brancher is exhausted - b_status = Brancher::cast(b_status->next()); + b_status = b_status->next(); } // No brancher with alternatives left, space is solved return SS_SOLVED; @@ -568,43 +568,44 @@ namespace Gecode { Space::choice(void) { if (!stable()) throw SpaceNotStable("Space::choice"); - if (failed() || (b_status == Brancher::cast(&bl))) { + if (failed() || (b_status == &bl)) { // There are no more choices to be generated // Delete all branchers - Brancher* b = Brancher::cast(bl.next()); - while (b != Brancher::cast(&bl)) { - Brancher* d = b; - b = Brancher::cast(b->next()); + ActorLink* b = bl.next(); + while (b != &bl) { + Brancher* d = Brancher::cast(b); + b = b->next(); rfree(d,d->dispose(*this)); } bl.init(); - b_status = b_commit = Brancher::cast(&bl); + b_status = b_commit = &bl; return nullptr; } /* * The call to choice() says that no older choices * can be used. Hence, all branchers that are exhausted can be deleted. */ - Brancher* b = Brancher::cast(bl.next()); + ActorLink* b = bl.next(); while (b != b_status) { - Brancher* d = b; - b = Brancher::cast(b->next()); + Brancher* d = Brancher::cast(b); + b = b->next(); d->unlink(); rfree(d,d->dispose(*this)); } // Make sure that b_commit does not point to a deleted brancher! b_commit = b_status; - return b_status->choice(*this); + return Brancher::cast(b_status)->choice(*this); } const Choice* Space::choice(Archive& e) const { unsigned int id; e >> id; - Brancher* b_cur = Brancher::cast(bl.next()); - while (b_cur != Brancher::cast(&bl)) { - if (id == b_cur->id()) - return b_cur->choice(*this,e); - b_cur = Brancher::cast(b_cur->next()); + ActorLink* b_cur = bl.next(); + while (b_cur != &bl) { + Brancher* b = Brancher::cast(b_cur); + if (id == b->id()) + return b->choice(*this,e); + b_cur = b_cur->next(); } throw SpaceNoBrancher("Space::choice"); } @@ -701,12 +702,13 @@ namespace Gecode { Space::kill_brancher(unsigned int id) { if (failed()) return; - for (Brancher* b = Brancher::cast(bl.next()); - b != Brancher::cast(&bl); b = Brancher::cast(b->next())) + for (ActorLink* link = bl.next(); link != &bl; link = link->next()) { + Brancher* b = Brancher::cast(link); if (b->id() == id) { kill(*b); return; } + } } @@ -740,7 +742,7 @@ namespace Gecode { pc.c.source = &s; pl.init(); bl.init(); - b_status = b_commit = Brancher::cast(&bl); + b_status = b_commit = &bl; // Copy all propagators { ActorLink* p = &pl; @@ -773,14 +775,14 @@ namespace Gecode { } // Setup brancher pointers if (s.b_status == &s.bl) { - b_status = Brancher::cast(&bl); + b_status = &bl; } else { - b_status = Brancher::cast(s.b_status->prev()); + b_status = s.b_status->prev(); } if (s.b_commit == &s.bl) { - b_commit = Brancher::cast(&bl); + b_commit = &bl; } else { - b_commit = Brancher::cast(s.b_commit->prev()); + b_commit = s.b_commit->prev(); } } catch (...) { recover(s); diff --git a/gecode/kernel/core.hpp b/gecode/kernel/core.hpp index 169aab6599..14deee686c 100755 --- a/gecode/kernel/core.hpp +++ b/gecode/kernel/core.hpp @@ -1808,7 +1808,7 @@ namespace Gecode { * * If equal to &bl, no brancher does exist. */ - Brancher* b_status; + ActorLink* b_status; /** * \brief Points to the first brancher to be used for commit * @@ -1820,7 +1820,7 @@ namespace Gecode { * * If equal to &bl, no brancher does exist. */ - Brancher* b_commit; + ActorLink* b_commit; /// Find brancher with identity \a id Brancher* brancher(unsigned int id); @@ -3755,9 +3755,9 @@ namespace Gecode { assert(!failed()); // Make sure that neither b_status nor b_commit does not point to b! if (b_commit == &b) - b_commit = Brancher::cast(b.next()); + b_commit = b.next(); if (b_status == &b) - b_status = Brancher::cast(b.next()); + b_status = b.next(); b.unlink(); rfree(&b,b.dispose(*this)); } @@ -3797,21 +3797,21 @@ namespace Gecode { * recomputation does not generate new choices during recomputation * and hence b_commit is moved from newer to older branchers. */ - Brancher* b_old = b_commit; + ActorLink* b_old = b_commit; // Try whether we are lucky - while (b_commit != Brancher::cast(&bl)) - if (id != b_commit->id()) - b_commit = Brancher::cast(b_commit->next()); + while (b_commit != &bl) + if (id != Brancher::cast(b_commit)->id()) + b_commit = b_commit->next(); else - return b_commit; - if (b_commit == Brancher::cast(&bl)) { + return Brancher::cast(b_commit); + if (b_commit == &bl) { // We did not find the brancher, start at the beginning - b_commit = Brancher::cast(bl.next()); + b_commit = bl.next(); while (b_commit != b_old) - if (id != b_commit->id()) - b_commit = Brancher::cast(b_commit->next()); + if (id != Brancher::cast(b_commit)->id()) + b_commit = b_commit->next(); else - return b_commit; + return Brancher::cast(b_commit); } return nullptr; } @@ -5006,8 +5006,8 @@ namespace Gecode { } q = nullptr; if (!home.pl.empty()) { - c = Propagator::cast(home.pl.next()); - e = Propagator::cast(&home.pl); + c = home.pl.next(); + e = &home.pl; } else { c = e = nullptr; } @@ -5032,8 +5032,8 @@ namespace Gecode { } q = nullptr; if (!home.pl.empty()) { - c = Propagator::cast(home.pl.next()); - e = Propagator::cast(&home.pl); + c = home.pl.next(); + e = &home.pl; } else { c = nullptr; } @@ -5088,8 +5088,8 @@ namespace Gecode { forceinline Space::IdlePropagators::IdlePropagators(Space& home) { - c = Propagator::cast(home.pl.next()); - e = Propagator::cast(&home.pl); + c = home.pl.next(); + e = &home.pl; } forceinline bool Space::IdlePropagators::operator ()(void) const { @@ -5107,7 +5107,7 @@ namespace Gecode { forceinline Space::Branchers::Branchers(Space& home) - : c(Brancher::cast(home.bl.next())), e(&home.bl) {} + : c(home.bl.next()), e(&home.bl) {} forceinline bool Space::Branchers::operator ()(void) const { return c != e; diff --git a/gecode/minimodel/lp-backend.hpp b/gecode/minimodel/lp-backend.hpp new file mode 100644 index 0000000000..2585a67760 --- /dev/null +++ b/gecode/minimodel/lp-backend.hpp @@ -0,0 +1,328 @@ +/* Optional continuous-LP backend for certified Gecode propagation. + * + * HiGHS proposes row multipliers. Only the separate exact certificate + * checker decides whether a lower bound may enter the constraint solver. + */ + +#ifndef __GECODE_MINIMODEL_LP_BACKEND_HPP__ +#define __GECODE_MINIMODEL_LP_BACKEND_HPP__ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Experimental { namespace LpRelaxation { + + struct BoundResult { + bool valid = false; + std::int64_t lower_bound = 0; + /// Diagnostic only: never rounded or used to prune the CP search. + double lp_objective = std::numeric_limits::quiet_NaN(); + /// Optional exact residual data for binary or explicit integer interval bounds. + std::shared_ptr certificate; + /// Copied numerical selection hint only; never an integer feasible witness. + /// Absent unless explicitly requested and HiGHS supplies finite valid values. + std::optional> primal_suggestion; + }; + + struct Stats { + std::uint64_t lp_calls = 0; + double lp_ms = 0.0; + std::uint64_t valid_bounds = 0; + std::uint64_t rejected = 0; + /// Floating-point infeasibility reports, ignored for CP pruning. + std::uint64_t infeasible_status = 0; + std::uint64_t certificate_evaluations = 0; + std::uint64_t conditional_checks = 0; + std::uint64_t variable_fixings = 0; + /// Integer variables whose interval was tightened, including assignments. + std::uint64_t variable_bound_tightenings = 0; + }; + + /** + * Shared, serialized workspace for the descendants of one CP model. + * + * Each call replaces every variable bound, including bounds loosened + * when search revisits a sibling. The previous simplex basis is merely + * a hot start, and is never treated as a certificate for another node. + * This object is deliberately not copied with Gecode spaces. + */ + namespace Detail { + class LpWorkspace { + // Borrowed from the owning, nonmovable backend. Never shared independently. + const SparseLinearModel& model; + const std::vector original_lower_,original_upper_; + const bool binary_; + + private: + mutable std::mutex mutex_; + Highs highs_; + Stats stats_; + std::vector lower_, upper_; + + static void require_ok(HighsStatus status, const char* operation) { + if (status != HighsStatus::kOk) + throw std::runtime_error(std::string("LP backend: HiGHS ") + operation); + } + + void validate_model() const { + const std::size_t n = model.c.size(), m = model.b.size(); + const std::size_t limit = + static_cast(std::numeric_limits::max()); + if ((n > limit) || (m > limit) || + (model.a.size() > limit) || + !LpCertificate::valid_sparse(model.matrix(),m,n)) + throw std::invalid_argument("LP backend: invalid canonical CSR dimensions/indices"); + // These integers are represented exactly as doubles in the LP. + // Restrict the prototype's numerical range; the certificate still + // checks every arithmetic operation independently. + for (const auto* values : {&model.a, &model.b, &model.c}) + for (std::int64_t value : *values) + if ((value < -1000000000LL) || (value > 1000000000LL)) + throw std::invalid_argument("LP backend: coefficient exceeds 1e9"); + } + + public: + LpWorkspace(const SparseLinearModel& input, + std::vector lower,std::vector upper,bool binary) + : model(input),original_lower_(std::move(lower)),original_upper_(std::move(upper)),binary_(binary) { + validate_model(); + if (original_lower_.size()!=model.c.size() || original_upper_.size()!=model.c.size()) + throw std::invalid_argument("LP backend: initial bound dimensions"); + for (std::size_t j=0;joriginal_upper_[j] || + original_lower_[j]Int::Limits::max || + (binary_ && (original_lower_[j]<0 || original_upper_[j]>1))) + throw std::invalid_argument("LP backend: unsupported initial integer bounds"); + require_ok(highs_.setOptionValue("output_flag", false), "output option"); + require_ok(highs_.setOptionValue("threads", 1), "threads option"); + require_ok(highs_.setOptionValue("parallel", "off"), "parallel option"); + require_ok(highs_.setOptionValue("solver", "simplex"), "solver option"); + require_ok(highs_.setOptionValue("simplex_strategy", 1), "simplex strategy"); + require_ok(highs_.setOptionValue("presolve", "off"), "presolve option"); + require_ok(highs_.setOptionValue("simplex_iteration_limit", 10000), + "iteration limit"); + + const std::size_t n = model.c.size(), m = model.b.size(); + lower_.assign(original_lower_.begin(),original_lower_.end()); + upper_.assign(original_upper_.begin(),original_upper_.end()); + if ((n == 0) || (m == 0) || model.a.empty()) + return; // Zero matrices use the checked box bound; native rows enforce feasibility. + + HighsLp lp; + lp.num_col_ = static_cast(n); + lp.num_row_ = static_cast(m); + lp.sense_ = ObjSense::kMinimize; + lp.col_cost_.assign(model.c.begin(), model.c.end()); + lp.col_lower_ = lower_; + lp.col_upper_ = upper_; + lp.row_lower_.assign(model.b.begin(), model.b.end()); + lp.row_upper_.assign(m, kHighsInf); + lp.a_matrix_.format_ = MatrixFormat::kRowwise; + lp.a_matrix_.num_col_ = lp.num_col_; + lp.a_matrix_.num_row_ = lp.num_row_; + // Copy CSR directly: O(rows+nnz), never a rows*columns scan. + // Replace HiGHS' initial zero offset instead of appending another one. + lp.a_matrix_.start_.assign(model.row_start.begin(),model.row_start.end()); + lp.a_matrix_.index_.assign(model.column.begin(),model.column.end()); + lp.a_matrix_.value_.assign(model.a.begin(),model.a.end()); + // integrality_ remains empty: HiGHS only solves continuous LPs. + require_ok(highs_.passModel(std::move(lp)), "model construction"); + } + + LpWorkspace(const LpWorkspace&) = delete; + LpWorkspace& operator=(const LpWorkspace&) = delete; + + BoundResult bound(const std::vector& lower, + const std::vector& upper, + bool retain_certificate=false,bool retain_primal=false) { + std::lock_guard lock(mutex_); + BoundResult result; + const std::size_t n = model.c.size(), m = model.b.size(); + if ((lower.size() != n) || (upper.size() != n)) { + ++stats_.rejected; + return result; + } + for (std::size_t j=0; joriginal_upper_[j] || lower[j]>upper[j]) { + ++stats_.rejected; + return result; + } + lower_[j] = static_cast(lower[j]); + upper_[j] = static_cast(upper[j]); + } + + std::vector duals(m, 0.0); + if ((n != 0) && (m != 0) && !model.a.empty()) { + using Clock = std::chrono::steady_clock; + const auto start = Clock::now(); + // HiGHS accumulates run time across reoptimizations. Add this + // call's allowance to the already consumed run time. + HighsStatus status = highs_.setOptionValue( + "time_limit", highs_.getRunTime()+0.2); + bool ran = false; + if (status == HighsStatus::kOk) + status = highs_.changeColsBounds( + 0, static_cast(n)-1, lower_.data(), upper_.data()); + if (status == HighsStatus::kOk) { + ++stats_.lp_calls; + ran = true; + status = highs_.run(); + } + stats_.lp_ms += std::chrono::duration( + Clock::now()-start).count(); + if (ran && (highs_.getModelStatus() == HighsModelStatus::kInfeasible)) + ++stats_.infeasible_status; + const HighsSolution& solution = highs_.getSolution(); + if (retain_primal && ran && status != HighsStatus::kError && + solution.value_valid && solution.col_value.size()==n && + std::all_of(solution.col_value.begin(),solution.col_value.end(), + [](double value){return std::isfinite(value);})) + result.primal_suggestion=solution.col_value; + if (!ran || (status == HighsStatus::kError) || !solution.dual_valid || + (solution.row_dual.size() != m)) { + ++stats_.rejected; + return result; + } + for (double dual : solution.row_dual) + if (!std::isfinite(dual)) { + ++stats_.rejected; + return result; + } + duals = solution.row_dual; + const HighsInfo& info = highs_.getInfo(); + if (info.valid && std::isfinite(info.objective_function_value)) + result.lp_objective = info.objective_function_value; + } + + if (retain_certificate) { + auto certificate=std::make_shared(); + result.valid=LpCertificate::prepare(model.matrix(),model.b,model.c,duals,*certificate) && + (binary_ ? certificate->lower_bound(lower,upper,result.lower_bound) + : certificate->lower_bound_integer(lower,upper,result.lower_bound)); + if (result.valid) + result.certificate=std::move(certificate); + } else { + // Preserve the original bound-only path unless explicitly requested. + result.valid = binary_ ? LpCertificate::lower_bound( + model.matrix(), model.b, model.c, lower, upper, duals, result.lower_bound) + : LpCertificate::integer_lower_bound( + model.matrix(), model.b, model.c, lower, upper, duals, result.lower_bound); + } + if (result.valid) + ++stats_.valid_bounds; + else + ++stats_.rejected; + return result; + } + + void record_filtering(std::uint64_t conditional_checks, + std::uint64_t variable_fixings) { + std::lock_guard lock(mutex_); + ++stats_.certificate_evaluations; + stats_.conditional_checks+=conditional_checks; + stats_.variable_fixings+=variable_fixings; + } + + void record_integer_filtering(std::uint64_t checks,std::uint64_t tightened,std::uint64_t fixed) { + std::lock_guard lock(mutex_); + ++stats_.certificate_evaluations; + stats_.conditional_checks+=checks; + stats_.variable_bound_tightenings+=tightened; + stats_.variable_fixings+=fixed; + } + + Stats statistics() const { + std::lock_guard lock(mutex_); + return stats_; + } + }; + + } // namespace Detail + + /// Existing strict binary workspace; public sparse model remains immutable. + class SparseBackend { + public: + const SparseLinearModel model; + private: + Detail::LpWorkspace workspace_; + public: + explicit SparseBackend(SparseLinearModel input) + : model(std::move(input)),workspace_(model, + std::vector(model.c.size(),0),std::vector(model.c.size(),1),true) {} + virtual ~SparseBackend() = default; + SparseBackend(const SparseBackend&) = delete; + SparseBackend& operator=(const SparseBackend&) = delete; + BoundResult bound(const std::vector& lower,const std::vector& upper, + bool retain_certificate=false) { + return workspace_.bound(lower,upper,retain_certificate); + } + BoundResult bound(const std::vector& lower,const std::vector& upper, + bool retain_certificate,bool retain_primal) { + return workspace_.bound(lower,upper,retain_certificate,retain_primal); + } + void record_filtering(std::uint64_t checks,std::uint64_t fixings) {workspace_.record_filtering(checks,fixings);} + Stats statistics() const {return workspace_.statistics();} + }; + + /** Explicit integer backend, deliberately unrelated to SparseBackend so an + * integer model cannot accidentally enter binary posting through an upcast. + * Each bound call must stay within the immutable original integer domains. + */ + class BoundedIntegerBackend { + public: + const BoundedIntegerModel model; + private: + static const SparseLinearModel& checked(const BoundedIntegerModel& input) { + validate_integer_model(input);return input.linear; + } + Detail::LpWorkspace workspace_; + public: + explicit BoundedIntegerBackend(BoundedIntegerModel input) + : model(std::move(input)),workspace_(checked(model),model.lower,model.upper,false) {} + BoundedIntegerBackend(const BoundedIntegerBackend&) = delete; + BoundedIntegerBackend& operator=(const BoundedIntegerBackend&) = delete; + BoundResult bound(const std::vector& lower,const std::vector& upper, + bool retain_certificate=false) { + return workspace_.bound(lower,upper,retain_certificate); + } + BoundResult bound(const std::vector& lower,const std::vector& upper, + bool retain_certificate,bool retain_primal) { + return workspace_.bound(lower,upper,retain_certificate,retain_primal); + } + void record_filtering(std::uint64_t checks,std::uint64_t tightened,std::uint64_t fixings) { + workspace_.record_integer_filtering(checks,tightened,fixings); + } + Stats statistics() const {return workspace_.statistics();} + }; + + /** Dense-compatible adapter. The public immutable dense model is preserved. + * Its current constructor input is copied into CSR once; repeated bounds and + * propagation use the base's immutable sparse model. Use SparseBackend to + * avoid retaining dense storage altogether. + */ + class Backend : public SparseBackend { + public: + const LinearModel model; + explicit Backend(LinearModel input) + : SparseBackend(sparse_model(input)), model(std::move(input)) {} + Backend(const Backend&) = delete; + Backend& operator=(const Backend&) = delete; + }; + +}}} + +#endif diff --git a/gecode/minimodel/lp-certificate.hpp b/gecode/minimodel/lp-certificate.hpp new file mode 100644 index 0000000000..b162ce86d9 --- /dev/null +++ b/gecode/minimodel/lp-certificate.hpp @@ -0,0 +1,405 @@ +/* Certified lower bounds for integer linear minimization. + * + * Experimental, opt-in support for LP-guided Gecode propagation. + */ + +#ifndef __GECODE_MINIMODEL_LP_CERTIFICATE_HPP__ +#define __GECODE_MINIMODEL_LP_CERTIFICATE_HPP__ + +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Experimental { namespace LpCertificate { + + /// Denominator used for the nonnegative rational multipliers. + constexpr std::int64_t scale = 1048576; // 2^20 + +#if defined(__SIZEOF_INT128__) && (defined(__GNUC__) || defined(__clang__)) + constexpr bool supported = true; +#else + constexpr bool supported = false; +#endif + + /** Borrowed CSR view; arrays are only read during a checker call. + * Row offsets include the terminal nnz offset. Each row has strictly + * increasing column indices and nonzero values (no duplicate ambiguity). + */ + struct SparseMatrixView { + const std::vector& row_start; + const std::vector& column; + const std::vector& value; + std::size_t columns; + }; + + inline bool valid_sparse(const SparseMatrixView& matrix, std::size_t rows, + std::size_t columns) { + if (matrix.columns!=columns || rows==std::numeric_limits::max() || + matrix.row_start.size()!=rows+1 || matrix.column.size()!=matrix.value.size() || + matrix.row_start.front()!=0 || matrix.row_start.back()!=matrix.value.size()) + return false; + for (std::size_t i=0; ilast || last>matrix.value.size()) return false; + for (std::size_t k=first; k=columns || matrix.value[k]==0 || + (k!=first && matrix.column[k]<=matrix.column[k-1])) return false; + } + return true; + } + + /// Deterministic work counts; no clocks or backend timings are involved. + struct PreparationStats { + std::size_t rows_visited=0; + std::size_t nonzero_products=0; + std::size_t residuals_initialized=0; + }; + + /** + * Convert finite candidate multipliers to nonnegative rationals q/scale. + * + * Negative multipliers are clipped to zero. Every resulting q is valid + * for the certificate, even if the LP solver's multiplier was inaccurate. + * No proximity to an optimal dual solution is assumed. On failure, the + * output vector is unchanged. + */ + inline bool + quantize(const std::vector& duals, + std::vector& result) { + std::vector candidate; + candidate.reserve(duals.size()); + for (double dual : duals) { + if (!std::isfinite(dual)) + return false; + if (dual <= 0.0) { + candidate.push_back(0); + continue; + } + const double scaled = std::ldexp(dual,20); + // Comparing against 2^63 avoids rounding INT64_MAX up to 2^63. + if (!std::isfinite(scaled) || (scaled >= std::ldexp(1.0,63))) + return false; + candidate.push_back(static_cast(std::floor(scaled))); + } + result.swap(candidate); + return true; + } + + /// Exact integer interval cuts, all derived from the same original box. + struct IntegerFilterResult { + std::int64_t lower_bound=0; + std::vector lower,upper; + bool infeasible=false; + }; + + /** + * An exact affine lower bound valid for every integer box of one model. + * + * Its constant is q*b and its residual coefficients are scale*c-A^T*q. + * A prepared certificate is independent of the box used to obtain its + * candidate LP multipliers, so descendants and siblings may evaluate it + * safely with their own bounds. No floating-point reduced cost is used. + */ + class Certificate { +#if defined(__SIZEOF_INT128__) && (defined(__GNUC__) || defined(__clang__)) + using Wide = __int128; + Wide constant_ = 0; + std::vector residual_; + bool valid_ = false; + + bool numerator(const std::vector& lower, + const std::vector& upper, + Wide& out, bool binary=true) const { + if (!valid_ || lower.size()!=residual_.size() || + upper.size()!=residual_.size()) + return false; + Wide value=constant_; + for (std::size_t j=0; jupper[j] || (binary && (lower[j]<0 || upper[j]>1))) + return false; + const std::int64_t endpoint=residual_[j]>=0 ? lower[j] : upper[j]; + Wide term; + if (__builtin_mul_overflow(residual_[j],static_cast(endpoint),&term) || + __builtin_add_overflow(value,term,&value)) + return false; + } + out=value; + return true; + } + + static bool ceiling(Wide numerator, std::int64_t& out) { + Wide value=numerator/static_cast(scale); + if (numerator % static_cast(scale)>0) + if (__builtin_add_overflow(value,static_cast(1),&value)) + return false; + if (value(std::numeric_limits::min()) || + value>static_cast(std::numeric_limits::max())) + return false; + out=static_cast(value); + return true; + } +#endif + friend bool prepare(const SparseMatrixView&, + const std::vector&, + const std::vector&, + const std::vector&, Certificate&, PreparationStats*); + + public: + /// Evaluate the legacy binary-box integer bound; failure leaves out intact. + bool lower_bound(const std::vector& lower, + const std::vector& upper, + std::int64_t& out) const { +#if defined(__SIZEOF_INT128__) && (defined(__GNUC__) || defined(__clang__)) + Wide value; + return numerator(lower,upper,value) && ceiling(value,out); +#else + (void) lower; (void) upper; (void) out; + return false; +#endif + } + + /// Explicit bounded-integer evaluation; the legacy lower_bound stays binary. + bool lower_bound_integer(const std::vector& lower, + const std::vector& upper, + std::int64_t& out) const { +#if defined(__SIZEOF_INT128__) && (defined(__GNUC__) || defined(__clang__)) + Wide value; + return numerator(lower,upper,value,false) && ceiling(value,out); +#else + (void) lower; (void) upper; (void) out; + return false; +#endif + } + + /** Intersect a finite integer box with exact residual interval cuts. + * For each j, remove its original box-minimum contribution, then solve + * residual[j]*x[j] <= scale*objective_upper - remainder with directed + * integer division. Every cut uses the unchanged original box. All + * arithmetic and quotient narrowing are checked; false leaves out intact. + * An infeasible result has no meaningful tightened-domain interpretation. + */ + bool filter_integer(const std::vector& lower, + const std::vector& upper, + std::int64_t objective_upper, IntegerFilterResult& out) const { +#if defined(__SIZEOF_INT128__) && (defined(__GNUC__) || defined(__clang__)) + Wide base,threshold; + IntegerFilterResult candidate; + if (!numerator(lower,upper,base,false) || !ceiling(base,candidate.lower_bound) || + __builtin_mul_overflow(static_cast(scale), + static_cast(objective_upper),&threshold)) return false; + candidate.lower=lower; candidate.upper=upper; + candidate.infeasible=base>threshold; + for (std::size_t j=0; !candidate.infeasible && j0 ? lower[j] : upper[j]; + if (__builtin_mul_overflow(residual,static_cast(endpoint),&minimum) || + __builtin_sub_overflow(base,minimum,&remainder) || + __builtin_sub_overflow(threshold,remainder,&right)) return false; + // Signed minimum / -1 is the only nonzero-divisor division overflow. + if (right==std::numeric_limits::min() && residual==-1) return false; + Wide quotient=right/residual; + const Wide fraction=right%residual; + if (residual>0) { + if (fraction<0 && __builtin_sub_overflow(quotient,static_cast(1),"ient)) return false; + if (quotient(lower[j])) candidate.infeasible=true; + else if (quotient(upper[j])) candidate.upper[j]=static_cast(quotient); + } else { + if (fraction<0 && __builtin_add_overflow(quotient,static_cast(1),"ient)) return false; + if (quotient>static_cast(upper[j])) candidate.infeasible=true; + else if (quotient>static_cast(lower[j])) candidate.lower[j]=static_cast(quotient); + } + } + out=std::move(candidate); + return true; +#else + (void) lower; (void) upper; (void) objective_upper; (void) out; + return false; +#endif + } + + /** + * Evaluate the bound and both conditional bounds for each unfixed bit. + * + * forbidden[j]&1 proves x[j]=0 cannot have cost<=objective_upper; + * forbidden[j]&2 proves the same for x[j]=1. Assigned variables have + * mask zero. All arithmetic, including the conditional difference, is + * checked. Failure leaves both outputs unchanged. + */ + bool filter(const std::vector& lower, + const std::vector& upper, + std::int64_t objective_upper, std::int64_t& bound, + std::vector& forbidden) const { +#if defined(__SIZEOF_INT128__) && (defined(__GNUC__) || defined(__clang__)) + Wide base,threshold; + std::int64_t rounded; + if (!numerator(lower,upper,base) || !ceiling(base,rounded) || + __builtin_mul_overflow(static_cast(scale), + static_cast(objective_upper),&threshold)) + return false; + std::vector candidate(residual_.size(),0); + for (std::size_t j=0; jthreshold) + candidate[j]|=1; + Wide with_one; + if (__builtin_add_overflow(without,residual_[j],&with_one)) + return false; + if (with_one>threshold) + candidate[j]|=2; + } + bound=rounded; + forbidden.swap(candidate); + return true; +#else + (void) lower; (void) upper; (void) objective_upper; + (void) bound; (void) forbidden; + return false; +#endif + } + }; + + /** Prepare an immutable affine bound from canonical sparse rows. + * Validation is O(rows+nnz), arithmetic is O(rows+columns+active-dual nnz). + * No rows*columns allocation or scan occurs. Failure preserves both outputs. + */ + inline bool + prepare(const SparseMatrixView& A, + const std::vector& b, + const std::vector& c, + const std::vector& duals, Certificate& out, + PreparationStats* work=nullptr) { +#if defined(__SIZEOF_INT128__) && (defined(__GNUC__) || defined(__clang__)) + using Wide = __int128; + const std::size_t rows=b.size(),columns=c.size(); + if (duals.size()!=rows || !valid_sparse(A,rows,columns)) return false; + std::vector q; + if (!quantize(duals,q)) return false; + Certificate candidate; + PreparationStats measured; + candidate.residual_.resize(columns); + for (std::size_t j=0; j(scale),static_cast(c[j]), + &candidate.residual_[j])) return false; + } + for (std::size_t i=0; i(q[i]),static_cast(b[i]),&product) || + __builtin_add_overflow(candidate.constant_,product,&candidate.constant_)) return false; + for (std::size_t k=A.row_start[i]; k(q[i]),static_cast(A.value[k]),&product) || + __builtin_sub_overflow(candidate.residual_[j],product,&candidate.residual_[j])) return false; + } + } + candidate.valid_=true; + out=std::move(candidate); + if (work) *work=measured; + return true; +#else + (void) A; (void) b; (void) c; (void) duals; (void) out; (void) work; + return false; +#endif + } + + /** Legacy dense row-major adapter; existing source calls remain valid. + * Dense input is scanned once to create canonical sparse rows. Callers that + * retain sparse storage should use the sparse overload directly. + */ + inline bool + prepare(const std::vector& A, + const std::vector& b, + const std::vector& c, + const std::vector& duals, Certificate& out) { + if (!supported) return false; + const auto rows=b.size(),columns=c.size(); + if (rows==std::numeric_limits::max() || duals.size()!=rows || + (rows && columns>std::numeric_limits::max()/rows) || + A.size()!=rows*columns) return false; + std::vector start,indices; + std::vector values; + start.reserve(rows+1); start.push_back(0); + for (std::size_t i=0; i=b, over a nonempty binary box using weak duality: + * + * c*x >= (q*b + sum_j min((scale*c-A^T*q)[j]*lower[j], + * (scale*c-A^T*q)[j]*upper[j])) / scale. + * + * Any nonnegative quantized q is valid. Checked signed 128-bit arithmetic + * and mathematical ceiling are the only source of pruning bounds. Floating + * LP objectives and infeasibility claims never certify a bound. False means + * no bound was produced and leaves out intact; it does not mean infeasible. + */ + inline bool + lower_bound(const SparseMatrixView& A, + const std::vector& b, + const std::vector& c, + const std::vector& lower, + const std::vector& upper, + const std::vector& duals, + std::int64_t& out) { + Certificate certificate; + return prepare(A,b,c,duals,certificate) && certificate.lower_bound(lower,upper,out); + } + + /// Legacy dense adapter; identical binary box and failure semantics. + inline bool + lower_bound(const std::vector& A, + const std::vector& b, + const std::vector& c, + const std::vector& lower, + const std::vector& upper, + const std::vector& duals, + std::int64_t& out) { + Certificate certificate; + return prepare(A,b,c,duals,certificate) && certificate.lower_bound(lower,upper,out); + } + + /// Finite integer boxes with integer objective coefficients; never continuous. + inline bool + integer_lower_bound(const SparseMatrixView& A, + const std::vector& b, + const std::vector& c, + const std::vector& lower, + const std::vector& upper, + const std::vector& duals, std::int64_t& out) { + Certificate certificate; + return prepare(A,b,c,duals,certificate) && certificate.lower_bound_integer(lower,upper,out); + } + inline bool + integer_lower_bound(const std::vector& A, + const std::vector& b, + const std::vector& c, + const std::vector& lower, + const std::vector& upper, + const std::vector& duals, std::int64_t& out) { + Certificate certificate; + return prepare(A,b,c,duals,certificate) && certificate.lower_bound_integer(lower,upper,out); + } + +}}} + +#endif diff --git a/gecode/minimodel/lp-cut-loop.hpp b/gecode/minimodel/lp-cut-loop.hpp new file mode 100644 index 0000000000..99bf89aef8 --- /dev/null +++ b/gecode/minimodel/lp-cut-loop.hpp @@ -0,0 +1,315 @@ +/* Explicit root LP / verified global-cover loop. SPDX-License-Identifier: MIT */ +#ifndef GECODE_MINIMODEL_LP_CUT_LOOP_HPP +#define GECODE_MINIMODEL_LP_CUT_LOOP_HPP + +#include +#include +#include + +namespace Gecode { namespace Experimental { namespace LpRelaxation { namespace Cuts { + +#ifdef GECODE_LP_CUT_LOOP_TEST_HOOKS +void root_cut_loop_test_event(const char* event); +#endif + +enum class RootLoopCompletion { + NoNewCuts, RoundLimit, WorkLimit, StorageLimit, SeparationLimit, + Cancelled, TimeLimit, NoPrimalSuggestion, InvalidSuggestion, + CallbackError, BackendError, AllocationFailure +}; + +struct RootLoopOptions { + std::size_t max_rounds=4,max_work=4000000; + std::size_t max_columns=200000,max_rows=200000,max_nonzeros=2000000; + PoolLimits pool; + SeparationOptions separation; + // A bounded power of two keeps native endpoints * denominator exact in double. + Integer denominator=1048576; + std::optional deadline; + std::function stop_requested; + void validate() const { + if (denominator<=0 || denominator>1048576 || (denominator&(denominator-1))) + throw std::invalid_argument("root cover denominator must be a power of two in [1,1048576]"); + } +}; + +struct RootLoopStatistics { + std::size_t work=0,rounds=0,augmentations=0,projected_coordinates=0; + std::size_t separated_cuts=0,duplicate_cuts=0,unsupported_rows=0,oversized_rows=0; + std::size_t arithmetic_rejections=0; + std::uint64_t lp_calls=0,valid_bounds=0,rejected_bounds=0,floating_infeasible_reports=0; + double lp_seconds=0.0; +}; + +/** Exact bound evidence retains its particular matrix and all appended-row + * attribution. Every cuts[i] is global and belongs to the original source; + * matrix row original_rows+i is the negation of cuts[i].inequality(). + * It does not certify integer feasibility or floating LP optimality. + */ +struct RootBoundEvidence { + SourceModel model; + std::vector cuts; + BoundResult bound; +}; + +struct RootLoopResult { + SourceModel original; + SourceModel model; + std::vector cuts; + // Null until construction has completed. Its model is an immutable copy of model. + std::shared_ptr backend; + std::optional best_bound; + RootLoopCompletion completion=RootLoopCompletion::RoundLimit; + RootLoopStatistics stats; + explicit RootLoopResult(SourceModel source):original(source),model(std::move(source)) {} +}; + +struct RationalSelection { + FractionalPoint point; + std::size_t projected_coordinates=0; +}; + +/** This projects a finite numerical suggestion into the original box, then + * rounds to the nearest grid point, halfway away from zero. It is solely a cut + * selection heuristic: neither a feasible witness nor a primal bound. + */ +inline RationalSelection rational_selection_point(const SourceModel& source, + const std::vector& values,Integer denominator=1048576) { + RootLoopOptions options;options.denominator=denominator;options.validate(); + const auto& model=source.model(); + if (values.size()!=model.lower.size()) + throw std::invalid_argument("root cover suggestion dimensions"); + RationalSelection result;result.point.denominator=denominator; + result.point.numerator.reserve(values.size()); + for (std::size_t j=0;j(model.lower[j]), + std::min(static_cast(model.upper[j]),values[j])); + if (projected!=values[j]) ++result.projected_coordinates; + // Source validation and the denominator cap bound these exact integer + // endpoints below 2^51. Scaling by a power of two introduces no error. + const auto lower=Detail::times_positive(model.lower[j],denominator); + const auto upper=Detail::times_positive(model.upper[j],denominator); + const double rounded=std::round(projected*static_cast(denominator)); + if (!std::isfinite(rounded) || rounded(lower) || rounded>static_cast(upper)) + throw std::invalid_argument("root cover rational projection is outside the original box"); + result.point.numerator.push_back(static_cast(rounded)); + } + return result; +} + +namespace LoopDetail { +struct Stop {RootLoopCompletion reason;}; +inline void event(const char* name) { +#ifdef GECODE_LP_CUT_LOOP_TEST_HOOKS + root_cut_loop_test_event(name); +#else + (void)name; +#endif +} +class Budget { + const RootLoopOptions& options_; + RootLoopStatistics& stats_; +public: + Budget(const RootLoopOptions& options,RootLoopStatistics& stats):options_(options),stats_(stats) {} + void checkpoint() const { + bool cancelled=false; + if (options_.stop_requested) { + try {cancelled=options_.stop_requested();} + catch (...) {throw Stop{RootLoopCompletion::CallbackError};} + } + if (cancelled) throw Stop{RootLoopCompletion::Cancelled}; + if (options_.deadline && std::chrono::steady_clock::now()>=*options_.deadline) + throw Stop{RootLoopCompletion::TimeLimit}; + } + std::size_t remaining() const {return options_.max_work-stats_.work;} + void charge(std::size_t units) { + checkpoint(); + if (units>remaining()) throw Stop{RootLoopCompletion::WorkLimit}; + stats_.work+=units; + } +}; +inline void shape(const BoundedIntegerModel& model,const RootLoopOptions& options) { + const auto limit=static_cast(std::numeric_limits::max()); + if (model.linear.c.size()>std::min(options.max_columns,limit) || + model.linear.b.size()>std::min(options.max_rows,limit) || + model.linear.a.size()>std::min(options.max_nonzeros,limit)) + throw Stop{RootLoopCompletion::StorageLimit}; +} +// A structural work reservation for each full validation or model copy. Copies, +// CSR validation and native activity checks are O(columns+rows+nonzeros). +inline void model_work(const BoundedIntegerModel& model,Budget& budget) { + for (unsigned i=0;i<3;++i) budget.charge(model.linear.c.size()); + for (unsigned i=0;i<2;++i) budget.charge(model.linear.b.size()); + for (unsigned i=0;i<2;++i) budget.charge(model.linear.a.size()); + budget.charge(1); +} +inline void records_work(const std::vector& cuts,Budget& budget) { + for (const auto& cut:cuts) {budget.charge(1);budget.charge(cut.inequality().column.size());} +} +inline BoundedIntegerModel append(const SourceModel& original,const std::vector& cuts, + const RootLoopOptions& options,Budget& budget) { + const auto& base=original.model(); + const auto row_limit=std::min({options.max_rows, + static_cast(std::numeric_limits::max()), + static_cast(Int::Limits::max)}); + const auto term_limit=std::min(options.max_nonzeros, + static_cast(std::numeric_limits::max())); + if (cuts.size()>row_limit-base.linear.b.size()) throw Stop{RootLoopCompletion::StorageLimit}; + std::size_t terms=base.linear.a.size(); + for (const auto& record:cuts) { + budget.charge(1); + if (!record.source().same_identity(original) || record.proof().scope.kind!=ScopeKind::Global) + throw std::invalid_argument("root cover append requires verified global original-source records"); + const auto n=record.inequality().column.size(); + if (n>term_limit-terms) throw Stop{RootLoopCompletion::StorageLimit}; + terms+=n; + } + model_work(base,budget); + BoundedIntegerModel result=base; + result.linear.a.reserve(terms);result.linear.column.reserve(terms); + result.linear.b.reserve(base.linear.b.size()+cuts.size()); + result.linear.row_start.reserve(base.linear.b.size()+cuts.size()+1); + for (const auto& record:cuts) { + budget.charge(1);const auto& cut=record.inequality(); + for (std::size_t k=0;k(result.model.model()); + budget.checkpoint();result.backend=std::move(initial); + for (std::size_t round=0;roundstatistics(); + ++result.stats.rounds; + LoopDetail::event("before_bound"); + BoundResult bound; + try {bound=result.backend->bound(source.model().lower,source.model().upper,true,true);} + catch (...) {LoopDetail::observed(before,result.backend->statistics(),result.stats);throw;} + LoopDetail::observed(before,result.backend->statistics(),result.stats); + LoopDetail::event("after_bound"); + budget.checkpoint(); + if (bound.valid && (!result.best_bound || bound.lower_bound>result.best_bound->bound.lower_bound)) { + LoopDetail::records_work(result.cuts,budget); + BoundResult retained;retained.valid=true;retained.lower_bound=bound.lower_bound; + retained.lp_objective=bound.lp_objective;retained.certificate=bound.certificate; + RootBoundEvidence evidence{result.model,result.cuts,std::move(retained)}; + LoopDetail::event("bound_publication"); + budget.checkpoint();result.best_bound=std::move(evidence); + } + if (!bound.primal_suggestion) throw LoopDetail::Stop{RootLoopCompletion::NoPrimalSuggestion}; + budget.charge(source.model().lower.size()); + RationalSelection selection; + try {selection=rational_selection_point(source,*bound.primal_suggestion,options.denominator);} + catch (const std::invalid_argument&) {throw LoopDetail::Stop{RootLoopCompletion::InvalidSuggestion};} + result.stats.projected_coordinates+=selection.projected_coordinates; + budget.checkpoint(); + auto separation=options.separation; + separation.max_work=std::min(separation.max_work,budget.remaining()); + auto separated=separate_covers(source,selection.point,Scope::global(),separation); + // The separator has already consumed these units, even if a stop occurs + // immediately afterward. Its own cap guarantees this addition is safe. + result.stats.work+=separated.stats.work; + result.stats.separated_cuts+=separated.cuts.size(); + result.stats.duplicate_cuts+=separated.stats.duplicate_cuts; + result.stats.unsupported_rows+=separated.stats.unsupported_rows; + result.stats.oversized_rows+=separated.stats.oversized_rows; + result.stats.arithmetic_rejections+=separated.stats.arithmetic_rejections; + budget.checkpoint(); + LoopDetail::records_work(pool.records(),budget); + CutPool staged_pool=pool; + bool storage=false; + for (const auto& cut:separated.cuts) { + // Pool canonical comparisons and copies are linear in its bounded + // records/terms; reserve that work before each transactional insertion. + LoopDetail::records_work(staged_pool.records(),budget); + budget.charge(1);budget.charge(cut.inequality().column.size()); + const auto inserted=staged_pool.insert(cut); + if (inserted==InsertStatus::Capacity) {storage=true;break;} + if (inserted==InsertStatus::Duplicate) ++result.stats.duplicate_cuts; + if (inserted==InsertStatus::ForeignSource || inserted==InsertStatus::Replaced) + throw std::runtime_error("root cover pool violated original/global append invariants"); + } + if (staged_pool.records().size()==pool.records().size()) { + if (storage) throw LoopDetail::Stop{RootLoopCompletion::StorageLimit}; + if (separated.stats.work_limit) throw LoopDetail::Stop{RootLoopCompletion::WorkLimit}; + if (separated.stats.row_limit || separated.stats.cut_limit) + throw LoopDetail::Stop{RootLoopCompletion::SeparationLimit}; + budget.checkpoint(); + result.completion=RootLoopCompletion::NoNewCuts;return result; + } + LoopDetail::event("augmentation_build"); + auto augmented=LoopDetail::append(source,staged_pool.records(),options,budget); + LoopDetail::model_work(augmented,budget); + SourceModel staged_model(std::move(augmented)); + LoopDetail::model_work(staged_model.model(),budget); + LoopDetail::model_work(staged_model.model(),budget); + LoopDetail::event("augmented_backend"); + auto staged_backend=std::make_shared(staged_model.model()); + LoopDetail::records_work(staged_pool.records(),budget); + auto staged_cuts=staged_pool.records(); + LoopDetail::event("augmentation_publication"); + budget.checkpoint(); + // All operations after this publication gate move/swap already owned data. + result.model=std::move(staged_model);result.backend=std::move(staged_backend); + result.cuts.swap(staged_cuts);pool=std::move(staged_pool);++result.stats.augmentations; + LoopDetail::event("after_augmentation_publication"); + if (storage) throw LoopDetail::Stop{RootLoopCompletion::StorageLimit}; + if (separated.stats.work_limit) throw LoopDetail::Stop{RootLoopCompletion::WorkLimit}; + } + budget.checkpoint(); + result.completion=RootLoopCompletion::RoundLimit; + } catch (const LoopDetail::Stop& stop) { + result.completion=stop.reason; + } catch (const std::bad_alloc&) { + result.completion=RootLoopCompletion::AllocationFailure; + } catch (const std::exception&) { + result.completion=RootLoopCompletion::BackendError; + } catch (...) { + result.completion=RootLoopCompletion::BackendError; + } + return result; +} + +}}}} +#endif diff --git a/gecode/minimodel/lp-cuts.hpp b/gecode/minimodel/lp-cuts.hpp new file mode 100644 index 0000000000..4a7be5f439 --- /dev/null +++ b/gecode/minimodel/lp-cuts.hpp @@ -0,0 +1,422 @@ +/* Exact scoped cover-cut records and bounded deterministic separation. + * Experimental, opt-in; no native or numerical backend integration. + * SPDX-License-Identifier: MIT + */ +#ifndef GECODE_MINIMODEL_LP_CUTS_HPP +#define GECODE_MINIMODEL_LP_CUTS_HPP + +#include +#include +#include + +namespace Gecode { namespace Experimental { namespace LpRelaxation { namespace Cuts { + +using Integer=std::int64_t; + +/// Copies share one immutable identity; rebuilding an equal model creates another. +class SourceModel { + std::shared_ptr data_; + static std::shared_ptr snapshot(BoundedIntegerModel input) { + validate_integer_model(input); + return std::make_shared(std::move(input)); + } +public: + explicit SourceModel(BoundedIntegerModel input):data_(snapshot(std::move(input))) {} + const BoundedIntegerModel& model() const { + if (!data_) throw std::invalid_argument("cut source is moved from"); + return *data_; + } + bool same_identity(const SourceModel& other) const noexcept { + return data_ && data_==other.data_; + } +}; + +struct Box { + std::vector lower,upper; +}; +enum class ScopeKind { Global, Local }; +struct Scope { + ScopeKind kind=ScopeKind::Global; + Box box; + static Scope global() {return {};} + static Scope local(Box value) {return {ScopeKind::Local,std::move(value)};} +}; + +/// Untrusted proof request. The checker derives weights/signs from the source row. +struct CoverProof { + std::size_t row=0; + std::vector columns; + Scope scope; +}; +/// Canonical original-variable inequality sum(coefficient[k]*x[column[k]])<=upper. +struct SparseCut { + std::vector column; + std::vector coefficient; + Integer upper=0; +}; + +namespace Detail { +struct LimitReached {}; +struct Meter { + std::size_t used=0,limit=std::numeric_limits::max(); + void tick() { + if (used==limit) throw LimitReached{}; + ++used; + } +}; +// A fixed merge schedule makes work-limit prefixes independent of std::sort's +// implementation. Both comparisons and output entries consume work units. +template +inline void sort(std::vector& values,Less less,Meter& work) { + const auto n=values.size(); + if (n<2) return; + std::vector scratch(n); + for (std::size_t width=1;widthn/2) break; + width*=2; + } +} +// Reuse the established strengthening arithmetic and literal representation. +using Term=Strengthening::Detail::Term; +using PackingRow=Strengthening::Detail::PackingRow; +inline Integer add(Integer a,Integer b) { + Integer out; + if (!Strengthening::Detail::add(a,b,out)) throw std::overflow_error("cover addition overflow"); + return out; +} +inline Integer subtract(Integer a,Integer b) { + Integer out; + if (!Strengthening::Detail::subtract(a,b,out)) throw std::overflow_error("cover subtraction overflow"); + return out; +} +inline Integer times_positive(Integer value,Integer positive) { + if (positive<=0) throw std::invalid_argument("cover multiplier must be positive"); + if ((value>0 && value>std::numeric_limits::max()/positive) || + (value<0 && value::min()/positive)) + throw std::overflow_error("cover multiplication overflow"); + return value*positive; +} +inline bool valid_box(const BoundedIntegerModel& model,const Box& box,Meter& work) { + const auto n=model.lower.size(); + if (box.lower.size()!=n || box.upper.size()!=n) return false; + for (std::size_t j=0;jmodel.upper[j] || box.lower[j]>box.upper[j]) return false; + } + return true; +} +struct Bounds { + const std::vector& lower; + const std::vector& upper; +}; +inline Bounds bounds(const BoundedIntegerModel& model,const Scope& scope,Meter& work) { + if (scope.kind==ScopeKind::Global) { + if (!scope.box.lower.empty() || !scope.box.upper.empty()) + throw std::invalid_argument("global cover proof cannot carry local bounds"); + return {model.lower,model.upper}; + } + if (scope.kind!=ScopeKind::Local || !valid_box(model,scope.box,work)) + throw std::invalid_argument("local cover scope must be a nonempty box inside the source domains"); + return {scope.box.lower,scope.box.upper}; +} +inline bool contains(const Box& outer,const Box& inner) { + for (std::size_t j=0;jouter.upper[j]) return false; + return true; +} +inline bool covers_scope(const Scope& outer,const Scope& inner) { + if (outer.kind==ScopeKind::Global) return true; + if (inner.kind==ScopeKind::Global) return false; + return contains(outer.box,inner.box); +} +struct UnsupportedRow : std::invalid_argument { + UnsupportedRow():std::invalid_argument("cover row has a free nonbinary variable in its certified scope") {} +}; +inline PackingRow packing(const BoundedIntegerModel& model,std::size_t row, + const Bounds& box,Meter& work) { + const auto& matrix=model.linear; + if (row>=matrix.b.size()) throw std::invalid_argument("cover proof source row is out of range"); + PackingRow result; + result.capacity=subtract(0,matrix.b[row]); + for (auto k=matrix.row_start[row];k0?a:subtract(0,a); + if (box.lower[j]==box.upper[j]) { + Integer fixed=times_positive(box.lower[j],weight); + if (a<0) fixed=subtract(0,fixed); + result.capacity=add(result.capacity,fixed); + continue; + } + if (box.lower[j]<0 || box.upper[j]>1) throw UnsupportedRow{}; + const bool complement=a>0; + result.terms.push_back({2*j+(complement?1U:0U),weight}); + if (complement) result.capacity=add(result.capacity,weight); + } + return result; +} +struct Verifier; +inline bool same_row(const SparseCut& a,const SparseCut& b) { + return a.upper==b.upper && a.column==b.column && a.coefficient==b.coefficient; +} +inline bool same_row(const SparseCut& a,const SparseCut& b,Meter& work) { + work.tick(); + if (a.upper!=b.upper || a.column.size()!=b.column.size()) return false; + for (std::size_t i=0;irow.terms.size()) + throw std::invalid_argument("cover proof selects too many free source terms"); + auto columns=claim.columns; + Detail::sort(columns,[](std::size_t a,std::size_t b){return a(columns.size())-1; + Integer weight=0;std::size_t position=0; + for (std::size_t i=0;i cuts_; + std::size_t nonzeros_=0,scope_values_=0; + static bool accumulate(std::size_t value,std::size_t limit,std::size_t& total) { + if (value>limit-total) return false; + total+=value;return true; + } +public: + explicit CutPool(SourceModel source,PoolLimits limits={}) + : source_(std::move(source)),limits_(limits) {(void)source_.model();} + const std::vector& records() const {return cuts_;} + std::size_t nonzeros() const {return nonzeros_;} + std::size_t scope_values() const {return scope_values_;} + InsertStatus insert(const VerifiedCut& candidate) { + if (!source_.same_identity(candidate.source())) return InsertStatus::ForeignSource; + std::vector remove(cuts_.size(),0);std::size_t removed=0; + for (std::size_t i=0;i next; + for (std::size_t i=0;i applicable(const Box& box) const { + std::vector result; + for (const auto& cut:cuts_) if (cut.applies_to(source_,box)) result.push_back(cut); + return result; + } +}; + +/// Exact rational selection point. It is not proof of a cut's validity. +struct FractionalPoint { + std::vector numerator; + Integer denominator=1048576; +}; +struct SeparationOptions { + std::size_t max_work=100000,max_rows=256,max_terms_per_row=512; + std::size_t max_cuts=32,max_cuts_per_row=4,max_starts_per_row=4; +}; +struct SeparationStats { + std::size_t work=0,rows=0,unsupported_rows=0,oversized_rows=0; + std::size_t arithmetic_rejections=0,duplicate_cuts=0; + bool work_limit=false,row_limit=false,cut_limit=false; +}; +struct SeparationResult { + std::vector cuts; + SeparationStats stats; +}; + +namespace Detail { +inline void point_in_box(const FractionalPoint& point,const Bounds& box,Meter& work) { + if (point.denominator<=0 || point.numerator.size()!=box.lower.size()) + throw std::invalid_argument("cover selection point requires matching coordinates and a positive denominator"); + for (std::size_t j=0;jupper) + throw std::invalid_argument("cover selection point is outside its certified scope box"); + } +} +inline Integer literal_value(const Term& term,const FractionalPoint& point) { + return term.literal&1U ? subtract(point.denominator,point.numerator[term.literal/2]) + : point.numerator[term.literal/2]; +} +inline bool violated(const std::vector& cover,const FractionalPoint& point,Meter& work) { + Integer activity=0; + for (const auto& term:cover) {work.tick();activity=add(activity,literal_value(term,point));} + return activity>times_positive(static_cast(cover.size())-1,point.denominator); +} +} + +/** Deterministic greedy covers with exact proof reconstruction and point checks. + * Reuses the established strengthening rule: grow a cover, remove any light + * term while its remainder still exceeds capacity, then emit cardinality <=k-1. + * Work caps can miss cuts; an empty result makes no separation-completeness claim. + */ +inline SeparationResult separate_covers(const SourceModel& source,const FractionalPoint& point, + const Scope& scope=Scope::global(),const SeparationOptions& options={}) { + SeparationResult result;Detail::Meter work{0,options.max_work}; + if (!options.max_cuts) {result.stats.cut_limit=true;return result;} + try { + const auto& model=source.model();const auto& matrix=model.linear; + const auto box=Detail::bounds(model,scope,work); + Detail::point_in_box(point,box,work); + for (std::size_t i=0;ioptions.max_terms_per_row) { + ++result.stats.oversized_rows;continue; + } + Detail::PackingRow row; + try {row=Detail::packing(model,i,box,work);} + catch (const Detail::UnsupportedRow&) {++result.stats.unsupported_rows;continue;} + catch (const std::overflow_error&) {++result.stats.arithmetic_rejections;continue;} + const auto before=result.cuts.size(); + const auto append=[&](const std::vector& cover) { + if (!Detail::violated(cover,point,work)) return; + CoverProof claim;claim.row=i;claim.scope=scope; + for (const auto& term:cover) {work.tick();claim.columns.push_back(term.literal/2);} + auto cut=Detail::Verifier::make(source,claim,work); + for (const auto& prior:result.cuts) { + if (Detail::same_row(prior.inequality(),cut.inequality(),work)) {++result.stats.duplicate_cuts;return;} + } + result.cuts.push_back(std::move(cut)); + }; + if (!options.max_cuts_per_row) continue; + try { + if (row.capacity<0) append({}); + else for (unsigned policy=0;policy<3 && result.cuts.size()bv; + } + if (policy!=2 && a.weight!=b.weight) return a.weight>b.weight; + return a.literal cover; + for (std::size_t k=0;k minimal; + for (const auto& term:cover) { + work.tick();const auto remaining=Detail::subtract(weight,term.weight); + if (remaining>row.capacity) weight=remaining;else minimal.push_back(term); + } + append(minimal); + } + } + } catch (const std::overflow_error&) {++result.stats.arithmetic_rejections;} + if (result.cuts.size()==options.max_cuts) {result.stats.cut_limit=true;break;} + } + } catch (const Detail::LimitReached&) {result.stats.work_limit=true;} + result.stats.work=work.used; + return result; +} + +}}}} +#endif diff --git a/gecode/minimodel/lp-model.hpp b/gecode/minimodel/lp-model.hpp new file mode 100644 index 0000000000..ebaef7e2eb --- /dev/null +++ b/gecode/minimodel/lp-model.hpp @@ -0,0 +1,202 @@ +/* Experimental binary and bounded-integer linear models. SPDX-License-Identifier: MIT */ +#ifndef GECODE_MINIMODEL_LP_MODEL_HPP +#define GECODE_MINIMODEL_LP_MODEL_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Experimental { namespace LpRelaxation { + +/// Binary minimization: min c*x, subject to a*x >= b. Dense row-major a. +/// Kept unchanged for source compatibility, including mutable public data. +struct LinearModel { + std::vector a, b, c; +}; + +/** Binary minimization in canonical compressed sparse row (CSR) storage. + * a[k] belongs to column[k]; row i occupies [row_start[i],row_start[i+1]). + * Each row's columns must be strictly increasing and values must be nonzero. + * The default value describes the empty model. No dense shadow is retained. + */ +struct SparseLinearModel { + std::vector row_start{0}, column; + std::vector a, b, c; + + LpCertificate::SparseMatrixView matrix() const { + return {row_start,column,a,c.size()}; + } + std::size_t nonzeros() const { return a.size(); } +}; + +/// Convert the current dense value, never a cached copy of mutable input. +inline SparseLinearModel sparse_model(const LinearModel& model) { + const auto n=model.c.size(),m=model.b.size(); + if (m==std::numeric_limits::max() || + (n && m>std::numeric_limits::max()/n) || model.a.size()!=m*n) + throw std::invalid_argument("Binary linear model dimensions"); + SparseLinearModel result; + result.b=model.b; result.c=model.c; + result.row_start.reserve(m+1); + for (std::size_t i=0;i::value,int>::type=0> +inline void validate_model(const Model& model) { + const auto n=model.c.size(),m=model.b.size(); + if (n>static_cast(std::numeric_limits::max()) || + m>static_cast(std::numeric_limits::max()) || + !LpCertificate::valid_sparse(model.matrix(),m,n)) + throw std::invalid_argument("Binary sparse model needs canonical CSR dimensions/indices"); + for (const auto* values : {&model.a,&model.b,&model.c}) + for (auto value : *values) + if (value < -1000000000LL || value > 1000000000LL) + throw std::invalid_argument("Binary linear coefficients must have magnitude <= 1e9"); + // n<=INT_MAX and abs(c)<=1e9 keep both sums within signed int64. + std::int64_t lo=0,hi=0; + for (auto value : model.c) { if (value<0) lo+=value; else hi+=value; } + if (loInt::Limits::max) + throw std::invalid_argument("Binary objective range exceeds Gecode integer limits"); +} + +inline void validate_model(const LinearModel& model) { + const auto n=model.c.size(), m=model.b.size(); + if (n>static_cast(std::numeric_limits::max()) || + m>static_cast(std::numeric_limits::max()) || + (n && m>std::numeric_limits::max()/n) || model.a.size()!=m*n) + throw std::invalid_argument("Binary linear model dimensions"); + for (const auto* values : {&model.a,&model.b,&model.c}) + for (auto value : *values) + if (value < -1000000000LL || value > 1000000000LL) + throw std::invalid_argument("Binary linear coefficients must have magnitude <= 1e9"); + std::int64_t lo=0,hi=0; + for (auto value : model.c) { if (value<0) lo+=value; else hi+=value; } + if (loInt::Limits::max) + throw std::invalid_argument("Binary objective range exceeds Gecode integer limits"); +} + +/// Post original sparse constraints without allocating or scanning dense rows. +template::value,int>::type=0> +inline void post_native(Home home,const IntVarArgs& x,IntVar objective, + const Model& model) { + validate_model(model); + if (model.c.size()!=static_cast(x.size())) + throw Int::ArgumentSizeMismatch("LpRelaxation::post_native"); + if (home.failed()) return; + dom(home,x,0,1); + for (std::size_t i=0;i(model.a[k]); + variables << x[static_cast(model.column[k])]; + } + linear(home,coefficients,variables,IRT_GQ,static_cast(model.b[i]),IPL_BND); + } + IntArgs costs(x.size()); + for (int j=0;j(model.c[j]); + linear(home,costs,x,IRT_EQ,objective,IPL_BND); +} + +/// Legacy dense posting reads the current input and converts exactly once. +inline void post_native(Home home,const IntVarArgs& x,IntVar objective, + const LinearModel& model) { + post_native(home,x,objective,sparse_model(model)); +} + +/** Explicit finite integer domains over a sparse linear model. No objective + * offset or fractional coefficient/lattice is implied by this integer API. + */ +struct BoundedIntegerModel { + SparseLinearModel linear; + std::vector lower,upper; +}; + +namespace Detail { +inline std::pair validate_integer_native(const BoundedIntegerModel& model) { + const auto& linear=model.linear; + const auto n=linear.c.size(),m=linear.b.size(); + if (n>static_cast(Int::Limits::max) || + m>static_cast(Int::Limits::max) || + model.lower.size()!=n || model.upper.size()!=n || + !LpCertificate::valid_sparse(linear.matrix(),m,n)) + throw std::invalid_argument("Bounded integer model needs matching domains and canonical CSR"); + for (const auto* values : {&linear.a,&linear.b,&linear.c}) + for (auto value : *values) + if (value < -1000000000LL || value > 1000000000LL) + throw std::invalid_argument("Integer linear coefficients must have magnitude <= 1e9"); + for (std::size_t j=0;jInt::Limits::max || + model.lower[j]>model.upper[j]) + throw std::invalid_argument("Integer domains must be nonempty and within native Gecode limits"); + // Each product fits int64 before comparison (1e9 * native_max). The sum + // never crosses its checked native limit. This excludes floating-point + // fallback propagation and protects every signed native row activity. + const auto accumulate=[&](std::int64_t coefficient,std::size_t j,std::int64_t limit, + std::int64_t& magnitude,std::int64_t& lo,std::int64_t& hi) { + const auto a=coefficient*model.lower[j],b=coefficient*model.upper[j]; + const auto absolute=std::max(std::abs(a),std::abs(b)); + if (absolute>limit-magnitude) + throw std::invalid_argument("Integer linear activity exceeds exact native limits; tighten domains or rescale with exact integers"); + magnitude+=absolute; lo+=std::min(a,b); hi+=std::max(a,b); + }; + for (std::size_t i=0;i(lo),static_cast(hi)}; +} +} + +inline void validate_integer_model(const BoundedIntegerModel& model) { + (void) Detail::validate_integer_native(model); +} + +/// Preserve original sparse integer rows/domains and the objective equality. +inline void post_native_integer(Home home,const IntVarArgs& x,IntVar objective, + const BoundedIntegerModel& model) { + const auto objective_range=Detail::validate_integer_native(model); + if (model.linear.c.size()!=static_cast(x.size())) + throw Int::ArgumentSizeMismatch("LpRelaxation::post_native_integer"); + if (home.failed()) return; + for (int j=0;j(model.lower[j]),static_cast(model.upper[j])); + dom(home,objective,objective_range.first,objective_range.second); + const auto& linear_model=model.linear; + for (std::size_t i=0;i(linear_model.a[k]); + variables << x[static_cast(linear_model.column[k])]; + } + linear(home,coefficients,variables,IRT_GQ,static_cast(linear_model.b[i]),IPL_BND); + } + IntArgs costs(x.size()); + for (int j=0;j(linear_model.c[j]); + linear(home,costs,x,IRT_EQ,objective,IPL_BND); +} + +}}} +#endif diff --git a/gecode/minimodel/lp-relaxation.hpp b/gecode/minimodel/lp-relaxation.hpp new file mode 100644 index 0000000000..7acdaa414d --- /dev/null +++ b/gecode/minimodel/lp-relaxation.hpp @@ -0,0 +1,321 @@ +/* Experimental HiGHS-backed LP bound propagator. SPDX-License-Identifier: MIT */ +#ifndef GECODE_MINIMODEL_LP_RELAXATION_HPP +#define GECODE_MINIMODEL_LP_RELAXATION_HPP + +#include +#include + +namespace Gecode { namespace Experimental { namespace LpRelaxation { + +enum class Frequency { Root, EveryNode }; + +struct Options { + Frequency frequency=Frequency::EveryNode; + bool reduced_cost_fixing=false; + /// Reoptimize after this many additional binary assignments (root always). + unsigned int assignment_interval=1; +}; + +/** + * Redundant LP lower-bound propagator for binary linear optimization. + * + * The shared backend serializes access to a warm-started continuous LP. Each + * call resets every column bound to the current space. Only an exact checked + * dual certificate may tighten the objective. Infeasible floating-point LP + * statuses alone never fail a space. The native constraints remain responsible + * for feasibility and the equality between the objective and c*x. + */ +class BoundPropagator : public Propagator { + ViewArray x; + Int::IntView objective; + std::shared_ptr backend; + std::shared_ptr certificate; + Options options; + int last_assigned; + bool has_bound; + std::int64_t bound_value; + +public: + BoundPropagator(Home home,const IntVarArgs& variables,IntVar cost, + const std::shared_ptr& engine,const Options& policy) + : Propagator(home),x(home,variables),objective(cost),backend(engine), + options(policy),last_assigned(-1),has_bound(false),bound_value(0) { + home.notice(*this,AP_DISPOSE); + x.subscribe(home,*this,Int::PC_INT_BND); + objective.subscribe(home,*this,Int::PC_INT_BND); + } + BoundPropagator(Home home,const IntVarArgs& variables,IntVar cost, + const std::shared_ptr& engine,Frequency f) + : BoundPropagator(home,variables,cost,engine,Options{f,false,1}) {} + BoundPropagator(Space& home,BoundPropagator& other) + : Propagator(home,other),backend(other.backend),certificate(other.certificate), + options(other.options), + last_assigned(other.last_assigned),has_bound(other.has_bound),bound_value(other.bound_value) { + x.update(home,other.x); + objective.update(home,other.objective); + } + Actor* copy(Space& home) override { return new(home) BoundPropagator(home,*this); } + PropCost cost(const Space&,const ModEventDelta&) const override { + // Finish cheaper native deductions before invoking numerical optimization. + return PropCost::crazy(PropCost::HI,x.size()); + } + void reschedule(Space& home) override { + x.reschedule(home,*this,Int::PC_INT_BND); + objective.reschedule(home,*this,Int::PC_INT_BND); + } + size_t dispose(Space& home) override { + home.ignore(*this,AP_DISPOSE); + if (!home.failed()) { + x.cancel(home,*this,Int::PC_INT_BND); + objective.cancel(home,*this,Int::PC_INT_BND); + } + certificate.~shared_ptr(); + backend.~shared_ptr(); + (void) Propagator::dispose(home); + return sizeof(*this); + } + ExecStatus propagate(Space& home,const ModEventDelta&) override { + int assigned=0; + for (int j=0;j(assigned-last_assigned)>=options.assignment_interval); + std::vector lower,upper; + if (solve_lp || certificate) { + lower.resize(x.size()); upper.resize(x.size()); + for (int j=0;j1; + BoundResult result=backend->bound(lower,upper,retain); + last_assigned=assigned; + if (result.certificate) + certificate=std::move(result.certificate); + if (result.valid && (!has_bound || result.lower_bound>bound_value)) { + bound_value=result.lower_bound; + has_bound=true; + } + } + + bool fixed=false; + if (certificate) { + std::int64_t evaluated; + if (options.reduced_cost_fixing) { + std::vector forbidden; + if (certificate->filter(lower,upper,objective.max(),evaluated,forbidden)) { + const std::uint64_t checks=2*static_cast(x.size()-assigned); + std::uint64_t fixings=0; + if (!has_bound || evaluated>bound_value) { + has_bound=true; bound_value=evaluated; + } + if (bound_value>objective.max()) { + backend->record_filtering(checks,fixings); + return ES_FAILED; + } + // Every forbidden bit was proved against the original box, so + // all these deductions are valid together. Native propagators + // run again before the next LP call after any successful fixing. + for (int j=0; jrecord_filtering(checks,fixings); + return ES_FAILED; + } + const ModEvent event=x[j].eq(home,forbidden[j]==1 ? 1 : 0); + if (me_failed(event)) { + backend->record_filtering(checks,fixings); + return ES_FAILED; + } + if (me_modified(event)) { ++fixings; fixed=true; } + } + backend->record_filtering(checks,fixings); + } + } else if (certificate->lower_bound(lower,upper,evaluated) && + (!has_bound || evaluated>bound_value)) { + // Throttling LP calls does not discard cheap deductions from the + // last exact certificate as this space's box becomes smaller. + has_bound=true; bound_value=evaluated; + } + } + // In a binary space every domain change increases the assigned count. + // An objective-only event can reuse its cached certified ancestor bound. + if (has_bound) { + if (bound_value>objective.max()) return ES_FAILED; + if (bound_value>objective.min()) + GECODE_ME_CHECK(objective.gq(home,static_cast(bound_value))); + } + // Legacy root-only bounding is one-shot. Root-only fixing keeps its + // certificate for future objective cuts, but never solves another LP. + if (options.frequency==Frequency::Root && + (!options.reduced_cost_fixing || !certificate)) + return home.ES_SUBSUMED(*this); + return fixed ? ES_NOFIX : ES_FIX; + } +}; + +/// Post all native rows/objective equality plus the optional LP bound actor. +template::value,int>::type=0> +inline void binary_linear_minimize(Home home,const IntVarArgs& x,IntVar objective, + const std::shared_ptr& backend, + const Options& options) { + if (!backend) throw std::invalid_argument("LP backend is null"); + if (!options.assignment_interval) + throw std::invalid_argument("LP assignment interval must be positive"); + const std::shared_ptr engine=backend; + post_native(home,x,objective,engine->model); + if (!home.failed()) + (void) new(home) BoundPropagator(home,x,objective,engine,options); +} + +template::value,int>::type=0> +inline void binary_linear_minimize(Home home,const IntVarArgs& x,IntVar objective, + const std::shared_ptr& backend, + Frequency frequency=Frequency::EveryNode) { + binary_linear_minimize(home,x,objective,backend,Options{frequency,false,1}); +} + +// Keep the original posting signatures for source/function-pointer compatibility. +inline void binary_linear_minimize(Home home,const IntVarArgs& x,IntVar objective, + const std::shared_ptr& backend, + const Options& options) { + binary_linear_minimize(home,x,objective, + std::shared_ptr(backend),options); +} +inline void binary_linear_minimize(Home home,const IntVarArgs& x,IntVar objective, + const std::shared_ptr& backend, + Frequency frequency=Frequency::EveryNode) { + binary_linear_minimize(home,x,objective,backend,Options{frequency,false,1}); +} + +/// Explicit integer policy; no binary-assignment scheduling is inferred. +struct IntegerOptions { + Frequency frequency=Frequency::EveryNode; + bool bound_tightening=false; + /// Reoptimize after this many observed variable interval changes (root always). + unsigned int bound_change_interval=1; +}; + +namespace Detail { +class IntegerBoundPropagator : public Propagator { + ViewArray x; + Int::IntView objective; + std::shared_ptr backend; + std::shared_ptr certificate; + std::vector last_lower,last_upper; + IntegerOptions options; + std::uint64_t pending_changes=0; + bool attempted=false,has_bound=false; + std::int64_t bound_value=0; +public: + IntegerBoundPropagator(Home home,const IntVarArgs& variables,IntVar cost, + const std::shared_ptr& engine,const IntegerOptions& policy) + : Propagator(home),x(home,variables),objective(cost),backend(engine),options(policy) { + home.notice(*this,AP_DISPOSE); + x.subscribe(home,*this,Int::PC_INT_BND); + objective.subscribe(home,*this,Int::PC_INT_BND); + } + IntegerBoundPropagator(Space& home,IntegerBoundPropagator& other) + : Propagator(home,other),backend(other.backend),certificate(other.certificate), + last_lower(other.last_lower),last_upper(other.last_upper),options(other.options), + pending_changes(other.pending_changes),attempted(other.attempted), + has_bound(other.has_bound),bound_value(other.bound_value) { + x.update(home,other.x);objective.update(home,other.objective); + } + Actor* copy(Space& home) override {return new(home) IntegerBoundPropagator(home,*this);} + PropCost cost(const Space&,const ModEventDelta&) const override { + return PropCost::crazy(PropCost::HI,x.size()); + } + void reschedule(Space& home) override { + x.reschedule(home,*this,Int::PC_INT_BND);objective.reschedule(home,*this,Int::PC_INT_BND); + } + std::size_t dispose(Space& home) override { + home.ignore(*this,AP_DISPOSE); + if (!home.failed()) { + x.cancel(home,*this,Int::PC_INT_BND);objective.cancel(home,*this,Int::PC_INT_BND); + } + last_lower.~vector();last_upper.~vector(); + certificate.~shared_ptr();backend.~shared_ptr(); + (void) Propagator::dispose(home);return sizeof(*this); + } + ExecStatus propagate(Space& home,const ModEventDelta&) override { + std::vector lower(x.size()),upper(x.size()); + int assigned=0;std::uint64_t changed=0; + for (int j=0;j(options.bound_change_interval,pending_changes+changed); + last_lower=lower;last_upper=upper; + const bool solve_lp=!attempted || (options.frequency==Frequency::EveryNode && + pending_changes>=options.bound_change_interval); + if (solve_lp) { + const bool retain=options.bound_tightening || options.bound_change_interval>1; + auto result=backend->bound(lower,upper,retain); + attempted=true;pending_changes=0; + if (result.certificate) certificate=std::move(result.certificate); + if (result.valid && (!has_bound || result.lower_bound>bound_value)) { + has_bound=true;bound_value=result.lower_bound; + } + } + bool modified=false; + if (certificate) { + if (options.bound_tightening) { + LpCertificate::IntegerFilterResult result; + if (certificate->filter_integer(lower,upper,objective.max(),result)) { + if (!has_bound || result.lower_bound>bound_value) {has_bound=true;bound_value=result.lower_bound;} + const auto checks=static_cast(x.size()-assigned); + std::uint64_t tightened=0,fixed=0; + if (result.infeasible || bound_value>objective.max()) { + backend->record_filtering(checks,tightened,fixed);return ES_FAILED; + } + // All intersections were certified against the same original box. + for (int j=0;j(result.lower[j])); + if (me_failed(a)) {backend->record_filtering(checks,tightened,fixed);return ES_FAILED;} + const auto b=x[j].lq(home,static_cast(result.upper[j])); + if (me_failed(b)) {backend->record_filtering(checks,tightened,fixed);return ES_FAILED;} + if (me_modified(a) || me_modified(b)) { + modified=true;++tightened; + if (!was_assigned && x[j].assigned()) ++fixed; + } + } + backend->record_filtering(checks,tightened,fixed); + } + } else { + std::int64_t evaluated; + if (certificate->lower_bound_integer(lower,upper,evaluated) && + (!has_bound || evaluated>bound_value)) {has_bound=true;bound_value=evaluated;} + } + } + if (has_bound) { + if (bound_value>objective.max()) return ES_FAILED; + if (bound_value>objective.min()) GECODE_ME_CHECK(objective.gq(home,static_cast(bound_value))); + } + if (options.frequency==Frequency::Root && (!options.bound_tightening || !certificate)) + return home.ES_SUBSUMED(*this); + return modified ? ES_NOFIX : ES_FIX; + } +}; +} + +inline void integer_linear_minimize(Home home,const IntVarArgs& x,IntVar objective, + const std::shared_ptr& backend, + const IntegerOptions& options={}) { + if (!backend) throw std::invalid_argument("Integer LP backend is null"); + if (!options.bound_change_interval || + (options.frequency!=Frequency::Root && options.frequency!=Frequency::EveryNode)) + throw std::invalid_argument("Invalid integer LP frequency or bound-change interval"); + post_native_integer(home,x,objective,backend->model); + if (!home.failed()) (void) new(home) Detail::IntegerBoundPropagator(home,x,objective,backend,options); +} + +}}} +#endif diff --git a/gecode/minimodel/lp-strengthening.hpp b/gecode/minimodel/lp-strengthening.hpp new file mode 100644 index 0000000000..6b4534c527 --- /dev/null +++ b/gecode/minimodel/lp-strengthening.hpp @@ -0,0 +1,435 @@ +/* Experimental binary linear presolve and redundant cuts. + * SPDX-License-Identifier: MIT + */ +#ifndef GECODE_MINIMODEL_LP_STRENGTHENING_HPP +#define GECODE_MINIMODEL_LP_STRENGTHENING_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Experimental { namespace LpRelaxation { +namespace Strengthening { + +struct Options { + bool gcd = true; + bool fixings = true; + bool pairs = true; + bool cliques = true; + bool covers = true; + std::size_t max_cuts = 256; + std::size_t max_graph_variables = 128; + std::size_t max_pair_checks = 200000; + std::size_t max_clique_seeds = 128; + std::size_t max_clique_size = 64; + std::size_t max_cover_cuts_per_row = 4; + std::size_t max_cover_terms = 512; + std::size_t max_cover_starts = 4; +}; + +struct Stats { + std::size_t rows_before = 0, rows_after = 0; + std::size_t gcd_rows = 0, duplicates_removed = 0; + std::size_t tautologies_removed = 0; + std::size_t pair_checks = 0, pair_conflicts = 0; + std::size_t fixing_cuts = 0, pair_cuts = 0; + std::size_t clique_cuts = 0, cover_cuts = 0; + std::size_t arithmetic_rejections = 0, cover_rows_skipped = 0; + bool graph_skipped = false, pair_limit_reached = false; + bool cut_limit_reached = false, infeasible = false; + std::size_t cuts_added(void) const { + return fixing_cuts + pair_cuts + clique_cuts + cover_cuts; + } +}; + +struct Result { + LinearModel model; + Stats stats; +}; + +namespace Detail { +using Integer = std::int64_t; +using Row = std::vector; + +inline bool add(Integer a, Integer b, Integer& out) { + if ((b > 0 && a > std::numeric_limits::max()-b) || + (b < 0 && a < std::numeric_limits::min()-b)) + return false; + out=a+b; + return true; +} +inline bool subtract(Integer a, Integer b, Integer& out) { + if ((b > 0 && a < std::numeric_limits::min()+b) || + (b < 0 && a > std::numeric_limits::max()+b)) + return false; + out=a-b; + return true; +} + +struct Term { + // 2*j means x[j]; 2*j+1 means 1-x[j]. + std::size_t literal; + Integer weight; +}; +struct PackingRow { + std::vector terms; + Integer capacity; +}; +enum class Kind { Original, Fixing, Pair, Clique, Cover }; + +class Builder { + const LinearModel& original; + const Options& options; + const std::size_t n; + std::map rows; + // Keep surviving originals in their input order and append generated cuts. + // Map iterators remain valid when new rows are inserted or bounds tightened. + std::vector::const_iterator> row_order; + Stats stats; + + void contradiction(void) { + row_order.clear(); + rows.clear(); + row_order.push_back(rows.emplace(Row(n,0),1).first); + stats.infeasible=true; + } + + // Insert an equivalent normalized original row or a proved redundant cut. + bool insert(Row row, Integer rhs, Kind kind) { + if (stats.infeasible) + return false; + Integer divisor=0, minimum=0, maximum=0; + for (Integer coefficient : row) { + // Input validation and generated +/-1 cuts exclude INT64_MIN here. + if (coefficient < -1000000000LL || coefficient > 1000000000LL) { + ++stats.arithmetic_rejections; + return false; + } + divisor=std::gcd(divisor,coefficient < 0 ? -coefficient : coefficient); + if (!add(minimum,std::min(0,coefficient),minimum) || + !add(maximum,std::max(0,coefficient),maximum)) { + ++stats.arithmetic_rejections; + return false; + } + } + if (rhs > maximum) { + contradiction(); + return false; + } + if (rhs <= minimum) { + if (kind==Kind::Original) + ++stats.tautologies_removed; + return false; + } + if (options.gcd && divisor > 1) { + for (Integer& coefficient : row) + coefficient/=divisor; + // Mathematical ceiling for a positive divisor, including negative rhs. + const Integer remainder=rhs%divisor; + rhs/=divisor; + if (remainder > 0 && !add(rhs,1,rhs)) { + ++stats.arithmetic_rejections; + return false; + } + if (kind==Kind::Original) + ++stats.gcd_rows; + } + if (rhs < -1000000000LL || rhs > 1000000000LL) { + ++stats.arithmetic_rejections; + return false; + } + auto found=rows.find(row); + if (found!=rows.end()) { + if (kind==Kind::Original) + ++stats.duplicates_removed; + if (found->second >= rhs) + return false; + } + if (kind!=Kind::Original && stats.cuts_added() >= options.max_cuts) { + stats.cut_limit_reached=true; + return false; + } + if (found==rows.end()) { + row_order.push_back(rows.emplace(std::move(row),rhs).first); + } else + found->second=rhs; + switch (kind) { + case Kind::Original: break; + case Kind::Fixing: ++stats.fixing_cuts; break; + case Kind::Pair: ++stats.pair_cuts; break; + case Kind::Clique: ++stats.clique_cuts; break; + case Kind::Cover: ++stats.cover_cuts; break; + } + return true; + } + + // Translate sum(literals)<=limit into a row in the original x variables. + bool cardinality(const std::vector& literals, + Integer limit, Kind kind) { + Row row(n,0); + Integer rhs; + if (!subtract(0,limit,rhs)) { + ++stats.arithmetic_rejections; + return false; + } + for (std::size_t literal : literals) { + const bool complement=(literal&1U)!=0; + if (!add(row[literal/2],complement ? 1 : -1,row[literal/2]) || + (complement && !add(rhs,1,rhs))) { + ++stats.arithmetic_rejections; + return false; + } + } + return insert(std::move(row),rhs,kind); + } + + Result finish(void) { + Result result; + result.model.c=original.c; + result.model.b.reserve(rows.size()); + for (const auto& position : row_order) { + const auto& entry=*position; + result.model.a.insert(result.model.a.end(),entry.first.begin(),entry.first.end()); + result.model.b.push_back(entry.second); + } + stats.rows_after=result.model.b.size(); + result.stats=stats; + return result; + } + +public: + Builder(const LinearModel& input,const Options& settings) + : original(input),options(settings),n(input.c.size()) { + validate_model(input); + stats.rows_before=input.b.size(); + } + + Result run(void) { + for (std::size_t i=0; i packing; + packing.reserve(rows.size()); + for (const auto& position : row_order) { + const auto& entry=*position; + PackingRow converted; + converted.capacity=0; + bool valid=true; + for (std::size_t j=0; j0; + const Integer weight=complement ? coefficient : -coefficient; + converted.terms.push_back({2*j+(complement ? 1 : 0),weight}); + if (complement && !add(converted.capacity,weight,converted.capacity)) + valid=false; + } + if (!subtract(converted.capacity,entry.second,converted.capacity)) + valid=false; + if (!valid) { + ++stats.arithmetic_rejections; + continue; + } + // Ax>=b is exactly sum |a[j]|*literal[j] <= sum(a[j]>0)a[j]-b. + if (converted.capacity<0) { + contradiction(); + return finish(); + } + packing.push_back(std::move(converted)); + } + + std::vector forced_zero(2*n,0); + if (options.fixings) { + for (const auto& row : packing) + for (const Term& term : row.terms) + if (term.weight>row.capacity) { + forced_zero[term.literal]=1; + if (forced_zero[term.literal^1U]) { + contradiction(); + return finish(); + } + cardinality({term.literal},0,Kind::Fixing); + } + } + if (stats.infeasible) + return finish(); + + const bool use_graph=(options.pairs || options.cliques) && + n<=options.max_graph_variables; + std::vector> conflicts; + if ((options.pairs || options.cliques) && !use_graph) + stats.graph_skipped=true; + if (use_graph) { + const std::size_t vertices=2*n; + if (vertices!=0 && vertices>std::numeric_limits::max()/vertices) + throw std::overflow_error("Binary conflict graph dimensions"); + std::vector adjacency(vertices*vertices,0); + std::vector degree(vertices,0); + auto edge=[&](std::size_t u,std::size_t v,bool derived) { + if (adjacency[u*vertices+v]) + return; + adjacency[u*vertices+v]=adjacency[v*vertices+u]=1; + ++degree[u]; ++degree[v]; + if (derived) { + conflicts.emplace_back(std::min(u,v),std::max(u,v)); + ++stats.pair_conflicts; + } + }; + for (std::size_t j=0; j= options.max_pair_checks) { + stats.pair_limit_reached=true; + stop_pairs=true; + break; + } + ++stats.pair_checks; + Integer weight; + if (!add(row.terms[i].weight,row.terms[j].weight,weight)) { + ++stats.arithmetic_rejections; + continue; + } + if (weight>row.capacity) + edge(row.terms[i].literal,row.terms[j].literal,true); + } + } + if (stop_pairs) break; + } + if (options.cliques && options.max_clique_size>=3) { + std::vector order(vertices); + std::iota(order.begin(),order.end(),0); + std::sort(order.begin(),order.end(),[&](std::size_t u,std::size_t v) { + return degree[u]!=degree[v] ? degree[u]>degree[v] : u clique{seed}; + for (std::size_t candidate : order) { + if (candidate==seed || forced_zero[candidate]) continue; + bool compatible=true; + for (std::size_t member : clique) + if (!adjacency[candidate*vertices+member]) { + compatible=false; break; + } + if (compatible) { + clique.push_back(candidate); + if (clique.size()>=options.max_clique_size) break; + } + } + if (clique.size()>=3) + cardinality(clique,1,Kind::Clique); + } + } + } + if (stats.infeasible) + return finish(); + + if (options.covers) { + for (const auto& row : packing) { + if (stats.infeasible) break; + if (row.terms.size()>options.max_cover_terms) { + ++stats.cover_rows_skipped; + continue; + } + std::vector terms; + for (const Term& term : row.terms) + if (!forced_zero[term.literal]) terms.push_back(term); + if (terms.empty() || + std::all_of(terms.begin(),terms.end(),[](const Term& t){return t.weight==1;})) + continue; // Unit-weight subset covers are already implied by the LP row. + const std::size_t before=stats.cover_cuts; + for (unsigned int policy=0; policy<3; ++policy) { + std::vector order=terms; + std::sort(order.begin(),order.end(),[&](const Term& u,const Term& v) { + if (policy!=2 && u.weight!=v.weight) + return policy==0 ? u.weight>v.weight : u.weight= options.max_cover_cuts_per_row) + break; + Integer weight=0; + bool valid=true; + std::vector cover; + for (std::size_t k=0; k literals; + for (const Term& term : cover) { + Integer without; + if (!subtract(weight,term.weight,without)) {valid=false; break;} + if (without>row.capacity) weight=without; + else literals.push_back(term.literal); + } + if (!valid) {++stats.arithmetic_rejections; continue;} + // The retained subset itself must still be a cover. + if (weight>row.capacity && !literals.empty()) + cardinality(literals,static_cast(literals.size())-1,Kind::Cover); + } + } + } + } + if (stats.infeasible) + return finish(); + if (options.pairs) + for (const auto& pair : conflicts) { + cardinality({pair.first,pair.second},1,Kind::Pair); + if (stats.infeasible) break; + } + return finish(); + } +}; +} // namespace Detail + +/** + * Return a binary-equivalent model with identical objective/variable mapping. + * + * GCD rounding is valid because each row activity is integral. Pair and clique + * cuts come only from proved literal conflicts; cover cuts come only from + * subsets whose nonnegative packing weights exceed the capacity. Complements + * are translated back to the original x variables. These cuts can remove + * fractional LP points, but do not remove any feasible binary assignment. + * + * All candidate arithmetic is checked. Original rows are never silently + * dropped on arithmetic failure. Limits can weaken the strengthening, but + * cannot change feasibility. max_cuts=0 retains only normalization/deduplication. + * A proved contradiction is returned as the row 0>=1. + */ +inline Result strengthen(const LinearModel& model,const Options& options=Options()) { + return Detail::Builder(model,options).run(); +} + +} // namespace Strengthening +}}} +#endif diff --git a/gecode/optimize.hh b/gecode/optimize.hh new file mode 100644 index 0000000000..a8ff351b24 --- /dev/null +++ b/gecode/optimize.hh @@ -0,0 +1,29 @@ +/* Additive numerical modeling, solving and workflows. Requires C++17. */ +#ifndef GECODE_OPTIMIZE_HH +#define GECODE_OPTIMIZE_HH + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#endif diff --git a/gecode/optimize/CMakeLists.txt b/gecode/optimize/CMakeLists.txt new file mode 100644 index 0000000000..7471c52914 --- /dev/null +++ b/gecode/optimize/CMakeLists.txt @@ -0,0 +1,498 @@ +cmake_minimum_required(VERSION 3.21) +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(GecodeOptimize VERSION 0.1.0 LANGUAGES C CXX) + include(CTest) + include(GNUInstallDirs) + set(GECODE_OPTIMIZE_STANDALONE ON) +endif() + +get_filename_component(GECODE_OPTIMIZE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE) +option(GECODE_OPTIMIZE_WITH_HIGHS "Enable numerical HiGHS LP/MILP adapter" ON) +set(_optimize_native_default OFF) +if(TARGET gecodeint AND TARGET gecodesearch) + set(_optimize_native_default ON) +endif() +option(GECODE_OPTIMIZE_WITH_NATIVE "Enable bounded integer Gecode bridge" ${_optimize_native_default}) +if(GECODE_OPTIMIZE_WITH_NATIVE AND GECODE_OPTIMIZE_STANDALONE) + message(FATAL_ERROR "Native optimization bridge requires a top-level Gecode build: configure the repository root with GECODE_ENABLE_OPTIMIZE=ON and enable int and search. Standalone builds support HiGHS or the backend-free core.") +endif() +if(GECODE_OPTIMIZE_WITH_NATIVE AND (NOT TARGET gecodeint OR NOT TARGET gecodesearch)) + message(FATAL_ERROR "Native optimization bridge requires both int and search components") +endif() +option(GECODE_OPTIMIZE_BUILD_TESTS "Build independent optimization conformance tests" ${BUILD_TESTING}) +option(GECODE_OPTIMIZE_TEST_PYTHON "Register Python binding tests (disable for sanitizer runtimes without an instrumented Python host)" ON) +set(GECODE_OPTIMIZE_HIGHS_SOURCE "" CACHE PATH "Optional local pinned HiGHS source (no download)") + +find_package(Threads REQUIRED) +if(GECODE_OPTIMIZE_WITH_HIGHS) + if(GECODE_OPTIMIZE_HIGHS_SOURCE) + # The source is provided explicitly; never fetch at configure/runtime. + set(_saved_build_testing "${BUILD_TESTING}") + set(BUILD_TESTING OFF) + set(BUILD_EXAMPLES OFF) + set(BUILD_CXX ON) + set(FAST_BUILD ON) + add_subdirectory("${GECODE_OPTIMIZE_HIGHS_SOURCE}" "${CMAKE_CURRENT_BINARY_DIR}/highs") + set(BUILD_TESTING "${_saved_build_testing}") + else() + find_package(highs 1.15 CONFIG REQUIRED) + endif() +endif() + +if(DEFINED GECODE_DEFAULT_LINK_VARIANT) + string(TOUPPER "${GECODE_DEFAULT_LINK_VARIANT}" _optimize_library_type) +elseif(BUILD_SHARED_LIBS) + set(_optimize_library_type SHARED) +else() + set(_optimize_library_type STATIC) +endif() +add_library(gecodeoptimize ${_optimize_library_type} model.cpp result.cpp validate.cpp constraints.cpp globals.cpp io.cpp solve.cpp lp_observations.cpp lp_basis.cpp lp_evidence.cpp lp_sensitivity.cpp lp_sensitivity_backend.cpp native.cpp native_presolve.cpp native_components.cpp native_symmetry.cpp diagnostics.cpp workflow.cpp scenarios.cpp relaxation.cpp pool.cpp presolve.cpp flatzinc.cpp quadratic.cpp quadratic_bound.cpp quadratic_solve.cpp) +add_library(Gecode::gecodeoptimize ALIAS gecodeoptimize) +add_library(Gecode::optimize ALIAS gecodeoptimize) +target_compile_features(gecodeoptimize PUBLIC cxx_std_17) +target_include_directories(gecodeoptimize PUBLIC + $ + $) +target_link_libraries(gecodeoptimize PUBLIC Threads::Threads) +if(GECODE_OPTIMIZE_WITH_HIGHS) + target_compile_definitions(gecodeoptimize PRIVATE GECODE_OPTIMIZE_WITH_HIGHS=1) + target_link_libraries(gecodeoptimize PRIVATE highs::highs) +endif() +if(GECODE_OPTIMIZE_WITH_NATIVE) + target_compile_definitions(gecodeoptimize PRIVATE GECODE_OPTIMIZE_WITH_NATIVE=1) + target_link_libraries(gecodeoptimize PRIVATE gecodeint gecodesearch) + # ELF shared C wrappers cannot embed non-PIC native archives. Keep the + # normal native-only build policy unchanged unless this component needs them. + if(UNIX AND NOT APPLE) + foreach(component support kernel search int) + if(TARGET gecode${component}_static) + set_property(TARGET gecode${component}_static PROPERTY POSITION_INDEPENDENT_CODE ON) + endif() + endforeach() + endif() +endif() +set_target_properties(gecodeoptimize PROPERTIES POSITION_INDEPENDENT_CODE ON WINDOWS_EXPORT_ALL_SYMBOLS ON) +# The C/Python boundary is always a shared library, including a static C++ build. +# Its explicit C exports are independent of internal C++ symbol visibility. +add_library(gecodeoptimize_c SHARED c_api.cpp) +add_library(Gecode::gecodeoptimize_c ALIAS gecodeoptimize_c) +add_library(Gecode::optimize_c ALIAS gecodeoptimize_c) +target_compile_features(gecodeoptimize_c PRIVATE cxx_std_17) +target_compile_definitions(gecodeoptimize_c PRIVATE GECODE_OPT_C_API_EXPORTS) +target_include_directories(gecodeoptimize_c PUBLIC + $ + $) +target_link_libraries(gecodeoptimize_c PRIVATE gecodeoptimize) +# Keep facade RTTI visible across shared-library boundaries: hiding ModelError's +# typeinfo in this wrapper breaks its exception classification on Apple libc++. +set_target_properties(gecodeoptimize_c PROPERTIES VERSION 1.0.0 SOVERSION 1) +if(APPLE) + set_target_properties(gecodeoptimize gecodeoptimize_c PROPERTIES INSTALL_RPATH "@loader_path") +elseif(UNIX) + set_target_properties(gecodeoptimize gecodeoptimize_c PROPERTIES INSTALL_RPATH "$ORIGIN") +endif() + +if(GECODE_OPTIMIZE_BUILD_TESTS) + enable_testing() + # Independent oracles retain their original exhaustive fixtures and do not + # depend on benchmark drivers, measurements or generated data. + function(gecode_optimize_add_oracle name source) + add_executable(optimize-${name}-test "${GECODE_OPTIMIZE_ROOT}/test/optimize/${source}") + target_compile_features(optimize-${name}-test PRIVATE cxx_std_17) + target_compile_definitions(optimize-${name}-test PRIVATE GECODE_NO_AUTOLINK) + target_include_directories(optimize-${name}-test PRIVATE "${CMAKE_BINARY_DIR}" "${GECODE_OPTIMIZE_ROOT}") + if(MSVC) + target_compile_options(optimize-${name}-test PRIVATE /UNDEBUG) + else() + target_compile_options(optimize-${name}-test PRIVATE -UNDEBUG) + endif() + add_test(NAME optimize-${name} COMMAND optimize-${name}-test) + set_tests_properties(optimize-${name} PROPERTIES TIMEOUT 60) + endfunction() + foreach(oracle certificate integer_certificate sparse_certificate) + string(REPLACE "_" "-" oracle_name "${oracle}") + gecode_optimize_add_oracle(lp-${oracle_name} lp_${oracle}.cpp) + endforeach() + foreach(test_name model bulk flatzinc quadratic quadratic_bound result validate constraints globals regular io solve lp_observations lp_observations_checks lp_basis lp_evidence lp_sensitivity session session_limits native native_auto native_race native_presolve native_components native_symmetry native_lp native_search native_knapsack native_branching native_starts native_neighborhoods diagnostics workflow scenarios relaxation pool presolve presolve_solve) + add_executable(optimize-${test_name}-test "${GECODE_OPTIMIZE_ROOT}/test/optimize/${test_name}.cpp") + target_link_libraries(optimize-${test_name}-test PRIVATE gecodeoptimize) + if(MSVC) + target_compile_options(optimize-${test_name}-test PRIVATE /UNDEBUG) + else() + target_compile_options(optimize-${test_name}-test PRIVATE -UNDEBUG) + endif() + add_test(NAME optimize-${test_name} COMMAND optimize-${test_name}-test) + set_tests_properties(optimize-${test_name} PROPERTIES TIMEOUT 60) + endforeach() + # Full-source Debug sanitizers make this nontrivial MIP lifetime fixture + # substantially slower. It is separate from the bounded fast regression gate. + set_tests_properties(optimize-session_limits PROPERTIES TIMEOUT 180) + if(GECODE_OPTIMIZE_WITH_HIGHS) + # Dirty the real backend before the separately compiled rejection seam. + # The production adapter and all ordinary solves contain no test selector. + add_library(optimize-lp-basis-failure-adapter OBJECT solve.cpp) + target_compile_features(optimize-lp-basis-failure-adapter PRIVATE cxx_std_17) + target_include_directories(optimize-lp-basis-failure-adapter PRIVATE "${GECODE_OPTIMIZE_ROOT}") + target_compile_definitions(optimize-lp-basis-failure-adapter PRIVATE + GECODE_OPTIMIZE_WITH_HIGHS=1 GECODE_OPTIMIZE_TEST_LP_BASIS_FAILURE=1) + target_link_libraries(optimize-lp-basis-failure-adapter PRIVATE highs::highs Threads::Threads) + add_executable(optimize-lp-basis-failure-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/lp_basis_failure.cpp" + model.cpp result.cpp validate.cpp constraints.cpp globals.cpp native.cpp native_presolve.cpp native_components.cpp native_symmetry.cpp presolve.cpp + lp_observations.cpp lp_basis.cpp $) + target_compile_features(optimize-lp-basis-failure-test PRIVATE cxx_std_17) + target_include_directories(optimize-lp-basis-failure-test PRIVATE "${GECODE_OPTIMIZE_ROOT}") + target_link_libraries(optimize-lp-basis-failure-test PRIVATE highs::highs Threads::Threads) + if(MSVC) + target_compile_options(optimize-lp-basis-failure-test PRIVATE /UNDEBUG) + else() + target_compile_options(optimize-lp-basis-failure-test PRIVATE -UNDEBUG) + endif() + add_test(NAME optimize-lp-basis-failure COMMAND optimize-lp-basis-failure-test) + set_tests_properties(optimize-lp-basis-failure PROPERTIES TIMEOUT 60) + endif() + # This executable owns its substitute backend and lifecycle hooks; neither + # they nor the experimental regularization override enter the public library. + add_executable(optimize-quadratic-coordinator-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/quadratic.cpp" + model.cpp result.cpp validate.cpp constraints.cpp globals.cpp + quadratic.cpp quadratic_bound.cpp quadratic_solve.cpp) + target_compile_features(optimize-quadratic-coordinator-test PRIVATE cxx_std_17) + target_include_directories(optimize-quadratic-coordinator-test PRIVATE "${GECODE_OPTIMIZE_ROOT}") + target_compile_definitions(optimize-quadratic-coordinator-test PRIVATE GECODE_QUADRATIC_TEST_HOOKS=1) + target_link_libraries(optimize-quadratic-coordinator-test PRIVATE Threads::Threads) + if(GECODE_OPTIMIZE_WITH_HIGHS) + target_compile_definitions(optimize-quadratic-coordinator-test PRIVATE GECODE_OPTIMIZE_WITH_HIGHS=1) + target_link_libraries(optimize-quadratic-coordinator-test PRIVATE highs::highs) + endif() + if(MSVC) + target_compile_options(optimize-quadratic-coordinator-test PRIVATE /UNDEBUG) + else() + target_compile_options(optimize-quadratic-coordinator-test PRIVATE -UNDEBUG) + endif() + add_test(NAME optimize-quadratic-coordinator COMMAND optimize-quadratic-coordinator-test) + set_tests_properties(optimize-quadratic optimize-quadratic-coordinator PROPERTIES TIMEOUT 180) + if(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$" AND NOT MSVC) + # Compile only the checker with unsafe arithmetic. Keep the harness and + # foundation normal so this verifies rejection, not a corrupted test oracle. + add_library(optimize-quadratic-fast-math OBJECT quadratic_bound.cpp) + target_compile_features(optimize-quadratic-fast-math PRIVATE cxx_std_17) + target_include_directories(optimize-quadratic-fast-math PRIVATE "${GECODE_OPTIMIZE_ROOT}") + target_compile_options(optimize-quadratic-fast-math PRIVATE -ffast-math) + add_executable(optimize-quadratic-arithmetic-rejection-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/quadratic_bound.cpp" + model.cpp result.cpp validate.cpp constraints.cpp globals.cpp quadratic.cpp + $) + target_compile_features(optimize-quadratic-arithmetic-rejection-test PRIVATE cxx_std_17) + target_include_directories(optimize-quadratic-arithmetic-rejection-test PRIVATE "${GECODE_OPTIMIZE_ROOT}") + target_compile_definitions(optimize-quadratic-arithmetic-rejection-test PRIVATE + GECODE_QUADRATIC_EXPECT_UNSUPPORTED_ARITHMETIC=1) + target_compile_options(optimize-quadratic-arithmetic-rejection-test PRIVATE -UNDEBUG) + target_link_libraries(optimize-quadratic-arithmetic-rejection-test PRIVATE Threads::Threads) + add_test(NAME optimize-quadratic-arithmetic-rejection COMMAND optimize-quadratic-arithmetic-rejection-test) + set_tests_properties(optimize-quadratic-arithmetic-rejection PROPERTIES TIMEOUT 60) + endif() + if(GECODE_OPTIMIZE_WITH_NATIVE) + target_compile_definitions(optimize-native-test PRIVATE GECODE_OPTIMIZE_WITH_NATIVE=1) + add_executable(optimize-brancher-lifecycle-test "${GECODE_OPTIMIZE_ROOT}/test/optimize/brancher_lifecycle.cpp") + target_link_libraries(optimize-brancher-lifecycle-test PRIVATE gecodeint gecodesearch) + add_test(NAME optimize-brancher-lifecycle COMMAND optimize-brancher-lifecycle-test) + set_tests_properties(optimize-brancher-lifecycle PROPERTIES TIMEOUT 60) + gecode_optimize_add_oracle(search-checkpoint search_checkpoint.cpp) + target_link_libraries(optimize-search-checkpoint-test PRIVATE gecodeint gecodesearch) + # These proof checkers use native configuration headers but no solver. + gecode_optimize_add_oracle(cut-proofs cuts.cpp) + gecode_optimize_add_oracle(lp-strengthening lp_strengthening.cpp) + if(GECODE_OPTIMIZE_WITH_HIGHS) + include(CheckCXXSourceCompiles) + check_cxx_source_compiles("#if !defined(__SIZEOF_INT128__) || !(defined(__GNUC__) || defined(__clang__)) +#error Checked wide integer certificates are unavailable +#endif +int main() { return 0; }" GECODE_OPTIMIZE_HAVE_CHECKED_WIDE) + if(GECODE_OPTIMIZE_HAVE_CHECKED_WIDE) + # Header-only coordinator: hooks exist only in this oracle executable. + gecode_optimize_add_oracle(cut-loop cut_loop.cpp) + target_compile_definitions(optimize-cut-loop-test PRIVATE + GECODE_LP_CUT_LOOP_TEST_NATIVE=1 GECODE_LP_CUT_LOOP_TEST_HOOKS=1) + target_link_libraries(optimize-cut-loop-test PRIVATE gecodeint gecodesearch highs::highs Threads::Threads) + set_tests_properties(optimize-cut-loop PROPERTIES TIMEOUT 120) + foreach(oracle backend integer_backend propagator integer_propagator sparse_storage reduced_cost) + string(REPLACE "_" "-" oracle_name "${oracle}") + gecode_optimize_add_oracle(lp-${oracle_name} lp_${oracle}.cpp) + target_link_libraries(optimize-lp-${oracle_name}-test PRIVATE gecodeint gecodesearch highs::highs Threads::Threads) + endforeach() + endif() + endif() + add_executable(optimize-native-search-coordinator-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/native_search.cpp" + model.cpp result.cpp validate.cpp constraints.cpp globals.cpp native.cpp native_presolve.cpp native_components.cpp native_symmetry.cpp presolve.cpp) + target_compile_features(optimize-native-search-coordinator-test PRIVATE cxx_std_17) + target_include_directories(optimize-native-search-coordinator-test PRIVATE "${GECODE_OPTIMIZE_ROOT}") + target_compile_definitions(optimize-native-search-coordinator-test PRIVATE + GECODE_NATIVE_SEARCH_TEST_HOOKS=1 GECODE_NATIVE_ROOT_CUT_TEST_HOOKS=1 GECODE_OPTIMIZE_WITH_NATIVE=1) + target_link_libraries(optimize-native-search-coordinator-test PRIVATE gecodeint gecodesearch Threads::Threads) + if(GECODE_OPTIMIZE_WITH_HIGHS) + target_compile_definitions(optimize-native-search-coordinator-test PRIVATE GECODE_OPTIMIZE_WITH_HIGHS=1) + target_link_libraries(optimize-native-search-coordinator-test PRIVATE highs::highs) + endif() + add_test(NAME optimize-native-search-coordinator COMMAND optimize-native-search-coordinator-test) + set_tests_properties(optimize-native-search-coordinator PROPERTIES TIMEOUT 120) + add_executable(optimize-native-lp-coordinator-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/native_lp.cpp" + model.cpp result.cpp validate.cpp constraints.cpp globals.cpp native.cpp native_presolve.cpp native_components.cpp native_symmetry.cpp presolve.cpp) + target_compile_features(optimize-native-lp-coordinator-test PRIVATE cxx_std_17) + target_include_directories(optimize-native-lp-coordinator-test PRIVATE "${GECODE_OPTIMIZE_ROOT}") + target_compile_definitions(optimize-native-lp-coordinator-test PRIVATE + GECODE_NATIVE_ROOT_CUT_TEST_HOOKS=1 GECODE_OPTIMIZE_WITH_NATIVE=1) + target_link_libraries(optimize-native-lp-coordinator-test PRIVATE gecodeint gecodesearch Threads::Threads) + if(GECODE_OPTIMIZE_WITH_HIGHS) + target_compile_definitions(optimize-native-lp-coordinator-test PRIVATE GECODE_OPTIMIZE_WITH_HIGHS=1) + target_link_libraries(optimize-native-lp-coordinator-test PRIVATE highs::highs) + endif() + add_test(NAME optimize-native-lp-coordinator COMMAND optimize-native-lp-coordinator-test) + set_tests_properties(optimize-native-lp-coordinator PROPERTIES TIMEOUT 120) + add_executable(optimize-native-branching-coordinator-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/native_branching.cpp" + model.cpp result.cpp validate.cpp constraints.cpp globals.cpp native.cpp native_presolve.cpp native_components.cpp native_symmetry.cpp presolve.cpp) + target_compile_features(optimize-native-branching-coordinator-test PRIVATE cxx_std_17) + target_include_directories(optimize-native-branching-coordinator-test PRIVATE "${GECODE_OPTIMIZE_ROOT}") + target_compile_definitions(optimize-native-branching-coordinator-test PRIVATE + GECODE_NATIVE_BRANCHING_TEST_HOOKS=1 GECODE_NATIVE_SEARCH_TEST_HOOKS=1 GECODE_OPTIMIZE_WITH_NATIVE=1) + target_link_libraries(optimize-native-branching-coordinator-test PRIVATE gecodeint gecodesearch Threads::Threads) + if(GECODE_OPTIMIZE_WITH_HIGHS) + target_compile_definitions(optimize-native-branching-coordinator-test PRIVATE GECODE_OPTIMIZE_WITH_HIGHS=1) + target_link_libraries(optimize-native-branching-coordinator-test PRIVATE highs::highs) + endif() + add_test(NAME optimize-native-branching-coordinator COMMAND optimize-native-branching-coordinator-test) + set_tests_properties(optimize-native_branching optimize-native-branching-coordinator PROPERTIES TIMEOUT 120) + add_executable(optimize-native-neighborhood-coordinator-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/native_neighborhoods.cpp" + model.cpp result.cpp validate.cpp constraints.cpp globals.cpp native.cpp native_presolve.cpp native_components.cpp native_symmetry.cpp presolve.cpp) + target_compile_features(optimize-native-neighborhood-coordinator-test PRIVATE cxx_std_17) + target_include_directories(optimize-native-neighborhood-coordinator-test PRIVATE "${GECODE_OPTIMIZE_ROOT}") + target_compile_definitions(optimize-native-neighborhood-coordinator-test PRIVATE + GECODE_NATIVE_NEIGHBORHOOD_TEST_HOOKS=1 GECODE_OPTIMIZE_WITH_NATIVE=1) + target_link_libraries(optimize-native-neighborhood-coordinator-test PRIVATE gecodeint gecodesearch Threads::Threads) + if(GECODE_OPTIMIZE_WITH_HIGHS) + target_compile_definitions(optimize-native-neighborhood-coordinator-test PRIVATE GECODE_OPTIMIZE_WITH_HIGHS=1) + target_link_libraries(optimize-native-neighborhood-coordinator-test PRIVATE highs::highs) + endif() + add_test(NAME optimize-native-neighborhood-coordinator COMMAND optimize-native-neighborhood-coordinator-test) + set_tests_properties(optimize-native_neighborhoods optimize-native-neighborhood-coordinator PROPERTIES TIMEOUT 120) + add_executable(optimize-native-start-coordinator-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/native_starts.cpp" + model.cpp result.cpp validate.cpp constraints.cpp globals.cpp native.cpp native_presolve.cpp native_components.cpp native_symmetry.cpp presolve.cpp) + target_compile_features(optimize-native-start-coordinator-test PRIVATE cxx_std_17) + target_include_directories(optimize-native-start-coordinator-test PRIVATE "${GECODE_OPTIMIZE_ROOT}") + target_compile_definitions(optimize-native-start-coordinator-test PRIVATE + GECODE_NATIVE_START_TEST_HOOKS=1 GECODE_NATIVE_SEARCH_TEST_HOOKS=1 GECODE_NATIVE_ROOT_CUT_TEST_HOOKS=1 GECODE_OPTIMIZE_WITH_NATIVE=1) + target_link_libraries(optimize-native-start-coordinator-test PRIVATE gecodeint gecodesearch Threads::Threads) + if(GECODE_OPTIMIZE_WITH_HIGHS) + target_compile_definitions(optimize-native-start-coordinator-test PRIVATE GECODE_OPTIMIZE_WITH_HIGHS=1) + target_link_libraries(optimize-native-start-coordinator-test PRIVATE highs::highs) + endif() + add_test(NAME optimize-native-start-coordinator COMMAND optimize-native-start-coordinator-test) + set_tests_properties(optimize-native_starts optimize-native-start-coordinator PROPERTIES TIMEOUT 120) + endif() + if(GECODE_OPTIMIZE_WITH_HIGHS) + target_compile_definitions(optimize-io-test PRIVATE GECODE_OPTIMIZE_IO_TEST_HIGHS=1) + target_link_libraries(optimize-io-test PRIVATE highs::highs) + endif() + set_tests_properties(optimize-io PROPERTIES + WORKING_DIRECTORY "${GECODE_OPTIMIZE_ROOT}") + # A deterministic substitute backend tests interruptions and invalid stage + # reports without timing-dependent sleeps or reliance on solver heuristics. + add_executable(optimize-workflow-coordinator-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/workflow.cpp" + model.cpp result.cpp validate.cpp constraints.cpp globals.cpp workflow.cpp) + target_compile_features(optimize-workflow-coordinator-test PRIVATE cxx_std_17) + target_include_directories(optimize-workflow-coordinator-test PRIVATE "${GECODE_OPTIMIZE_ROOT}") + target_compile_definitions(optimize-workflow-coordinator-test PRIVATE GECODE_WORKFLOW_TEST_FAKE_SOLVER=1) + target_link_libraries(optimize-workflow-coordinator-test PRIVATE Threads::Threads) + if(MSVC) + target_compile_options(optimize-workflow-coordinator-test PRIVATE /UNDEBUG) + else() + target_compile_options(optimize-workflow-coordinator-test PRIVATE -UNDEBUG) + endif() + add_test(NAME optimize-workflow-coordinator COMMAND optimize-workflow-coordinator-test) + set_tests_properties(optimize-workflow-coordinator PROPERTIES TIMEOUT 60) + add_executable(optimize-diagnostics-coordinator-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/diagnostics.cpp" + model.cpp result.cpp validate.cpp constraints.cpp globals.cpp diagnostics.cpp) + target_compile_features(optimize-diagnostics-coordinator-test PRIVATE cxx_std_17) + target_include_directories(optimize-diagnostics-coordinator-test PRIVATE "${GECODE_OPTIMIZE_ROOT}") + target_compile_definitions(optimize-diagnostics-coordinator-test PRIVATE GECODE_DIAGNOSTICS_TEST_FAKE_SOLVER=1) + target_link_libraries(optimize-diagnostics-coordinator-test PRIVATE Threads::Threads) + if(MSVC) + target_compile_options(optimize-diagnostics-coordinator-test PRIVATE /UNDEBUG) + else() + target_compile_options(optimize-diagnostics-coordinator-test PRIVATE -UNDEBUG) + endif() + add_test(NAME optimize-diagnostics-coordinator COMMAND optimize-diagnostics-coordinator-test) + set_tests_properties(optimize-diagnostics-coordinator PROPERTIES TIMEOUT 60) + add_executable(optimize-relaxation-coordinator-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/relaxation.cpp" + model.cpp result.cpp validate.cpp constraints.cpp globals.cpp workflow.cpp relaxation.cpp) + target_compile_features(optimize-relaxation-coordinator-test PRIVATE cxx_std_17) + target_include_directories(optimize-relaxation-coordinator-test PRIVATE "${GECODE_OPTIMIZE_ROOT}") + target_compile_definitions(optimize-relaxation-coordinator-test PRIVATE GECODE_RELAXATION_TEST_FAKE_SOLVER=1) + target_link_libraries(optimize-relaxation-coordinator-test PRIVATE Threads::Threads) + if(MSVC) + target_compile_options(optimize-relaxation-coordinator-test PRIVATE /UNDEBUG) + else() + target_compile_options(optimize-relaxation-coordinator-test PRIVATE -UNDEBUG) + endif() + add_test(NAME optimize-relaxation-coordinator COMMAND optimize-relaxation-coordinator-test) + set_tests_properties(optimize-relaxation-coordinator PROPERTIES TIMEOUT 60) + add_executable(optimize-pool-coordinator-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/pool.cpp" + model.cpp result.cpp validate.cpp constraints.cpp globals.cpp pool.cpp) + target_compile_features(optimize-pool-coordinator-test PRIVATE cxx_std_17) + target_include_directories(optimize-pool-coordinator-test PRIVATE "${GECODE_OPTIMIZE_ROOT}") + target_compile_definitions(optimize-pool-coordinator-test PRIVATE GECODE_POOL_TEST_FAKE_SOLVER=1) + target_link_libraries(optimize-pool-coordinator-test PRIVATE Threads::Threads) + if(MSVC) + target_compile_options(optimize-pool-coordinator-test PRIVATE /UNDEBUG) + else() + target_compile_options(optimize-pool-coordinator-test PRIVATE -UNDEBUG) + endif() + add_test(NAME optimize-pool-coordinator COMMAND optimize-pool-coordinator-test) + set_tests_properties(optimize-pool-coordinator PROPERTIES TIMEOUT 60) + # Only this executable has substitute scenario solves and checkpoint hooks. + # Link the ordinary library for shared model/session support; its separate + # scenarios object is not pulled into a static link by these definitions. + add_executable(optimize-scenarios-coordinator-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/scenarios_coordinator.cpp" scenarios.cpp) + target_compile_definitions(optimize-scenarios-coordinator-test PRIVATE GECODE_OPTIMIZE_TEST_SCENARIOS=1) + target_link_libraries(optimize-scenarios-coordinator-test PRIVATE gecodeoptimize) + if(MSVC) + target_compile_options(optimize-scenarios-coordinator-test PRIVATE /UNDEBUG) + else() + target_compile_options(optimize-scenarios-coordinator-test PRIVATE -UNDEBUG) + endif() + add_test(NAME optimize-scenarios-coordinator COMMAND optimize-scenarios-coordinator-test) + set_tests_properties(optimize-scenarios-coordinator PROPERTIES TIMEOUT 60) + add_executable(optimize-lp-evidence-coordinator-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/lp_evidence_coordinator.cpp" lp_evidence.cpp) + target_compile_definitions(optimize-lp-evidence-coordinator-test PRIVATE GECODE_OPTIMIZE_TEST_LP_EVIDENCE=1) + target_link_libraries(optimize-lp-evidence-coordinator-test PRIVATE gecodeoptimize) + add_test(NAME optimize-lp-evidence-coordinator COMMAND optimize-lp-evidence-coordinator-test) + set_tests_properties(optimize-lp-evidence-coordinator PROPERTIES TIMEOUT 60) + # The substitute factor and lifecycle checkpoints exist only in this test's + # separately compiled analyzer, never in the product library. + add_executable(optimize-lp-sensitivity-coordinator-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/lp_sensitivity_coordinator.cpp" lp_sensitivity.cpp) + target_compile_definitions(optimize-lp-sensitivity-coordinator-test PRIVATE GECODE_OPTIMIZE_TEST_LP_SENSITIVITY=1) + target_link_libraries(optimize-lp-sensitivity-coordinator-test PRIVATE gecodeoptimize) + add_test(NAME optimize-lp-sensitivity-coordinator COMMAND optimize-lp-sensitivity-coordinator-test) + set_tests_properties(optimize-lp-sensitivity-coordinator PROPERTIES TIMEOUT 60) + if(GECODE_OPTIMIZE_WITH_HIGHS) + add_executable(optimize-lp-sensitivity-factor-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/lp_sensitivity_factor.cpp") + target_compile_features(optimize-lp-sensitivity-factor-test PRIVATE cxx_std_17) + target_link_libraries(optimize-lp-sensitivity-factor-test PRIVATE highs::highs) + add_test(NAME optimize-lp-sensitivity-factor COMMAND optimize-lp-sensitivity-factor-test) + set_tests_properties(optimize-lp-sensitivity-factor PROPERTIES TIMEOUT 60) + endif() + add_executable(optimize-lp-sensitivity-binding-cleanup-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/lp_sensitivity_binding_cleanup.cpp" c_api.cpp) + target_compile_definitions(optimize-lp-sensitivity-binding-cleanup-test PRIVATE + GECODE_OPTIMIZE_TEST_SENSITIVITY_BINDING=1 GECODE_OPT_C_API_EXPORTS) + target_link_libraries(optimize-lp-sensitivity-binding-cleanup-test PRIVATE gecodeoptimize) + add_test(NAME optimize-lp-sensitivity-binding-cleanup COMMAND optimize-lp-sensitivity-binding-cleanup-test) + set_tests_properties(optimize-lp-sensitivity-binding-cleanup PROPERTIES TIMEOUT 60) + add_executable(optimize-lp-sensitivity-c-test "${GECODE_OPTIMIZE_ROOT}/test/optimize/lp_sensitivity_c.c") + set_target_properties(optimize-lp-sensitivity-c-test PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + target_link_libraries(optimize-lp-sensitivity-c-test PRIVATE gecodeoptimize_c) + add_test(NAME optimize-lp-sensitivity-c COMMAND optimize-lp-sensitivity-c-test) + set_tests_properties(optimize-lp-sensitivity-c PROPERTIES TIMEOUT 60) + add_executable(optimize-lp-evidence-binding-cleanup-test + "${GECODE_OPTIMIZE_ROOT}/test/optimize/lp_evidence_binding_cleanup.cpp" c_api.cpp) + target_compile_definitions(optimize-lp-evidence-binding-cleanup-test PRIVATE + GECODE_OPTIMIZE_TEST_EVIDENCE_BINDING=1 GECODE_OPT_C_API_EXPORTS) + target_link_libraries(optimize-lp-evidence-binding-cleanup-test PRIVATE gecodeoptimize) + add_test(NAME optimize-lp-evidence-binding-cleanup COMMAND optimize-lp-evidence-binding-cleanup-test) + set_tests_properties(optimize-lp-evidence-binding-cleanup PROPERTIES TIMEOUT 60) + add_executable(optimize-lp-evidence-c-test "${GECODE_OPTIMIZE_ROOT}/test/optimize/lp_evidence_c.c") + set_target_properties(optimize-lp-evidence-c-test PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + target_link_libraries(optimize-lp-evidence-c-test PRIVATE gecodeoptimize_c) + add_test(NAME optimize-lp-evidence-c COMMAND optimize-lp-evidence-c-test) + set_tests_properties(optimize-lp-evidence-c PROPERTIES TIMEOUT 60) + add_executable(optimize-c-api-test "${GECODE_OPTIMIZE_ROOT}/test/optimize/c_api.c") + set_target_properties(optimize-c-api-test PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + target_link_libraries(optimize-c-api-test PRIVATE gecodeoptimize_c) + add_test(NAME optimize-c-api COMMAND optimize-c-api-test) + set_tests_properties(optimize-c-api PROPERTIES TIMEOUT 60) + add_executable(optimize-lp-observations-c-test "${GECODE_OPTIMIZE_ROOT}/test/optimize/lp_observations_c.c") + set_target_properties(optimize-lp-observations-c-test PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + target_link_libraries(optimize-lp-observations-c-test PRIVATE gecodeoptimize_c) + add_test(NAME optimize-lp-observations-c COMMAND optimize-lp-observations-c-test) + set_tests_properties(optimize-lp-observations-c PROPERTIES TIMEOUT 60) + add_executable(optimize-scenarios-c-test "${GECODE_OPTIMIZE_ROOT}/test/optimize/scenarios_c.c") + set_target_properties(optimize-scenarios-c-test PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + target_link_libraries(optimize-scenarios-c-test PRIVATE gecodeoptimize_c) + add_test(NAME optimize-scenarios-c COMMAND optimize-scenarios-c-test) + set_tests_properties(optimize-scenarios-c PROPERTIES TIMEOUT 60) + foreach(c_test lp_basis regular) + string(REPLACE "_" "-" c_test_name "${c_test}") + add_executable(optimize-${c_test_name}-c-api-test "${GECODE_OPTIMIZE_ROOT}/test/optimize/${c_test}_c_api.c") + set_target_properties(optimize-${c_test_name}-c-api-test PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + target_link_libraries(optimize-${c_test_name}-c-api-test PRIVATE gecodeoptimize_c) + add_test(NAME optimize-${c_test_name}-c-api COMMAND optimize-${c_test_name}-c-api-test) + set_tests_properties(optimize-${c_test_name}-c-api PROPERTIES TIMEOUT 60) + endforeach() + find_package(Python3 3.9 COMPONENTS Interpreter QUIET) + if(Python3_Interpreter_FOUND) + set(_optimize_containment_arguments) + if(WIN32) + list(APPEND _optimize_containment_arguments --require-windows) + endif() + add_test(NAME optimize-process-containment COMMAND "${CMAKE_COMMAND}" -E env + "PYTHONDONTWRITEBYTECODE=1" "${Python3_EXECUTABLE}" -B + "${GECODE_OPTIMIZE_ROOT}/test/optimize/test_process_containment.py" + ${_optimize_containment_arguments} -v) + set_tests_properties(optimize-process-containment PROPERTIES TIMEOUT 60) + endif() + if(Python3_Interpreter_FOUND AND GECODE_OPTIMIZE_TEST_PYTHON) + add_test(NAME optimize-python COMMAND "${CMAKE_COMMAND}" -E env + "PYTHONDONTWRITEBYTECODE=1" "PYTHONPATH=${GECODE_OPTIMIZE_ROOT}/python" + "GECODE_OPTIMIZE_LIBRARY=$" + "${Python3_EXECUTABLE}" -m unittest discover -s "${GECODE_OPTIMIZE_ROOT}/python/tests" -v) + set_tests_properties(optimize-python PROPERTIES TIMEOUT 60) + endif() +endif() + +add_executable(optimize-example "${GECODE_OPTIMIZE_ROOT}/examples/optimize-linear.cpp") +target_link_libraries(optimize-example PRIVATE gecodeoptimize) +add_executable(optimize-file "${GECODE_OPTIMIZE_ROOT}/examples/optimize-file.cpp") +target_link_libraries(optimize-file PRIVATE gecodeoptimize) + +if(GECODE_OPTIMIZE_STANDALONE) + include(CMakePackageConfigHelpers) + install(TARGETS gecodeoptimize gecodeoptimize_c EXPORT GecodeOptimizeTargets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/gecode/optimize" + FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h" + PATTERN "quadratic_bound.hpp" EXCLUDE + PATTERN "lp_basis_detail.hpp" EXCLUDE + PATTERN "lp_sensitivity_backend.hpp" EXCLUDE + PATTERN "lp_sensitivity_highs_detail.hpp" EXCLUDE + PATTERN "native_regular_limits.hpp" EXCLUDE + PATTERN "lp_observations_detail.hpp" EXCLUDE) + install(FILES "${GECODE_OPTIMIZE_ROOT}/gecode/optimize.hh" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/gecode") + # The semantic compiler consumes owning records without a parser dependency. + install(FILES "${GECODE_OPTIMIZE_ROOT}/gecode/flatzinc/capture-records.hh" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/gecode/flatzinc") + configure_package_config_file(GecodeOptimizeConfig.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/GecodeOptimizeConfig.cmake" + INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/GecodeOptimize") + write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/GecodeOptimizeConfigVersion.cmake" + VERSION 0.1.0 COMPATIBILITY SameMajorVersion) + install(EXPORT GecodeOptimizeTargets NAMESPACE Gecode:: DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/GecodeOptimize") + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/GecodeOptimizeConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/GecodeOptimizeConfigVersion.cmake" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/GecodeOptimize") +endif() diff --git a/gecode/optimize/GecodeOptimizeConfig.cmake.in b/gecode/optimize/GecodeOptimizeConfig.cmake.in new file mode 100644 index 0000000000..8d9b26b736 --- /dev/null +++ b/gecode/optimize/GecodeOptimizeConfig.cmake.in @@ -0,0 +1,44 @@ +@PACKAGE_INIT@ + +# A generated export silently returns when its targets already +# exists. Distinguish repeated finds of this package from an optimization +# target imported by a combined Gecode package or another install prefix. +get_filename_component(_gecode_optimize_origin "${CMAKE_CURRENT_LIST_DIR}" REALPATH) +foreach(_gecode_optimize_target IN ITEMS Gecode::gecodeoptimize Gecode::optimize Gecode::gecodeoptimize_c Gecode::optimize_c) + if(TARGET "${_gecode_optimize_target}") + get_target_property(_gecode_optimize_existing_origin "${_gecode_optimize_target}" + GECODE_OPTIMIZE_PACKAGE_ORIGIN) + if(NOT "${_gecode_optimize_existing_origin}" STREQUAL "${_gecode_optimize_origin}") + set(GecodeOptimize_FOUND FALSE) + set(GecodeOptimize_NOT_FOUND_MESSAGE + "Target ${_gecode_optimize_target} already belongs to a different optimization package. Use either the combined Gecode optimization component or one standalone GecodeOptimize installation in a CMake target scope.") + unset(_gecode_optimize_existing_origin) + unset(_gecode_optimize_target) + unset(_gecode_optimize_origin) + return() + endif() + endif() +endforeach() +unset(_gecode_optimize_existing_origin) +unset(_gecode_optimize_target) + +include(CMakeFindDependencyMacro) +find_dependency(Threads) +if("@GECODE_OPTIMIZE_WITH_HIGHS@" STREQUAL "ON") + find_dependency(highs 1.15 CONFIG) +endif() +include("${CMAKE_CURRENT_LIST_DIR}/GecodeOptimizeTargets.cmake") +set_property(TARGET Gecode::gecodeoptimize PROPERTY GECODE_OPTIMIZE_PACKAGE_ORIGIN + "${_gecode_optimize_origin}") +if(NOT TARGET Gecode::optimize) + add_library(Gecode::optimize ALIAS Gecode::gecodeoptimize) +endif() +if(TARGET Gecode::gecodeoptimize_c) + set_property(TARGET Gecode::gecodeoptimize_c PROPERTY GECODE_OPTIMIZE_PACKAGE_ORIGIN + "${_gecode_optimize_origin}") + if(NOT TARGET Gecode::optimize_c) + add_library(Gecode::optimize_c ALIAS Gecode::gecodeoptimize_c) + endif() +endif() +unset(_gecode_optimize_origin) +check_required_components(GecodeOptimize) diff --git a/gecode/optimize/c_api.cpp b/gecode/optimize/c_api.cpp new file mode 100644 index 0000000000..4064972edc --- /dev/null +++ b/gecode/optimize/c_api.cpp @@ -0,0 +1,1361 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace O=Gecode::Optimize; +namespace { +thread_local char last_error[4096] = {}; +struct ApiError { int32_t code; const char* message; }; +[[noreturn]] void argument(const char* message){throw ApiError{GECODE_OPT_INVALID_ARGUMENT,message};} +void required(const void* pointer){if(!pointer)argument("required pointer is NULL");} +template int32_t boundary(F&& call) noexcept { + last_error[0]='\0'; + try{call();return GECODE_OPT_OK;} + catch(const ApiError& e){std::snprintf(last_error,sizeof(last_error),"%s",e.message);return e.code;} + catch(const O::ModelError& e){std::snprintf(last_error,sizeof(last_error),"%s",e.what());return GECODE_OPT_MODEL_ERROR;} + catch(const std::bad_alloc&){std::snprintf(last_error,sizeof(last_error),"allocation failed");return GECODE_OPT_OUT_OF_MEMORY;} + catch(const std::exception& e){std::snprintf(last_error,sizeof(last_error),"C++ backend exception: %s",e.what());return GECODE_OPT_INTERNAL_ERROR;} + catch(...){std::snprintf(last_error,sizeof(last_error),"unknown C++ backend exception");return GECODE_OPT_INTERNAL_ERROR;} +} +enum class Kind { Model, Session, Result, Cancellation, Pool, Repair, QuadraticModel, QuadraticResult, LpObservedResult, Basis, BasisSolveResult, ScenarioBatchResult, LpEvidenceResult, LpEvidenceStage, SensitivityResult }; +struct Entry {Kind kind;std::shared_ptr value;}; +struct Registry {std::mutex mutex;uint64_t next=1;std::unordered_map entries;}; +Registry& registry(){static Registry value;return value;} +struct ModelBox {std::mutex mutex;O::Model model;ModelBox()=default;explicit ModelBox(O::Model&& value):model(std::move(value)){};}; +struct QuadraticBox {std::mutex mutex;O::QuadraticModel model;}; +struct SessionBox {std::mutex mutex;O::SolveSession session;}; +struct BasisBox {std::shared_ptr basis;}; +struct RepairBox {O::RelaxationResult result;std::vector active;}; +template std::shared_ptr get(uint64_t handle,Kind kind){ + auto& r=registry();std::lock_guard lock(r.mutex);const auto found=r.entries.find(handle); + if(!handle||found==r.entries.end()||found->second.kind!=kind)throw ApiError{GECODE_OPT_INVALID_HANDLE,"unknown, destroyed, or wrong-kind handle"}; + return std::static_pointer_cast(found->second.value); +} +template uint64_t put(Kind kind,std::shared_ptr value){ + auto& r=registry();std::lock_guard lock(r.mutex); + if(r.next==std::numeric_limits::max())throw ApiError{GECODE_OPT_INTERNAL_ERROR,"handle token space exhausted"}; + const auto token=r.next++;r.entries.emplace(token,Entry{kind,std::move(value)});return token; +} +void destroy(uint64_t handle,Kind kind){ + std::shared_ptr doomed; + {auto& r=registry();std::lock_guard lock(r.mutex);const auto found=r.entries.find(handle); + if(!handle||found==r.entries.end()||found->second.kind!=kind)throw ApiError{GECODE_OPT_INVALID_HANDLE,"unknown, destroyed, or wrong-kind handle"}; + doomed=std::move(found->second.value);r.entries.erase(found);} +} +template void mutate(uint64_t handle,F&& f){auto box=get(handle,Kind::Model);std::lock_guard lock(box->mutex);f(box->model);} +O::ModelSnapshot snapshot(uint64_t handle){auto box=get(handle,Kind::Model);std::lock_guard lock(box->mutex);return box->model.snapshot();} +template void mutate_quadratic(uint64_t handle,F&& f){ + auto box=get(handle,Kind::QuadraticModel); + std::lock_guard lock(box->mutex);f(box->model); +} +O::QuadraticSnapshot quadratic_snapshot(uint64_t handle){ + auto box=get(handle,Kind::QuadraticModel); + std::lock_guard lock(box->mutex);return box->model.snapshot(); +} +O::Variable variable(gecode_opt_id id){if(id.kind!=GECODE_OPT_VARIABLE_ID||id.reserved)argument("wrong variable ID kind/reserved field");return {id.model_id,id.slot};} +O::Constraint row(gecode_opt_id id){if(id.kind!=GECODE_OPT_ROW_ID||id.reserved)argument("wrong row ID kind/reserved field");return {id.model_id,id.slot};} +O::GlobalConstraint global(gecode_opt_id id){if(id.kind!=GECODE_OPT_GLOBAL_ID||id.reserved)argument("wrong global ID kind/reserved field");return {id.model_id,id.slot};} +O::Indicator indicator(gecode_opt_id id){if(id.kind!=GECODE_OPT_INDICATOR_ID||id.reserved)argument("wrong indicator ID kind/reserved field");return {id.model_id,id.slot};} +gecode_opt_id identifier(O::GlobalConstraint id) noexcept {return {id.model_id,id.id,GECODE_OPT_GLOBAL_ID,0};} +gecode_opt_id identifier(O::Variable id) noexcept {return {id.model_id,id.id,GECODE_OPT_VARIABLE_ID,0};} +gecode_opt_id identifier(O::Constraint id) noexcept {return {id.model_id,id.id,GECODE_OPT_ROW_ID,0};} +template std::size_t length(uint64_t n){ + if(n>std::numeric_limits::max()||n>static_cast(std::numeric_limits::max())/sizeof(T)) + argument("count exceeds representable address/vector range"); + if(n>std::vector().max_size())argument("count exceeds vector maximum size"); + return static_cast(n); +} +template std::size_t count(uint64_t n,const T* pointer){ + const auto size=length(n);if(n&&!pointer)argument("nonzero count requires a non-NULL array");return size; +} +std::vector variables(const gecode_opt_id* input,uint64_t n){ + const auto size=count(n,input);std::vector result;result.reserve(size); + for(std::size_t i=0;i integers(const int64_t* input,uint64_t n){ + const auto size=count(n,input);if(!size)return {};return {input,input+size}; +} +std::vector terms(const gecode_opt_term* input,uint64_t n){std::vector result;const auto size=count(n,input);result.reserve(size); + for(std::size_t i=0;i(name); + while(*p){ + const unsigned char first=*p++; + if(first<0x80)continue; + unsigned count=0;uint32_t code=0,min=0; + if(first>=0xc2&&first<=0xdf){count=1;code=first&0x1f;min=0x80;} + else if(first>=0xe0&&first<=0xef){count=2;code=first&0x0f;min=0x800;} + else if(first>=0xf0&&first<=0xf4){count=3;code=first&7;min=0x10000;} + else argument("bulk name is not valid UTF-8"); + for(unsigned i=0;i0xbf)argument("bulk name is not valid UTF-8"); + ++p;code=(code<<6)|(next&0x3f); + } + if(code0x10ffff||(code>=0xd800&&code<=0xdfff))argument("bulk name is not valid UTF-8"); + } + return name; +} +void bulk_output(gecode_opt_id* output,uint64_t capacity,uint64_t needed){ + length(capacity); + if(capacity indices(const uint64_t* input,uint64_t n){ + const auto size=count(n,input);std::vector out;out.reserve(size); + for(std::size_t i=0;istd::numeric_limits::max())argument("CSR index exceeds size_t range"); + out.push_back(static_cast(input[i])); + } + return out; +} +std::vector numbers(const double* input,uint64_t n,bool finite=false){ + const auto size=count(n,input);std::vector out;out.reserve(size); + for(std::size_t i=0;istruct_size!=sizeof(*input))argument("options struct_size does not match ABI v1"); + if(input->reserved||input->has_node_limit<0||input->has_node_limit>1)argument("invalid reserved field or node-limit presence flag"); + result.backend=backend(input->backend);result.guarantee=guarantee(input->guarantee); + result.threads=input->threads;result.random_seed=input->random_seed;result.time_limit_seconds=input->time_limit_seconds; + result.relative_gap=input->relative_gap;result.absolute_gap=input->absolute_gap; + result.feasibility_tolerance=input->feasibility_tolerance;result.integrality_tolerance=input->integrality_tolerance; + if(input->has_node_limit)result.node_limit=input->node_limit; + if(input->cancellation)result.cancellation=get(input->cancellation,Kind::Cancellation); + const auto n=count(input->primal_start_count,input->primal_start);result.primal_start.reserve(n); + for(std::size_t i=0;iprimal_start[i].variable),input->primal_start[i].value}); + result.validate();return result; +} +void size_check(const void* pointer,uint64_t actual,std::size_t expected){required(pointer);if(actual!=expected)argument("output struct size does not match ABI v1");} +using Clock=std::chrono::steady_clock; +void account_preparation(O::SolveOptions& options,Clock::time_point start){ + if(std::isfinite(options.time_limit_seconds))options.time_limit_seconds=std::max(0.0,options.time_limit_seconds-std::chrono::duration(Clock::now()-start).count()); +} +gecode_opt_options_v1 default_options(){ + O::SolveOptions o;gecode_opt_options_v1 out{};out.struct_size=sizeof(out); + out.backend=GECODE_OPT_AUTO;out.guarantee=GECODE_OPT_NUMERICAL; + out.threads=o.threads;out.random_seed=o.random_seed;out.time_limit_seconds=o.time_limit_seconds; + out.relative_gap=o.relative_gap;out.absolute_gap=o.absolute_gap; + out.feasibility_tolerance=o.feasibility_tolerance;out.integrality_tolerance=o.integrality_tolerance;return out; +} +gecode_opt_optional_number_v1 optional(std::optional value){return {value.has_value(),0,value.value_or(0)};} +int32_t completion(O::PoolCompletion value){switch(value){ + case O::PoolCompletion::Incomplete:return GECODE_OPT_POOL_INCOMPLETE; + case O::PoolCompletion::RequestedLimit:return GECODE_OPT_POOL_REQUESTED_LIMIT; + case O::PoolCompletion::Exhausted:return GECODE_OPT_POOL_EXHAUSTED;} + argument("unknown pool completion"); +} +O::RelaxationSide side(int32_t value){switch(value){case GECODE_OPT_RELAX_LOWER:return O::RelaxationSide::Lower; + case GECODE_OPT_RELAX_UPPER:return O::RelaxationSide::Upper;default:argument("unknown relaxation side");}} +int32_t side(O::RelaxationSide value){switch(value){case O::RelaxationSide::Lower:return GECODE_OPT_RELAX_LOWER; + case O::RelaxationSide::Upper:return GECODE_OPT_RELAX_UPPER;}argument("unknown relaxation side");} +template const T& at(const std::vector& values,uint64_t index){ + if(index>=values.size())argument("workflow index is out of range");return values[static_cast(index)]; +} +template bool output_array(T* buffer,uint64_t capacity,uint64_t* needed,std::size_t n){ + required(needed);*needed=static_cast(n);if(!capacity&&!buffer)return false; + count(capacity,buffer);if(capacity<*needed)throw ApiError{GECODE_OPT_BUFFER_TOO_SMALL,"workflow buffer is smaller than required count"};return true; +} +void output_text(const std::string& text,char* buffer,uint64_t capacity,uint64_t* needed){ + if(text.size()==std::numeric_limits::max()||text.size()==std::numeric_limits::max())argument("text length cannot include NUL"); + if(output_array(buffer,capacity,needed,text.size()+1))std::memcpy(buffer,text.c_str(),text.size()+1); +} +void original_variable(const RepairBox& box,O::Variable variable){ + if(variable.model_id!=box.result.source_model_id||variable.id>=box.active.size()||!box.active[variable.id]) + throw O::ModelError("repair lookup variable is foreign, absent, or deleted"); +} +} + +#define API(name, signature, ...) extern "C" int32_t gecode_opt_v1_##name signature noexcept {return boundary([&](){__VA_ARGS__;});} +extern "C" uint32_t gecode_opt_v1_abi_version(void) noexcept {return 1;} +extern "C" const char* gecode_opt_v1_last_error(void) noexcept {return last_error;} +API(options_default,(gecode_opt_options_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));O::SolveOptions o;*out={};out->struct_size=sizeof(*out);out->backend=GECODE_OPT_AUTO;out->guarantee=GECODE_OPT_NUMERICAL; + out->threads=o.threads;out->random_seed=o.random_seed;out->time_limit_seconds=o.time_limit_seconds;out->relative_gap=o.relative_gap;out->absolute_gap=o.absolute_gap; + out->feasibility_tolerance=o.feasibility_tolerance;out->integrality_tolerance=o.integrality_tolerance) +API(capabilities,(int32_t selection,int32_t* available,int32_t* lp,int32_t* mip), + required(available);required(lp);required(mip);auto c=O::capabilities(backend(selection));*available=c.available;*lp=c.linear_programming;*mip=c.mixed_integer_linear) +API(model_create,(gecode_opt_handle* out),required(out);*out=0;*out=put(Kind::Model,std::make_shared())) +API(model_destroy,(gecode_opt_handle handle),destroy(handle,Kind::Model)) +API(model_identity,(gecode_opt_handle handle,uint64_t* owner,uint64_t* revision), + required(owner);required(revision);mutate(handle,[&](O::Model& m){*owner=m.id();*revision=m.revision();})) +API(model_read,(const char* filename,gecode_opt_handle* out),required(filename);required(out);*out=0;*out=put(Kind::Model,std::make_shared(O::read_model(filename)))) +API(model_write,(gecode_opt_handle handle,const char* filename),required(filename);O::write_model(snapshot(handle),filename)) +API(model_add_variable,(gecode_opt_handle handle,int32_t kind,double lower,double upper,const char* name,gecode_opt_id* out), + required(out);*out={};const auto t=type(kind);mutate(handle,[&](O::Model& m){auto v=m.add_variable(t,lower,upper,name?name:"");*out={v.model_id,v.id,GECODE_OPT_VARIABLE_ID,0};})) +API(model_add_row,(gecode_opt_handle handle,const gecode_opt_term* input,uint64_t n,double lower,double upper,const char* name,gecode_opt_id* out), + required(out);*out={};auto ts=terms(input,n);mutate(handle,[&](O::Model& m){auto r=m.add_row(ts,lower,upper,name?name:"");*out={r.model_id,r.id,GECODE_OPT_ROW_ID,0};})) +API(model_add_variables,(gecode_opt_handle h,const gecode_opt_variable_spec_v1* input,uint64_t n,gecode_opt_id* output,uint64_t capacity), + const auto size=count(n,input);length(n);bulk_output(output,capacity,n); + std::vector specs;specs.reserve(size); + for(std::size_t i=0;i(n);bulk_output(output,capacity,n); + std::vector specs;specs.reserve(size); + for(std::size_t i=0;i(b.lower_count);bulk_output(output,capacity,b.lower_count); + if(b.lower_count!=b.upper_count||!b.row_start_count||b.row_start_count-1!=b.lower_count|| + b.column_count!=b.coefficient_count||(b.names_count&&b.names_count!=b.lower_count)) + argument("CSR independent array dimensions do not agree"); + O::SparseRowBatch batch; + batch.columns=variables(b.columns,b.columns_count);batch.row_start=indices(b.row_start,b.row_start_count); + batch.column=indices(b.column,b.column_count);batch.coefficient=numbers(b.coefficient,b.coefficient_count,true); + batch.lower=numbers(b.lower,b.lower_count);batch.upper=numbers(b.upper,b.upper_count); + const auto names=count(b.names_count,b.names);length(b.names_count);batch.names.reserve(names); + for(std::size_t i=0;istd::numeric_limits::max()/arity)argument("table dimensions overflow"); + if(n!=arity*rows)argument("table value_count must equal arity times tuple_count"); + const auto row_count=length>(rows);auto vs=variables(input,arity);auto values=integers(flat,n); + std::vector> tuples;tuples.reserve(row_count); + if(vs.empty())tuples.resize(row_count); + else for(std::size_t i=0;i(edge_count); + for(std::size_t i=0;imodel_id,data.inactive_gate->id,GECODE_OPT_VARIABLE_ID,0};}})) +API(model_remove_indicator,(gecode_opt_handle h,gecode_opt_id id),mutate(h,[&](O::Model& m){O::remove_indicator(m,indicator(id));})) +API(model_add_boolean_and,(gecode_opt_handle h,gecode_opt_id result,const gecode_opt_id* input,uint64_t n,const char* name), + auto vs=variables(input,n);auto r=variable(result);mutate(h,[&](O::Model& m){O::add_boolean_and(m,r,vs,name?name:"");})) +API(model_add_boolean_or,(gecode_opt_handle h,gecode_opt_id result,const gecode_opt_id* input,uint64_t n,const char* name), + auto vs=variables(input,n);auto r=variable(result);mutate(h,[&](O::Model& m){O::add_boolean_or(m,r,vs,name?name:"");})) +API(cancellation_create,(gecode_opt_handle* out),required(out);*out=0;*out=put(Kind::Cancellation,std::make_shared())) +API(cancellation_cancel,(gecode_opt_handle h),get(h,Kind::Cancellation)->cancel()) +API(cancellation_is_cancelled,(gecode_opt_handle h,int32_t* out),required(out);*out=get(h,Kind::Cancellation)->cancelled()) +API(cancellation_copy,(gecode_opt_handle h,gecode_opt_handle* out),required(out);*out=0;auto token=get(h,Kind::Cancellation);*out=put(Kind::Cancellation,std::move(token))) +API(cancellation_destroy,(gecode_opt_handle h),destroy(h,Kind::Cancellation)) +API(session_create,(gecode_opt_handle* out),required(out);*out=0;*out=put(Kind::Session,std::make_shared())) +API(session_destroy,(gecode_opt_handle h),destroy(h,Kind::Session)) +API(session_reset,(gecode_opt_handle h),auto box=get(h,Kind::Session);std::lock_guard lock(box->mutex);box->session.reset()) +API(session_statistics,(gecode_opt_handle h,gecode_opt_session_statistics_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto box=get(h,Kind::Session);std::lock_guard lock(box->mutex);auto s=box->session.statistics(); + *out={s.solve_calls,s.model_loads,s.incremental_updates,s.unchanged_models,s.basis_warm_starts,s.incumbent_starts}) +API(solve,(gecode_opt_handle h,const gecode_opt_options_v1* input,gecode_opt_handle* out), + const auto start=Clock::now();required(out);*out=0;auto opts=options(input);auto model=snapshot(h);account_preparation(opts,start);*out=put(Kind::Result,std::make_shared(O::solve(model,opts)))) +API(session_solve,(gecode_opt_handle session,gecode_opt_handle h,const gecode_opt_options_v1* input,gecode_opt_handle* out), + const auto start=Clock::now();required(out);*out=0;auto opts=options(input);auto model=snapshot(h);auto box=get(session,Kind::Session); + std::lock_guard lock(box->mutex);account_preparation(opts,start);*out=put(Kind::Result,std::make_shared(box->session.solve(model,opts)))) +API(result_destroy,(gecode_opt_handle h),destroy(h,Kind::Result)) +API(result_info,(gecode_opt_handle h,gecode_opt_result_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto r=get(h,Kind::Result); + *out={r->model_id,r->revision,static_cast(r->active_variables.size()),termination(r->termination),guarantee(r->guarantee),r->has_solution(),r->solution_validated,r->start_submitted,0,r->elapsed_seconds}) +API(result_number,(gecode_opt_handle h,int32_t field,int32_t* present,double* value), + required(present);required(value);*present=0;*value=0;auto r=get(h,Kind::Result);std::optional number; + switch(field){case GECODE_OPT_OBJECTIVE:number=r->objective;break;case GECODE_OPT_BEST_BOUND:number=r->best_bound;break; + case GECODE_OPT_ABSOLUTE_GAP:number=r->absolute_gap;break;case GECODE_OPT_RELATIVE_GAP:number=r->relative_gap;break; + case GECODE_OPT_NATIVE_GAP:number=r->native_backend_gap;break;default:argument("unknown result number field");} + if(number){*present=1;*value=*number;}) +API(result_value,(gecode_opt_handle h,gecode_opt_id v,double* out),required(out);auto r=get(h,Kind::Result); + if(!r->has_solution())throw ApiError{GECODE_OPT_NO_SOLUTION,"result has no validated solution"};*out=r->value(variable(v))) +API(result_values,(gecode_opt_handle h,double* values,uint8_t* active,uint8_t* present,uint64_t capacity,uint64_t* needed), + required(needed);auto r=get(h,Kind::Result);*needed=static_cast(r->active_variables.size()); + if(capacity==0&&!values&&!active&&!present)return; + count(capacity,values);required(active);required(present); + if(capacity<*needed)throw ApiError{GECODE_OPT_BUFFER_TOO_SMALL,"value buffers are smaller than required slot count"}; + const bool solution=r->has_solution();for(std::size_t i=0;iactive_variables.size();++i){active[i]=r->active_variables[i];present[i]=active[i]&&solution;values[i]=present[i]?r->values[i]:0;}) +API(result_text,(gecode_opt_handle h,int32_t field,char* buffer,uint64_t capacity,uint64_t* needed), + required(needed);auto r=get(h,Kind::Result);const std::string* text=nullptr; + switch(field){case GECODE_OPT_BACKEND_NAME:text=&r->backend;break;case GECODE_OPT_BACKEND_VERSION:text=&r->backend_version;break;case GECODE_OPT_MESSAGE:text=&r->message;break;default:argument("unknown result text field");} + if(text->size()==std::numeric_limits::max())argument("text length cannot include NUL");*needed=static_cast(text->size())+1; + if(!capacity&&!buffer)return;count(capacity,buffer);if(capacity<*needed)throw ApiError{GECODE_OPT_BUFFER_TOO_SMALL,"text buffer is smaller than required NUL-terminated length"}; + std::memcpy(buffer,text->c_str(),static_cast(*needed))) +API(pool_options_default,(gecode_opt_pool_options_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));*out={};out->struct_size=sizeof(*out);out->solve=default_options();out->max_solutions=10) +API(pool_solve,(gecode_opt_handle h,const gecode_opt_pool_options_v1* input,gecode_opt_handle* out), + const auto start=Clock::now();required(out);*out=0;O::PoolOptions opts; + if(input){if(input->struct_size!=sizeof(*input)||input->reserved||input->has_projection<0||input->has_projection>1) + argument("invalid pool options size, reserved field, or projection presence flag"); + opts.solve=options(&input->solve);opts.max_solutions=length(input->max_solutions); + if(input->has_projection)opts.projection=variables(input->projection,input->projection_count); + else if(input->projection||input->projection_count)argument("absent projection requires NULL pointer and zero count");} + auto model=snapshot(h);account_preparation(opts.solve,start); + *out=put(Kind::Pool,std::make_shared(O::solve_pool(model,opts)))) +API(pool_destroy,(gecode_opt_handle h),destroy(h,Kind::Pool)) +API(pool_info,(gecode_opt_handle h,gecode_opt_pool_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto r=get(h,Kind::Pool); + *out={r->model_id,r->revision,static_cast(r->projection.size()),static_cast(r->entries.size()), + static_cast(r->attempts.size()),static_cast(r->ranked_prefix),termination(r->termination),completion(r->completion),guarantee(r->guarantee),0,r->elapsed_seconds}) +API(pool_projection,(gecode_opt_handle h,gecode_opt_id* buffer,uint64_t capacity,uint64_t* needed), + auto r=get(h,Kind::Pool);if(output_array(buffer,capacity,needed,r->projection.size())) + for(std::size_t i=0;iprojection.size();++i)buffer[i]=identifier(r->projection[i])) +API(pool_entry_info,(gecode_opt_handle h,uint64_t index,gecode_opt_pool_entry_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto r=get(h,Kind::Pool);const auto& e=at(r->entries,index); + *out={static_cast(e.projection_values.size()),e.rank_established,0}) +API(pool_entry_result,(gecode_opt_handle h,uint64_t index,gecode_opt_handle* out), + required(out);*out=0;auto r=get(h,Kind::Pool);*out=put(Kind::Result,std::make_shared(at(r->entries,index).solution))) +API(pool_entry_projection,(gecode_opt_handle h,uint64_t index,int64_t* buffer,uint64_t capacity,uint64_t* needed), + auto r=get(h,Kind::Pool);const auto& values=at(r->entries,index).projection_values; + if(output_array(buffer,capacity,needed,values.size()))std::copy(values.begin(),values.end(),buffer)) +API(pool_attempt_info,(gecode_opt_handle h,uint64_t index,gecode_opt_pool_attempt_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto r=get(h,Kind::Pool);const auto& a=at(r->attempts,index); + *out={termination(a.termination),guarantee(a.guarantee),a.candidate_accepted,a.rank_established,optional(a.objective),optional(a.remaining_bound)}) +API(pool_message,(gecode_opt_handle h,char* buffer,uint64_t capacity,uint64_t* needed), + auto r=get(h,Kind::Pool);output_text(r->message,buffer,capacity,needed)) +API(repair_options_default,(gecode_opt_repair_options_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));*out={};out->struct_size=sizeof(*out);out->solve=default_options()) +API(repair_solve,(gecode_opt_handle h,const gecode_opt_repair_options_v1* input,gecode_opt_handle* out), + const auto start=Clock::now();required(out);*out=0;O::RelaxationOptions opts; + if(input){if(input->struct_size!=sizeof(*input)||input->reserved||input->optimize_original_objective<0||input->optimize_original_objective>1) + argument("invalid repair options size, reserved field, or objective refinement flag"); + opts.solve=options(&input->solve);opts.optimize_original_objective=input->optimize_original_objective; + const auto n=count(input->selection_count,input->selections); + for(std::size_t i=0;iselections[i]; + if(selection.reserved)argument("relaxation selection reserved field must be zero");const auto s=side(selection.side); + if(!std::isfinite(selection.penalty)||selection.penalty<=0)argument("relaxation penalty must be positive and finite"); + if(selection.source.kind==GECODE_OPT_VARIABLE_ID)opts.bounds.push_back({variable(selection.source),s,selection.penalty}); + else if(selection.source.kind==GECODE_OPT_ROW_ID)opts.rows.push_back({row(selection.source),s,selection.penalty}); + else argument("relaxation selection requires a Variable or Row ID");}} + auto model=snapshot(h);auto box=std::make_shared();box->active.reserve(model.variables.size()); + for(const auto& v:model.variables)box->active.push_back(v.active); + account_preparation(opts.solve,start);box->result=O::relax_feasibility(model,opts);*out=put(Kind::Repair,std::move(box))) +API(repair_destroy,(gecode_opt_handle h),destroy(h,Kind::Repair)) +API(repair_info,(gecode_opt_handle h,gecode_opt_repair_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::Repair);const auto& r=b->result; + *out={r.source_model_id,r.source_revision,r.private_model?r.private_model->model_id:0,r.private_model?r.private_model->revision:0, + static_cast(b->active.size()),static_cast(r.items.size()),static_cast(r.workflow.stages.size()), + static_cast(r.workflow.completed_stages),termination(r.termination),guarantee(r.guarantee),r.private_model.has_value(),r.has_repair(), + r.minimum_violation_established,r.original_objective_optimized,termination(r.workflow.termination),guarantee(r.workflow.guarantee),r.elapsed_seconds,r.workflow.elapsed_seconds}) +API(repair_number,(gecode_opt_handle h,int32_t field,int32_t* present,double* value), + required(present);required(value);*present=0;*value=0;auto b=get(h,Kind::Repair);const auto& r=b->result;std::optional number; + switch(field){case GECODE_OPT_REPAIR_MINIMUM_VIOLATION:number=r.minimum_weighted_violation;break; + case GECODE_OPT_REPAIR_VIOLATION:number=r.weighted_violation;break;case GECODE_OPT_REPAIR_ORIGINAL_OBJECTIVE:number=r.original_objective;break; + default:argument("unknown repair number field");}if(number){*present=1;*value=*number;}) +API(repair_original_value,(gecode_opt_handle h,gecode_opt_id id,double* out), + required(out);auto b=get(h,Kind::Repair);const auto v=variable(id);original_variable(*b,v); + if(!b->result.has_repair())throw ApiError{GECODE_OPT_NO_SOLUTION,"repair has no independently validated repaired assignment"}; + *out=at(b->result.original_values,v.id)) +API(repair_original_values,(gecode_opt_handle h,double* values,uint8_t* active,uint8_t* present,uint64_t capacity,uint64_t* needed), + required(needed);auto b=get(h,Kind::Repair);*needed=static_cast(b->active.size()); + if(!capacity&&!values&&!active&&!present)return;count(capacity,values);required(active);required(present); + if(capacity<*needed)throw ApiError{GECODE_OPT_BUFFER_TOO_SMALL,"repair value buffers are smaller than source slot count"}; + const bool solution=b->result.has_repair();if(solution&&b->result.original_values.size()!=b->active.size())throw std::runtime_error("malformed repair assignment size"); + for(std::size_t i=0;iactive.size();++i){active[i]=b->active[i];present[i]=active[i]&&solution;values[i]=present[i]?b->result.original_values[i]:0;}) +API(repair_variable_map,(gecode_opt_handle h,gecode_opt_id* source,gecode_opt_id* private_ids,uint8_t* active,uint64_t capacity,uint64_t* needed), + required(needed);auto b=get(h,Kind::Repair);*needed=static_cast(b->active.size()); + if(!capacity&&!source&&!private_ids&&!active)return;count(capacity,source);required(private_ids);required(active); + if(capacity<*needed)throw ApiError{GECODE_OPT_BUFFER_TOO_SMALL,"repair mapping buffers are smaller than source slot count"}; + const auto& ids=b->result.private_variables;if(!ids.empty()&&ids.size()!=b->active.size())throw std::runtime_error("malformed repair variable mapping"); + for(std::size_t i=0;iactive.size();++i){source[i]={b->result.source_model_id,static_cast(i),GECODE_OPT_VARIABLE_ID,0}; + private_ids[i]=ids.empty()?gecode_opt_id{}:identifier(ids[i]);active[i]=b->active[i];}) +API(repair_validation,(gecode_opt_handle h,gecode_opt_validation_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::Repair);const auto& v=b->result.original_validation; + *out={v.valid,v.model_valid,static_cast(v.violated_globals),v.max_bound_violation,v.max_row_violation, + v.max_integrality_violation,v.max_indicator_violation,optional(v.objective)}) +API(repair_item_info,(gecode_opt_handle h,uint64_t index,gecode_opt_repair_item_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::Repair);const auto& item=at(b->result.items,index); + *out={item.source_row?identifier(*item.source_row):item.source_variable?identifier(*item.source_variable):gecode_opt_id{}, + identifier(item.slack),identifier(item.penalty_row),side(item.side),0,item.original_bound,item.penalty, + optional(item.activity),optional(item.violation),optional(item.weighted_violation),optional(item.slack_value)}) +API(repair_stage_info,(gecode_opt_handle h,uint64_t index,gecode_opt_repair_stage_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::Repair);const auto& stage=at(b->result.workflow.stages,index); + *out={static_cast(stage.index),stage.completed,0,optional(stage.retention_bound)}) +API(repair_stage_result,(gecode_opt_handle h,uint64_t index,gecode_opt_handle* out), + required(out);*out=0;auto b=get(h,Kind::Repair);*out=put(Kind::Result,std::make_shared(at(b->result.workflow.stages,index).result))) +API(repair_final_result,(gecode_opt_handle h,gecode_opt_handle* out), + required(out);*out=0;auto b=get(h,Kind::Repair); + if(!b->result.has_repair())throw ApiError{GECODE_OPT_NO_SOLUTION,"repair has no independently validated repaired assignment"}; + *out=put(Kind::Result,std::make_shared(b->result.workflow.final_solution))) +API(repair_violation_lock,(gecode_opt_handle h,int32_t* present,gecode_opt_id* out), + required(present);required(out);*present=0;*out={};auto b=get(h,Kind::Repair); + if(b->result.violation_lock){*present=1;*out=identifier(*b->result.violation_lock);}) +API(repair_objective_values,(gecode_opt_handle h,double* buffer,uint64_t capacity,uint64_t* needed), + auto b=get(h,Kind::Repair);const auto& values=b->result.workflow.objective_values; + if(output_array(buffer,capacity,needed,values.size()))std::copy(values.begin(),values.end(),buffer)) +API(repair_text,(gecode_opt_handle h,int32_t field,uint64_t index,char* buffer,uint64_t capacity,uint64_t* needed), + auto b=get(h,Kind::Repair);const auto& r=b->result;const std::string* text=nullptr; + if(field>=GECODE_OPT_REPAIR_MESSAGE&&field<=GECODE_OPT_REPAIR_VALIDATION_MESSAGE&&index)argument("non-indexed repair text requires index zero"); + switch(field){case GECODE_OPT_REPAIR_MESSAGE:text=&r.message;break;case GECODE_OPT_REPAIR_WORKFLOW_MESSAGE:text=&r.workflow.message;break; + case GECODE_OPT_REPAIR_VALIDATION_MESSAGE:text=&r.original_validation.message;break; + case GECODE_OPT_REPAIR_ITEM_NAME:text=&at(r.items,index).name;break;case GECODE_OPT_REPAIR_STAGE_NAME:text=&at(r.workflow.stages,index).name;break; + default:argument("unknown repair text field");}output_text(*text,buffer,capacity,needed)) +API(quadratic_capabilities,(int32_t* available), + required(available);*available=O::quadratic_capabilities().available) +API(quadratic_options_default,(gecode_opt_quadratic_options_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));O::QuadraticOptions defaults;*out={};out->struct_size=sizeof(*out); + out->solve=default_options();out->iteration_limit=defaults.iteration_limit; + out->max_auxiliary_variables=defaults.max_auxiliary_variables;out->max_lifted_nonzeros=defaults.max_lifted_nonzeros; + out->stationarity_tolerance=defaults.stationarity_tolerance;out->complementarity_tolerance=defaults.complementarity_tolerance; + out->optimality_tolerance=defaults.optimality_tolerance) +API(quadratic_model_create,(gecode_opt_handle* out), + required(out);*out=0;*out=put(Kind::QuadraticModel,std::make_shared())) +API(quadratic_model_destroy,(gecode_opt_handle h),destroy(h,Kind::QuadraticModel)) +API(quadratic_model_identity,(gecode_opt_handle h,uint64_t* owner,uint64_t* revision), + required(owner);required(revision);mutate_quadratic(h,[&](O::QuadraticModel& m){*owner=m.id();*revision=m.revision();})) +API(quadratic_model_add_continuous,(gecode_opt_handle h,double l,double u,const char* name,gecode_opt_id* out), + required(out);*out={};auto label=bulk_name(name); + mutate_quadratic(h,[&](O::QuadraticModel& m){*out=identifier(m.add_continuous(l,u,std::move(label)));})) +API(quadratic_model_add_row,(gecode_opt_handle h,const gecode_opt_term* input,uint64_t n,double l,double u,const char* name,gecode_opt_id* out), + required(out);*out={};auto ts=terms(input,n);auto label=bulk_name(name); + mutate_quadratic(h,[&](O::QuadraticModel& m){*out=identifier(m.add_row(ts,l,u,std::move(label)));})) +API(quadratic_model_set_objective,(gecode_opt_handle h,const gecode_opt_weighted_square_v1* input,uint64_t n, + const gecode_opt_term* linear,uint64_t linear_n,int32_t objective_sense,double offset), + const auto selected=sense(objective_sense);const auto size=count(n,input); + length(n); + std::vector squares;squares.reserve(size); + for(std::size_t i=0;istruct_size!=sizeof(*input))argument("quadratic options struct_size does not match ABI v1"); + if(input->solve.struct_size!=sizeof(input->solve))argument("nested options struct_size does not match ABI v1"); + if(input->reserved)argument("quadratic options reserved field must be zero"); + opts.solve=options(&input->solve);opts.iteration_limit=input->iteration_limit; + opts.max_auxiliary_variables=length(input->max_auxiliary_variables); + opts.max_lifted_nonzeros=length(input->max_lifted_nonzeros); + opts.stationarity_tolerance=input->stationarity_tolerance;opts.complementarity_tolerance=input->complementarity_tolerance; + opts.optimality_tolerance=input->optimality_tolerance;opts.validate();} + const auto model=quadratic_snapshot(h);account_preparation(opts.solve,start); + auto result=std::make_shared(O::solve_quadratic(model,opts)); + result->result.elapsed_seconds=std::chrono::duration(Clock::now()-start).count(); + *out=put(Kind::QuadraticResult,std::move(result))) +API(quadratic_result_destroy,(gecode_opt_handle h),destroy(h,Kind::QuadraticResult)) +API(quadratic_result_info,(gecode_opt_handle h,gecode_opt_quadratic_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto q=get(h,Kind::QuadraticResult);const auto& r=q->result; + *out={{r.model_id,r.revision,static_cast(r.active_variables.size()),termination(r.termination),guarantee(r.guarantee), + r.has_solution(),r.solution_validated,r.start_submitted,0,r.elapsed_seconds},q->qp_iterations,q->regularization}) +API(quadratic_result_checks,(gecode_opt_handle h,gecode_opt_quadratic_checks_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto q=get(h,Kind::QuadraticResult);const auto& c=q->checks; + *out={c.primal_valid,c.objective_valid,c.kkt_available,c.kkt_valid,c.bound_valid,0, + optional(c.kkt_available?std::optional(c.max_stationarity):std::nullopt), + optional(c.kkt_available?std::optional(c.max_complementarity):std::nullopt), + optional(c.original_objective),optional(c.normalized_lower_bound),optional(c.gap_upper_bound), + static_cast(c.objective_valid?c.square_values.size():0), + static_cast(c.objective_valid?c.original_gradient.size():0)}) +API(quadratic_result_number,(gecode_opt_handle h,int32_t field,int32_t* present,double* value), + required(present);required(value);*present=0;*value=0;auto q=get(h,Kind::QuadraticResult); + const auto& r=q->result;std::optional number; + switch(field){case GECODE_OPT_OBJECTIVE:number=r.objective;break;case GECODE_OPT_BEST_BOUND:number=r.best_bound;break; + case GECODE_OPT_ABSOLUTE_GAP:number=r.absolute_gap;break;case GECODE_OPT_RELATIVE_GAP:number=r.relative_gap;break; + case GECODE_OPT_NATIVE_GAP:number=r.native_backend_gap;break;case GECODE_OPT_QP_VENDOR_OBJECTIVE:number=q->vendor_objective;break; + case GECODE_OPT_QP_VENDOR_DUAL_ESTIMATE:number=q->vendor_dual_estimate;break;default:argument("unknown quadratic number field");} + if(number){*present=1;*value=*number;}) +API(quadratic_result_value,(gecode_opt_handle h,gecode_opt_id v,double* out), + required(out);auto q=get(h,Kind::QuadraticResult);const auto& r=q->result; + if(!r.has_solution())throw ApiError{GECODE_OPT_NO_SOLUTION,"QP result has no validated original solution"};*out=r.value(variable(v))) +API(quadratic_result_values,(gecode_opt_handle h,double* values,uint8_t* active,uint8_t* present,uint64_t capacity,uint64_t* needed), + required(needed);auto q=get(h,Kind::QuadraticResult);const auto& r=q->result; + *needed=static_cast(r.active_variables.size());if(!capacity&&!values&&!active&&!present)return; + count(capacity,values);count(capacity,active);count(capacity,present); + if(capacity<*needed)throw ApiError{GECODE_OPT_BUFFER_TOO_SMALL,"QP value buffers are smaller than required slot count"}; + const bool solution=r.has_solution();for(std::size_t i=0;i(h,Kind::QuadraticResult);const auto& c=q->checks;const std::vector* data=nullptr; + switch(field){case GECODE_OPT_QP_SQUARE_RESIDUALS:data=&c.square_values;break; + case GECODE_OPT_QP_ORIGINAL_GRADIENT:data=&c.original_gradient;break;default:argument("unknown quadratic array field");} + const auto size=c.objective_valid?data->size():0; + if(output_array(values,capacity,needed,size)&&size)std::copy(data->begin(),data->end(),values)) +API(quadratic_result_text,(gecode_opt_handle h,int32_t field,char* buffer,uint64_t capacity,uint64_t* needed), + auto q=get(h,Kind::QuadraticResult);const std::string* value=nullptr; + switch(field){case GECODE_OPT_BACKEND_NAME:value=&q->result.backend;break;case GECODE_OPT_BACKEND_VERSION:value=&q->result.backend_version;break; + case GECODE_OPT_MESSAGE:value=&q->result.message;break;case GECODE_OPT_QP_CHECK_MESSAGE:value=&q->checks.message;break; + default:argument("unknown quadratic text field");}output_text(*value,buffer,capacity,needed)) +namespace { +O::LpObservationOptions lp_options(const gecode_opt_lp_options_v1* input) { + O::LpObservationOptions out;if(!input)return out; + if(input->struct_size!=sizeof(*input))argument("LP options struct_size does not match ABI v1"); + if(input->reserved || input->duals<0 || input->duals>1 || input->basis<0 || input->basis>1) + argument("invalid LP reserved field or request flag"); + out.solve=options(&input->solve);out.duals=input->duals;out.basis=input->basis; + out.checks={input->dual_feasibility,input->stationarity,input->complementarity,input->objective_gap}; + out.validate();return out; +} +std::shared_ptr lp_observations(uint64_t handle) { + auto result=get(handle,Kind::LpObservedResult); + if(!result->observations)throw ApiError{GECODE_OPT_NO_OBSERVATIONS,"result has no owning LP observations"}; + return result->observations; +} +int32_t lp_state(O::LpObservationState value) {switch(value){ + case O::LpObservationState::NotRequested:return GECODE_OPT_LP_NOT_REQUESTED; + case O::LpObservationState::Available:return GECODE_OPT_LP_AVAILABLE; + case O::LpObservationState::Unavailable:return GECODE_OPT_LP_UNAVAILABLE; + case O::LpObservationState::Rejected:return GECODE_OPT_LP_REJECTED; +}argument("unknown LP LpObservationState enum");} +int32_t lp_reason(O::LpObservationReason value) {switch(value){ + case O::LpObservationReason::None:return GECODE_OPT_LP_REASON_NONE; + case O::LpObservationReason::NotRequested:return GECODE_OPT_LP_REASON_NOT_REQUESTED; + case O::LpObservationReason::Unsupported:return GECODE_OPT_LP_REASON_UNSUPPORTED; + case O::LpObservationReason::NoBackendSolve:return GECODE_OPT_LP_REASON_NO_BACKEND_SOLVE; + case O::LpObservationReason::NoPrimalPoint:return GECODE_OPT_LP_REASON_NO_PRIMAL_POINT; + case O::LpObservationReason::NotOptimal:return GECODE_OPT_LP_REASON_NOT_OPTIMAL; + case O::LpObservationReason::NoDualPoint:return GECODE_OPT_LP_REASON_NO_DUAL_POINT; + case O::LpObservationReason::NoBasis:return GECODE_OPT_LP_REASON_NO_BASIS; + case O::LpObservationReason::ElidedConstantRows:return GECODE_OPT_LP_REASON_ELIDED_CONSTANT_ROWS; + case O::LpObservationReason::Interrupted:return GECODE_OPT_LP_REASON_INTERRUPTED; + case O::LpObservationReason::InvalidBackendData:return GECODE_OPT_LP_REASON_INVALID_BACKEND_DATA; + case O::LpObservationReason::FailedChecks:return GECODE_OPT_LP_REASON_FAILED_CHECKS; + case O::LpObservationReason::AllocationFailure:return GECODE_OPT_LP_REASON_ALLOCATION_FAILURE; + case O::LpObservationReason::InvalidModel:return GECODE_OPT_LP_REASON_INVALID_MODEL; +}argument("unknown LP LpObservationReason enum");} +int32_t lp_basis(O::LpBasisStatus value) {switch(value){ + case O::LpBasisStatus::Lower:return GECODE_OPT_LP_BASIS_LOWER; + case O::LpBasisStatus::Basic:return GECODE_OPT_LP_BASIS_BASIC; + case O::LpBasisStatus::Upper:return GECODE_OPT_LP_BASIS_UPPER; + case O::LpBasisStatus::Zero:return GECODE_OPT_LP_BASIS_ZERO; + case O::LpBasisStatus::NonbasicUnspecified:return GECODE_OPT_LP_BASIS_NONBASIC_UNSPECIFIED; +}argument("unknown LP LpBasisStatus enum");} +int32_t lp_source(O::LpDualSource value) {switch(value){ + case O::LpDualSource::None:return GECODE_OPT_LP_DUAL_NONE; + case O::LpDualSource::Backend:return GECODE_OPT_LP_DUAL_BACKEND; + case O::LpDualSource::DerivedConstantRow:return GECODE_OPT_LP_DUAL_DERIVED_CONSTANT_ROW; +}argument("unknown LP LpDualSource enum");} +const O::LpObservationGroup& lp_group(const O::LpObservations& data,int32_t field) { + switch(field){case GECODE_OPT_LP_PRIMAL_ROWS:return data.primal_rows(); + case GECODE_OPT_LP_DUAL_POINT:return data.dual_point();case GECODE_OPT_LP_BASIS:return data.basis(); + default:argument("unknown LP group selector");} +} +gecode_opt_lp_row_v1 lp_row(const O::LpRowObservation& data) { + gecode_opt_lp_row_v1 out{};out.struct_size=sizeof(out);out.active=data.active; + out.dual_source=lp_source(data.dual_source);out.has_basis=data.basis.has_value(); + if(data.basis)out.basis=lp_basis(*data.basis); + out.activity=optional(data.activity);out.lower_slack=optional(data.lower_slack); + out.upper_slack=optional(data.upper_slack);out.dual=optional(data.dual);return out; +} +gecode_opt_lp_column_v1 lp_column(const O::LpColumnObservation& data) { + gecode_opt_lp_column_v1 out{};out.struct_size=sizeof(out);out.active=data.active; + out.has_basis=data.basis.has_value();if(data.basis)out.basis=lp_basis(*data.basis); + out.reduced_cost=optional(data.reduced_cost);return out; +} +} +API(lp_options_default,(gecode_opt_lp_options_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));O::LpObservationOptions o;*out={};out->struct_size=sizeof(*out);out->solve=default_options(); + out->duals=o.duals;out->basis=o.basis;out->dual_feasibility=o.checks.dual_feasibility;out->stationarity=o.checks.stationarity; + out->complementarity=o.checks.complementarity;out->objective_gap=o.checks.objective_gap) +API(lp_capabilities,(gecode_opt_lp_capabilities_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));const auto c=O::lp_observation_capabilities();*out={};out->struct_size=sizeof(*out); + out->available=c.available;out->duals=c.duals;out->basis_export=c.basis_export;out->limitation_count=c.limitations.size()) +API(lp_capability_text,(int32_t field,uint64_t index,char* buffer,uint64_t capacity,uint64_t* needed), + const auto c=O::lp_observation_capabilities();const std::string* value=nullptr; + if(field!=GECODE_OPT_LP_CAP_LIMITATION&&index)argument("LP capability text index must be zero"); + switch(field){case GECODE_OPT_LP_CAP_BACKEND:value=&c.backend;break;case GECODE_OPT_LP_CAP_VERSION:value=&c.backend_version;break; + case GECODE_OPT_LP_CAP_LIMITATION:value=&at(c.limitations,index);break;default:argument("unknown LP capability text selector");} + output_text(*value,buffer,capacity,needed)) +API(solve_lp_observed,(gecode_opt_handle h,const gecode_opt_lp_options_v1* input,gecode_opt_handle* out), + const auto start=Clock::now();required(out);*out=0;auto opts=lp_options(input);auto model=snapshot(h); + account_preparation(opts.solve,start);*out=put(Kind::LpObservedResult,std::make_shared(O::solve_lp_observed(model,opts)))) +API(session_solve_lp_observed,(gecode_opt_handle session,gecode_opt_handle h,const gecode_opt_lp_options_v1* input,gecode_opt_handle* out), + const auto start=Clock::now();required(out);*out=0;auto opts=lp_options(input);auto model=snapshot(h);auto box=get(session,Kind::Session); + std::lock_guard lock(box->mutex);account_preparation(opts.solve,start); + *out=put(Kind::LpObservedResult,std::make_shared(box->session.solve_lp_observed(model,opts)))) +API(lp_observed_result_destroy,(gecode_opt_handle h),destroy(h,Kind::LpObservedResult)) +API(lp_observed_result_copy_result,(gecode_opt_handle h,gecode_opt_handle* out), + required(out);*out=0;auto r=get(h,Kind::LpObservedResult); + *out=put(Kind::Result,std::make_shared(r->result))) +API(lp_observed_result_info,(gecode_opt_handle h,gecode_opt_lp_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto observed=get(h,Kind::LpObservedResult);const auto& r=observed->result; + *out={};out->struct_size=sizeof(*out);out->result={r.model_id,r.revision,static_cast(r.active_variables.size()),termination(r.termination), + guarantee(r.guarantee),r.has_solution(),r.solution_validated,r.start_submitted,0,r.elapsed_seconds}; + if(observed->observations){const auto& d=*observed->observations;out->has_observations=1;out->model_id=d.id();out->revision=d.revision();out->row_slots=d.rows().size();out->column_slots=d.columns().size();}) +API(lp_observed_result_metadata,(gecode_opt_handle h,gecode_opt_lp_metadata_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto data=lp_observations(h);const auto& m=data->metadata();*out={};out->struct_size=sizeof(*out); + out->dual_feasibility=m.checks.dual_feasibility;out->stationarity=m.checks.stationarity;out->complementarity=m.checks.complementarity; + out->objective_gap=m.checks.objective_gap;out->primal_check_tolerance=m.primal_check_tolerance; + out->backend_primal_tolerance=optional(m.backend_primal_tolerance);out->backend_dual_tolerance=optional(m.backend_dual_tolerance)) +API(lp_observed_result_group,(gecode_opt_handle h,int32_t field,gecode_opt_lp_group_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto data=lp_observations(h);const auto& g=lp_group(*data,field); + *out={};out->struct_size=sizeof(*out);out->state=lp_state(g.state);out->reason=lp_reason(g.reason)) +API(lp_observed_result_checks,(gecode_opt_handle h,gecode_opt_lp_checks_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto data=lp_observations(h);const auto& c=data->checks();*out={};out->struct_size=sizeof(*out); + out->primal_valid=c.primal_valid;out->dual_signs_valid=c.dual_signs_valid;out->stationarity_valid=c.stationarity_valid; + out->complementarity_valid=c.complementarity_valid;out->gap_valid=c.gap_valid;out->accepted=c.accepted; + out->max_dual_sign_violation=optional(c.max_dual_sign_violation);out->max_stationarity=optional(c.max_stationarity); + out->max_complementarity=optional(c.max_complementarity);out->dual_objective_estimate=optional(c.dual_objective_estimate);out->normalized_gap=optional(c.normalized_gap)) +API(lp_observed_result_row,(gecode_opt_handle h,gecode_opt_id id,gecode_opt_lp_row_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto data=lp_observations(h);*out=lp_row(data->row(row(id)))) +API(lp_observed_result_column,(gecode_opt_handle h,gecode_opt_id id,gecode_opt_lp_column_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto data=lp_observations(h);*out=lp_column(data->column(variable(id)))) +API(lp_observed_result_rows,(gecode_opt_handle h,gecode_opt_lp_row_v1* buffer,uint64_t element_size,uint64_t capacity,uint64_t* needed), + if(element_size!=sizeof(*buffer))argument("LP row element size does not match ABI v1");auto data=lp_observations(h); + if(output_array(buffer,capacity,needed,data->rows().size()))for(std::size_t i=0;irows().size();++i)buffer[i]=lp_row(data->rows()[i])) +API(lp_observed_result_columns,(gecode_opt_handle h,gecode_opt_lp_column_v1* buffer,uint64_t element_size,uint64_t capacity,uint64_t* needed), + if(element_size!=sizeof(*buffer))argument("LP column element size does not match ABI v1");auto data=lp_observations(h); + if(output_array(buffer,capacity,needed,data->columns().size()))for(std::size_t i=0;icolumns().size();++i)buffer[i]=lp_column(data->columns()[i])) +API(lp_observed_result_text,(gecode_opt_handle h,int32_t field,char* buffer,uint64_t capacity,uint64_t* needed), + auto d=lp_observations(h);const std::string* value=nullptr; + switch(field){case GECODE_OPT_LP_BACKEND_NAME:value=&d->metadata().backend;break;case GECODE_OPT_LP_BACKEND_VERSION:value=&d->metadata().backend_version;break; + case GECODE_OPT_LP_PRIMAL_MESSAGE:value=&d->primal_rows().message;break;case GECODE_OPT_LP_DUAL_MESSAGE:value=&d->dual_point().message;break; + case GECODE_OPT_LP_BASIS_MESSAGE:value=&d->basis().message;break;case GECODE_OPT_LP_CHECK_MESSAGE:value=&d->checks().message;break; + default:argument("unknown LP observation text selector");}output_text(*value,buffer,capacity,needed)) + +namespace { +std::vector> basis_status_input(const int32_t* values,uint64_t n) { + const auto size=count(n,values);length>(n); + std::vector> out;out.reserve(size); + for(std::size_t i=0;i basis) { + if(!basis)throw ApiError{GECODE_OPT_NO_BASIS,"result has no requested LP basis"}; + auto box=std::make_shared();box->basis=std::move(basis);return put(Kind::Basis,std::move(box)); +} +O::LpBasisSolveOptions basis_options(uint64_t h,const gecode_opt_lp_options_v1* input) { + O::LpBasisSolveOptions out;out.observations=lp_options(input);out.basis=get(h,Kind::Basis)->basis; + out.validate();return out; +} +} +API(basis_from_observed,(gecode_opt_handle h,gecode_opt_handle* out), + required(out);*out=0;*out=own_basis(O::make_lp_basis(*lp_observations(h)))) +API(basis_from_model,(gecode_opt_handle h,const int32_t* columns,uint64_t column_count,const int32_t* rows,uint64_t row_count,gecode_opt_handle* out), + required(out);*out=0;count(column_count,columns);count(row_count,rows); + O::LpBasisData data;data.columns=basis_status_input(columns,column_count);data.rows=basis_status_input(rows,row_count); + data.source=snapshot(h);*out=own_basis(O::make_lp_basis(data))) +API(basis_destroy,(gecode_opt_handle h),destroy(h,Kind::Basis)) +API(basis_info,(gecode_opt_handle h,gecode_opt_basis_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::Basis)->basis;*out={};out->struct_size=sizeof(*out); + out->model_id=b->id();out->revision=b->revision();out->row_slots=b->rows().size();out->column_slots=b->columns().size();out->origin=basis_origin(b->origin())) +API(basis_statuses,(gecode_opt_handle h,int32_t entity_kind,int32_t* buffer,uint64_t capacity,uint64_t* needed), + if(entity_kind!=GECODE_OPT_VARIABLE_ID&&entity_kind!=GECODE_OPT_ROW_ID)argument("basis statuses require VARIABLE_ID or ROW_ID selector"); + auto b=get(h,Kind::Basis)->basis;const auto& values=entity_kind==GECODE_OPT_VARIABLE_ID?b->columns():b->rows(); + if(output_array(buffer,capacity,needed,values.size()))for(std::size_t i=0;i(h,Kind::Basis)->basis; + if(key.model_id!=b->id()||key.id>=b->rows().size()||!b->rows()[key.id])throw O::ModelError("basis row is foreign, absent, or deleted"); + *out=lp_basis(*b->rows()[key.id])) +API(basis_column,(gecode_opt_handle h,gecode_opt_id id,int32_t* out), + required(out);const auto key=variable(id);auto b=get(h,Kind::Basis)->basis; + if(key.model_id!=b->id()||key.id>=b->columns().size()||!b->columns()[key.id])throw O::ModelError("basis variable is foreign, absent, or deleted"); + *out=lp_basis(*b->columns()[key.id])) +API(solve_lp_with_basis,(gecode_opt_handle h,gecode_opt_handle basis,const gecode_opt_lp_options_v1* input,gecode_opt_handle* out), + const auto start=Clock::now();required(out);*out=0;auto opts=basis_options(basis,input);auto model=snapshot(h); + account_preparation(opts.observations.solve,start); + *out=put(Kind::BasisSolveResult,std::make_shared(O::solve_lp_with_basis(model,opts)))) +API(session_solve_lp_with_basis,(gecode_opt_handle session,gecode_opt_handle h,gecode_opt_handle basis,const gecode_opt_lp_options_v1* input,gecode_opt_handle* out), + const auto start=Clock::now();required(out);*out=0;auto opts=basis_options(basis,input);auto model=snapshot(h);auto box=get(session,Kind::Session); + std::lock_guard lock(box->mutex);account_preparation(opts.observations.solve,start); + *out=put(Kind::BasisSolveResult,std::make_shared(box->session.solve_lp_with_basis(model,opts)))) +API(basis_result_destroy,(gecode_opt_handle h),destroy(h,Kind::BasisSolveResult)) +API(basis_result_info,(gecode_opt_handle h,gecode_opt_basis_result_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::BasisSolveResult);const auto& r=b->observed.result; + *out={};out->struct_size=sizeof(*out);out->result={r.model_id,r.revision,static_cast(r.active_variables.size()),termination(r.termination), + guarantee(r.guarantee),r.has_solution(),r.solution_validated,r.start_submitted,0,r.elapsed_seconds}; + if(b->requested_basis){out->has_requested_basis=1;out->requested_model_id=b->requested_basis->id();out->requested_revision=b->requested_basis->revision();} + out->state=basis_submission(b->submission.state);out->backend_attempted=b->submission.backend_attempted; + out->has_statuses_changed=b->submission.statuses_changed.has_value();if(b->submission.statuses_changed)out->statuses_changed=*b->submission.statuses_changed) +API(basis_result_message,(gecode_opt_handle h,char* buffer,uint64_t capacity,uint64_t* needed), + auto b=get(h,Kind::BasisSolveResult);output_text(b->submission.message,buffer,capacity,needed)) +API(basis_result_copy_observed,(gecode_opt_handle h,gecode_opt_handle* out), + required(out);*out=0;auto b=get(h,Kind::BasisSolveResult); + *out=put(Kind::LpObservedResult,std::make_shared(b->observed))) +API(basis_result_copy_basis,(gecode_opt_handle h,gecode_opt_handle* out), + required(out);*out=0;auto b=get(h,Kind::BasisSolveResult);*out=own_basis(b->requested_basis)) + + +namespace { +O::ScenarioId scenario_id(gecode_opt_scenario_id id){return {id.batch_id,id.index};} +gecode_opt_scenario_id scenario_id(O::ScenarioId id){return {id.batch_id,id.index};} +const O::ScenarioBatch& scenario_batch(const O::ScenarioBatchResult& result){ + if(!result.batch)throw O::ModelError("scenario batch was not admitted");return *result.batch; +} +const O::ScenarioOutcome& scenario_outcome(const O::ScenarioBatchResult& result,gecode_opt_scenario_id id){ + scenario_batch(result).definition(scenario_id(id));return at(result.outcomes,id.index); +} +gecode_opt_session_statistics_v1 scenario_statistics(const O::SessionStatistics& s){ + return {s.solve_calls,s.model_loads,s.incremental_updates,s.unchanged_models,s.basis_warm_starts,s.incumbent_starts}; +} +int32_t scenario_completion(O::ScenarioBatchCompletion value){switch(value){ + case O::ScenarioBatchCompletion::Rejected:return GECODE_OPT_SCENARIO_REJECTED; + case O::ScenarioBatchCompletion::Interrupted:return GECODE_OPT_SCENARIO_INTERRUPTED; + case O::ScenarioBatchCompletion::Complete:return GECODE_OPT_SCENARIO_COMPLETE; +}argument("unknown scenario completion");} +int32_t scenario_state(O::ScenarioRunState value){switch(value){ + case O::ScenarioRunState::NotStarted:return GECODE_OPT_SCENARIO_NOT_STARTED; + case O::ScenarioRunState::Attempted:return GECODE_OPT_SCENARIO_ATTEMPTED; +}argument("unknown scenario run state");} +O::ScenarioBatchOptions scenario_options(const gecode_opt_scenario_options_v1* input){ + O::ScenarioBatchOptions result;if(!input)return result; + if(input->struct_size!=sizeof(*input))argument("scenario options struct_size does not match v1"); + if(input->reserved||input->reserved_flags)argument("scenario options reserved fields must be zero"); + result.solve=options(&input->solve); + switch(input->reuse){case GECODE_OPT_SCENARIO_AUTOMATIC:result.reuse=O::ScenarioReuse::Automatic;break; + case GECODE_OPT_SCENARIO_COLD:result.reuse=O::ScenarioReuse::Cold;break;default:argument("unknown scenario reuse");} + result.max_scenarios=length(input->max_scenarios); + result.max_patch_entries=length(input->max_patch_entries); + result.max_saved_value_slots=length(input->max_saved_value_slots); + if(input->max_work>std::numeric_limits::max())argument("scenario work limit exceeds size_t"); + result.max_work=static_cast(input->max_work);result.validate();return result; +} +void scenario_presence(int32_t present){if(present!=0&&present!=1)argument("scenario presence flag must be zero or one");} +void scenario_bounds(const gecode_opt_scenario_bounds_v1& input){ + if(input.struct_size!=sizeof(input)||input.reserved)argument("invalid scenario bound size/reserved"); + scenario_presence(input.has_lower);scenario_presence(input.has_upper); +} +std::optional scenario_side(int32_t present,double value){return present?std::optional(value):std::nullopt;} +gecode_opt_scenario_bounds_v1 scenario_bound(gecode_opt_id id,std::optional lower,std::optional upper){ + return {sizeof(gecode_opt_scenario_bounds_v1),0,id,lower.has_value(),upper.has_value(),lower.value_or(0),upper.value_or(0)}; +} +std::vector scenario_definitions(const gecode_opt_scenario_definition_v1* input,std::size_t n){ + std::vector out;out.reserve(n); + for(std::size_t i=0;istruct_size=sizeof(*out);out->solve=default_options(); + out->reuse=GECODE_OPT_SCENARIO_AUTOMATIC;out->max_scenarios=o.max_scenarios;out->max_patch_entries=o.max_patch_entries; + out->max_saved_value_slots=o.max_saved_value_slots;out->max_work=o.max_work) +API(solve_scenarios,(gecode_opt_handle h,const gecode_opt_scenario_definition_v1* input,uint64_t n,uint64_t element_size,const gecode_opt_scenario_options_v1* option_input,gecode_opt_handle* output), + const auto start=Clock::now();required(output);*output=0; + if(element_size!=sizeof(*input))argument("scenario definition element_size does not match v1"); + const auto size=count(n,input);length(n);auto opts=scenario_options(option_input);const auto total_seconds=opts.solve.time_limit_seconds; + std::shared_ptr result; + {auto model=snapshot(h); + bool capped=size>opts.max_scenarios;std::size_t patches=0; + // Preflight counts and versioned headers before allocating owned patch arrays. + if(!capped)for(std::size_t i=0;i(item.objective_count); + const auto nv=count(item.variable_count,item.variable_bounds);length(item.variable_count); + const auto nr=count(item.row_count,item.row_bounds);length(item.row_count); + for(auto add:{nt,nv,nr}){if(add>opts.max_patch_entries-patches){capped=true;break;}patches+=add;} + if(capped)break; + } + if(capped){result=std::make_shared();result->model_id=model.model_id;result->revision=model.revision; + result->stop_reason=O::Termination::MemoryLimit;result->message="scenario binding input storage limit"; + if(opts.solve.cancellation&&opts.solve.cancellation->cancelled()){ + result->stop_reason=O::Termination::Cancelled;result->message="scenario binding cancelled before input copies"; + }else if(std::isfinite(total_seconds)&&std::chrono::duration(Clock::now()-start).count()>=total_seconds){ + result->stop_reason=O::Termination::TimeLimit;result->message="scenario binding deadline reached before input copies"; + }} + else {auto definitions=scenario_definitions(input,size);account_preparation(opts.solve,start); + result=std::make_shared(O::solve_scenarios(model,definitions,opts));}} + // The binding's temporary source and copied input arrays are now gone. Keep + // timely individual histories if only whole-call cleanup exceeded the budget. + if(result->completion==O::ScenarioBatchCompletion::Complete){ + std::optional stopped; + if(opts.solve.cancellation&&opts.solve.cancellation->cancelled())stopped=O::Termination::Cancelled; + else if(std::isfinite(total_seconds)&&std::chrono::duration(Clock::now()-start).count()>=total_seconds)stopped=O::Termination::TimeLimit; + if(stopped){result->completion=O::ScenarioBatchCompletion::Interrupted;result->stop_reason=stopped; + result->message="Whole scenario binding budget stopped during final input cleanup";} + } + result->elapsed_seconds=std::chrono::duration(Clock::now()-start).count(); + *output=put(Kind::ScenarioBatchResult,std::move(result))) +API(scenario_batch_destroy,(gecode_opt_handle h),destroy(h,Kind::ScenarioBatchResult)) +API(scenario_batch_info,(gecode_opt_handle h,gecode_opt_scenario_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto r=get(h,Kind::ScenarioBatchResult);*out={};out->struct_size=sizeof(*out); + out->model_id=r->model_id;out->revision=r->revision;out->has_batch=bool(r->batch);out->completion=scenario_completion(r->completion); + if(r->batch){out->batch_id=r->batch->id();out->scenario_count=r->batch->size();}out->outcome_count=r->outcomes.size(); + out->has_stop_reason=r->stop_reason.has_value();if(r->stop_reason)out->stop_reason=termination(*r->stop_reason); + out->has_offending_scenario=r->offending_scenario.has_value();if(r->offending_scenario)out->offending_scenario=*r->offending_scenario; + out->all_resolved=r->all_resolved();out->attempted=r->attempted;out->resolved=r->resolved;out->work=r->work; + out->elapsed_seconds=r->elapsed_seconds;out->reuse_statistics=scenario_statistics(r->reuse_statistics)) +API(scenario_batch_id,(gecode_opt_handle h,uint64_t index,gecode_opt_scenario_id* out), + required(out);auto r=get(h,Kind::ScenarioBatchResult); + if(index>std::numeric_limits::max())argument("scenario index exceeds size_t"); + *out=scenario_id(scenario_batch(*r).scenario(static_cast(index)))) +API(scenario_batch_outcome,(gecode_opt_handle h,gecode_opt_scenario_id id,gecode_opt_scenario_outcome_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::ScenarioBatchResult);const auto& o=scenario_outcome(*b,id); + *out={};out->struct_size=sizeof(*out);out->scenario=scenario_id(o.scenario);out->state=scenario_state(o.state); + out->has_result=o.result.has_value();out->has_check=o.check.has_value();out->reuse_delta=scenario_statistics(o.reuse_delta);out->elapsed_seconds=o.elapsed_seconds; + if(o.result){const auto& r=*o.result;out->result={r.model_id,r.revision,static_cast(r.active_variables.size()), + termination(r.termination),guarantee(r.guarantee),r.has_solution(),r.solution_validated,r.start_submitted,0,r.elapsed_seconds};}) +API(scenario_batch_check,(gecode_opt_handle h,gecode_opt_scenario_id id,gecode_opt_scenario_check_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::ScenarioBatchResult);const auto& o=scenario_outcome(*b,id); + *out={};out->struct_size=sizeof(*out);out->has_check=o.check.has_value();if(o.check){const auto& c=*o.check; + out->identity_valid=c.identity_valid;out->candidate_examined=c.candidate_examined; + if(c.candidate_examined){out->objective_matches=c.objective_matches;out->exact_witness_validated=c.exact_witness_validated; + const auto& v=c.validation;out->validation={v.valid,v.model_valid,static_cast(v.violated_globals), + v.max_bound_violation,v.max_row_violation,v.max_integrality_violation,v.max_indicator_violation,optional(v.objective)};}}) +API(scenario_batch_copy_result,(gecode_opt_handle h,gecode_opt_scenario_id id,gecode_opt_handle* out), + required(out);*out=0;auto b=get(h,Kind::ScenarioBatchResult);const auto& o=scenario_outcome(*b,id); + if(!o.result)throw ApiError{GECODE_OPT_NO_SOLUTION,"scenario has no attempted result"}; + *out=put(Kind::Result,std::make_shared(*o.result))) +API(scenario_batch_map,(gecode_opt_handle h,gecode_opt_id original,gecode_opt_id* out), + required(out);auto b=get(h,Kind::ScenarioBatchResult);const auto& source=scenario_batch(*b); + if(original.kind==GECODE_OPT_VARIABLE_ID)*out=identifier(source.map(variable(original))); + else if(original.kind==GECODE_OPT_ROW_ID)*out=identifier(source.map(row(original)));else argument("scenario map requires variable or row ID")) +API(scenario_batch_value,(gecode_opt_handle h,gecode_opt_scenario_id id,gecode_opt_id original,double* out), + required(out);auto b=get(h,Kind::ScenarioBatchResult);const auto& o=scenario_outcome(*b,id); + const auto v=variable(original);scenario_batch(*b).map(v); + if(!o.result||!o.result->has_solution())throw ApiError{GECODE_OPT_NO_SOLUTION,"scenario has no validated solution"}; + *out=b->value(scenario_id(id),v)) +API(scenario_batch_message,(gecode_opt_handle h,char* buffer,uint64_t capacity,uint64_t* needed), + auto b=get(h,Kind::ScenarioBatchResult);output_text(b->message,buffer,capacity,needed)) +API(scenario_batch_text,(gecode_opt_handle h,gecode_opt_scenario_id id,int32_t field,char* buffer,uint64_t capacity,uint64_t* needed), + auto b=get(h,Kind::ScenarioBatchResult);const auto& def=scenario_batch(*b).definition(scenario_id(id)); + switch(field){case GECODE_OPT_SCENARIO_NAME:output_text(def.name,buffer,capacity,needed);break; + case GECODE_OPT_SCENARIO_VALIDATION_MESSAGE:{const auto& o=scenario_outcome(*b,id); + output_text(o.check&&o.check->candidate_examined?o.check->validation.message:std::string(),buffer,capacity,needed);break;} + default:argument("unknown scenario text selector");}) +API(scenario_batch_definition,(gecode_opt_handle h,gecode_opt_scenario_id id,gecode_opt_scenario_definition_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::ScenarioBatchResult);const auto& d=scenario_batch(*b).definition(scenario_id(id)); + *out={sizeof(*out),0,static_cast(d.objective_coefficients.size()),static_cast(d.variable_bounds.size()), + static_cast(d.row_bounds.size()),optional(d.objective_offset)}) +API(scenario_batch_objective,(gecode_opt_handle h,gecode_opt_scenario_id id,gecode_opt_term* buffer,uint64_t capacity,uint64_t* needed), + auto b=get(h,Kind::ScenarioBatchResult);const auto& d=scenario_batch(*b).definition(scenario_id(id)); + if(output_array(buffer,capacity,needed,d.objective_coefficients.size()))for(std::size_t i=0;i(h,Kind::ScenarioBatchResult);const auto& d=scenario_batch(*b).definition(scenario_id(id)); + if(kind==GECODE_OPT_VARIABLE_ID){if(output_array(buffer,capacity,needed,d.variable_bounds.size()))for(std::size_t i=0;i owner;std::size_t index;}; +const O::LpEvidence& evidence(const EvidenceBox& box){ + if(!box.result.evidence)throw ApiError{GECODE_OPT_NO_EVIDENCE,"LP evidence artifact is unavailable"};return *box.result.evidence; +} +O::LpEvidenceOptions evidence_options(const gecode_opt_evidence_options_v1* input){ + O::LpEvidenceOptions out;if(!input)return out; + if(input->struct_size!=sizeof(*input)||input->reserved||input->reserved_flags)argument("invalid LP evidence options size/reserved"); + out.solve=options(&input->solve); + switch(input->request){case GECODE_OPT_EVIDENCE_AUTOMATIC:out.request=O::LpEvidenceRequest::Automatic;break; + case GECODE_OPT_EVIDENCE_PRIMAL_RAY:out.request=O::LpEvidenceRequest::PrimalRay;break; + case GECODE_OPT_EVIDENCE_FARKAS:out.request=O::LpEvidenceRequest::Farkas;break; + case GECODE_OPT_EVIDENCE_BOTH:out.request=O::LpEvidenceRequest::Both;break;default:argument("unknown LP evidence request");} + out.checks={input->recession,input->stationarity,input->minimum_improvement,input->minimum_contradiction}; + const auto limit=[](uint64_t value){if(value>std::numeric_limits::max())argument("LP evidence limit exceeds size_t");return static_cast(value);}; + out.limits.max_auxiliary_variables=limit(input->max_auxiliary_variables);out.limits.max_auxiliary_rows=limit(input->max_auxiliary_rows); + out.limits.max_auxiliary_nonzeros=limit(input->max_auxiliary_nonzeros);out.limits.max_retained_slots=limit(input->max_retained_slots); + out.limits.max_work=limit(input->max_work);out.limits.max_auxiliary_solves=limit(input->max_auxiliary_solves);out.validate();return out; +} +int32_t evidence_state(O::LpEvidenceState value){switch(value){ + case O::LpEvidenceState::NotRequested:return GECODE_OPT_EVIDENCE_NOT_REQUESTED; + case O::LpEvidenceState::Available:return GECODE_OPT_EVIDENCE_AVAILABLE; + case O::LpEvidenceState::Unavailable:return GECODE_OPT_EVIDENCE_UNAVAILABLE; + case O::LpEvidenceState::Rejected:return GECODE_OPT_EVIDENCE_REJECTED; +}argument("unknown LP evidence state");} +int32_t evidence_reason(O::LpEvidenceReason value){switch(value){ + case O::LpEvidenceReason::None:return GECODE_OPT_EVIDENCE_REASON_NONE; + case O::LpEvidenceReason::NotRequested:return GECODE_OPT_EVIDENCE_REASON_NOT_REQUESTED; + case O::LpEvidenceReason::Unsupported:return GECODE_OPT_EVIDENCE_REASON_UNSUPPORTED; + case O::LpEvidenceReason::NoFeasibleBase:return GECODE_OPT_EVIDENCE_REASON_NO_FEASIBLE_BASE; + case O::LpEvidenceReason::NoImprovingDirection:return GECODE_OPT_EVIDENCE_REASON_NO_IMPROVEMENT; + case O::LpEvidenceReason::NoContradiction:return GECODE_OPT_EVIDENCE_REASON_NO_CONTRADICTION; + case O::LpEvidenceReason::Stopped:return GECODE_OPT_EVIDENCE_REASON_STOPPED; + case O::LpEvidenceReason::InvalidBackendData:return GECODE_OPT_EVIDENCE_REASON_INVALID_BACKEND; + case O::LpEvidenceReason::FailedOriginalChecks:return GECODE_OPT_EVIDENCE_REASON_FAILED_CHECKS; + case O::LpEvidenceReason::InconsistentEvidence:return GECODE_OPT_EVIDENCE_REASON_INCONSISTENT; + case O::LpEvidenceReason::InvalidModel:return GECODE_OPT_EVIDENCE_REASON_INVALID_MODEL; + case O::LpEvidenceReason::ResourceLimit:return GECODE_OPT_EVIDENCE_REASON_RESOURCE_LIMIT; + case O::LpEvidenceReason::AllocationFailure:return GECODE_OPT_EVIDENCE_REASON_ALLOCATION; +}argument("unknown LP evidence reason");} +int32_t evidence_completion(O::LpEvidenceCompletion value){switch(value){ + case O::LpEvidenceCompletion::Complete:return GECODE_OPT_EVIDENCE_COMPLETE; + case O::LpEvidenceCompletion::Interrupted:return GECODE_OPT_EVIDENCE_INTERRUPTED; + case O::LpEvidenceCompletion::Rejected:return GECODE_OPT_EVIDENCE_ANALYSIS_REJECTED; +}argument("unknown LP evidence completion");} +int32_t evidence_phase(O::LpEvidencePhase value){switch(value){ + case O::LpEvidencePhase::FeasibleBase:return GECODE_OPT_EVIDENCE_FEASIBLE_BASE; + case O::LpEvidencePhase::Recession:return GECODE_OPT_EVIDENCE_RECESSION; + case O::LpEvidencePhase::Farkas:return GECODE_OPT_EVIDENCE_FARKAS_PHASE; +}argument("unknown LP evidence phase");} +int32_t evidence_side(O::LpEvidenceSide value){switch(value){case O::LpEvidenceSide::Lower:return GECODE_OPT_EVIDENCE_LOWER; + case O::LpEvidenceSide::Upper:return GECODE_OPT_EVIDENCE_UPPER;}argument("unknown LP evidence side");} +int32_t evidence_column(O::LpEvidenceColumnKind value){switch(value){ + case O::LpEvidenceColumnKind::SourceVariable:return GECODE_OPT_EVIDENCE_SOURCE_VARIABLE; + case O::LpEvidenceColumnKind::RowSide:return GECODE_OPT_EVIDENCE_ROW_SIDE; + case O::LpEvidenceColumnKind::VariableSide:return GECODE_OPT_EVIDENCE_VARIABLE_SIDE; +}argument("unknown LP evidence column kind");} +const O::LpEvidenceGroup& evidence_group(const EvidenceBox& box,int32_t field){ + const auto& data=evidence(box);switch(field){case GECODE_OPT_EVIDENCE_PRIMAL_GROUP:return data.primal_ray(); + case GECODE_OPT_EVIDENCE_FARKAS_GROUP:return data.farkas();default:argument("unknown LP evidence group");} +} +bool evidence_available(const EvidenceBox& box,int32_t field){return evidence_group(box,field).state==O::LpEvidenceState::Available&&!box.cleanup_stopped;} +void require_evidence_available(const EvidenceBox& box,int32_t field){if(!evidence_available(box,field))throw ApiError{GECODE_OPT_NO_EVIDENCE,"LP evidence group is not Available"};} +std::size_t evidence_slot(const O::LpEvidence& data,gecode_opt_id id){ + const auto& source=data.source(); + if(id.kind==GECODE_OPT_VARIABLE_ID){const auto v=variable(id); + if(v.model_id!=data.id()||v.id>=source.variables.size()||!source.variables[v.id].active)throw O::ModelError("LP evidence variable is foreign, absent or deleted");return static_cast(v.id);} + if(id.kind==GECODE_OPT_ROW_ID){const auto r=row(id); + if(r.model_id!=data.id()||r.id>=source.rows.size()||!source.rows[r.id].active)throw O::ModelError("LP evidence row is foreign, absent or deleted");return static_cast(r.id);} + argument("LP evidence source must be variable or row"); +} +gecode_opt_validation_info_v1 evidence_validation(const O::ValidationReport& v){return {v.valid,v.model_valid,static_cast(v.violated_globals), + v.max_bound_violation,v.max_row_violation,v.max_integrality_violation,v.max_indicator_violation,optional(v.objective)};} +gecode_opt_evidence_slot_v1 evidence_slot_record(const O::LpEvidence& data,int32_t kind,std::size_t index){ + gecode_opt_evidence_slot_v1 out{};out.struct_size=sizeof(out);const bool column=kind==GECODE_OPT_VARIABLE_ID; + const auto& source=data.source();const auto& primal=data.primal_data();const auto& farkas=data.farkas_data(); + out.source={data.id(),static_cast(index),static_cast(kind),0}; + double lower,upper; + if(column){const auto& v=at(source.variables,index);out.active=v.active;lower=v.lower;upper=v.upper; + if(out.active&&indexstruct_size=sizeof(*out);out->solve=default_options();out->request=GECODE_OPT_EVIDENCE_AUTOMATIC; + out->recession=o.checks.recession;out->stationarity=o.checks.stationarity;out->minimum_improvement=o.checks.minimum_improvement;out->minimum_contradiction=o.checks.minimum_contradiction; + out->max_auxiliary_variables=o.limits.max_auxiliary_variables;out->max_auxiliary_rows=o.limits.max_auxiliary_rows;out->max_auxiliary_nonzeros=o.limits.max_auxiliary_nonzeros; + out->max_retained_slots=o.limits.max_retained_slots;out->max_work=o.limits.max_work;out->max_auxiliary_solves=o.limits.max_auxiliary_solves) +API(analyze_lp_evidence,(gecode_opt_handle h,const gecode_opt_evidence_options_v1* input,gecode_opt_handle* output), + const auto start=Clock::now();required(output);*output=0;auto opts=evidence_options(input);const auto seconds=opts.solve.time_limit_seconds; + auto box=std::make_shared();{auto source=snapshot(h);account_preparation(opts.solve,start);box->result=O::analyze_lp_evidence(source,opts);} + evidence_binding_checkpoint(); + std::optional stopped; + if(opts.solve.cancellation&&opts.solve.cancellation->cancelled())stopped=O::Termination::Cancelled; + else if(std::isfinite(seconds)&&std::chrono::duration(Clock::now()-start).count()>=seconds)stopped=O::Termination::TimeLimit; + if(stopped){box->cleanup_stopped=true;box->result.completion=O::LpEvidenceCompletion::Interrupted;box->result.stop_reason=stopped; + box->result.message="Whole LP evidence binding allowance stopped during input cleanup";} + box->result.elapsed_seconds=std::chrono::duration(Clock::now()-start).count();*output=put(Kind::LpEvidenceResult,std::move(box))) +API(lp_evidence_destroy,(gecode_opt_handle h),destroy(h,Kind::LpEvidenceResult)) +API(lp_evidence_info,(gecode_opt_handle h,gecode_opt_evidence_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::LpEvidenceResult);const auto& r=b->result; + *out={};out->struct_size=sizeof(*out);out->model_id=r.model_id;out->revision=r.revision;out->has_evidence=bool(r.evidence); + out->completion=evidence_completion(r.completion);out->has_stop_reason=r.stop_reason.has_value();if(r.stop_reason)out->stop_reason=termination(*r.stop_reason); + if(r.evidence){out->row_slots=r.evidence->source().rows.size();out->column_slots=r.evidence->source().variables.size();out->stage_count=r.evidence->stages().size();} + out->attempted_calls=r.attempted_calls;out->work=r.work;out->elapsed_seconds=r.elapsed_seconds) +API(lp_evidence_group,(gecode_opt_handle h,int32_t field,gecode_opt_evidence_group_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::LpEvidenceResult);const auto& g=evidence_group(*b,field); + *out={sizeof(*out),0,evidence_state(g.state),evidence_reason(g.reason)}; + if(b->cleanup_stopped&&g.state!=O::LpEvidenceState::NotRequested){out->state=GECODE_OPT_EVIDENCE_UNAVAILABLE;out->reason=GECODE_OPT_EVIDENCE_REASON_STOPPED;}) +API(lp_evidence_metadata,(gecode_opt_handle h,gecode_opt_evidence_metadata_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::LpEvidenceResult);const auto& e=evidence(*b);const auto& t=e.tolerances(); + *out={sizeof(*out),0,t.recession,t.stationarity,t.minimum_improvement,t.minimum_contradiction,e.primal_tolerance()}) +API(lp_evidence_primal,(gecode_opt_handle h,gecode_opt_evidence_primal_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::LpEvidenceResult);const auto& p=evidence(*b).primal_data(); + *out={};out->struct_size=sizeof(*out);out->has_base_check=p.base_check.model_valid; + if(out->has_base_check)out->base_check=evidence_validation(p.base_check); + out->direction_scale=optional(p.direction_scale);out->normalized_objective_slope=optional(p.normalized_objective_slope); + out->max_variable_recession_violation=optional(p.max_variable_recession_violation);out->max_row_recession_violation=optional(p.max_row_recession_violation)) +API(lp_evidence_farkas,(gecode_opt_handle h,gecode_opt_evidence_farkas_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::LpEvidenceResult);const auto& f=evidence(*b).farkas_data(); + *out={sizeof(*out),0,optional(f.multiplier_scale),optional(f.contradiction_margin),optional(f.max_stationarity)}) +API(lp_evidence_slot,(gecode_opt_handle h,gecode_opt_id id,gecode_opt_evidence_slot_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::LpEvidenceResult);const auto& e=evidence(*b); + *out=evidence_slot_record(e,static_cast(id.kind),evidence_slot(e,id))) +API(lp_evidence_slots,(gecode_opt_handle h,int32_t kind,gecode_opt_evidence_slot_v1* buffer,uint64_t element_size,uint64_t capacity,uint64_t* needed), + if(element_size!=sizeof(*buffer))argument("LP evidence slot element size does not match v1"); + auto b=get(h,Kind::LpEvidenceResult);const auto& e=evidence(*b);std::size_t n; + if(kind==GECODE_OPT_VARIABLE_ID)n=e.source().variables.size();else if(kind==GECODE_OPT_ROW_ID)n=e.source().rows.size();else argument("LP evidence slots require variable/row kind"); + if(output_array(buffer,capacity,needed,n))for(std::size_t i=0;i(h,Kind::LpEvidenceResult);const auto v=variable(id);const auto& e=evidence(*b);evidence_slot(e,id); + if(field!=GECODE_OPT_EVIDENCE_BASE_VALUE&&field!=GECODE_OPT_EVIDENCE_DIRECTION_VALUE)argument("unknown LP evidence value selector"); + require_evidence_available(*b,GECODE_OPT_EVIDENCE_PRIMAL_GROUP); + *out=field==GECODE_OPT_EVIDENCE_BASE_VALUE?e.base_value(v):e.direction_value(v)) +API(lp_evidence_multiplier,(gecode_opt_handle h,gecode_opt_id id,gecode_opt_evidence_slot_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::LpEvidenceResult);const auto& e=evidence(*b);const auto slot=evidence_slot(e,id); + require_evidence_available(*b,GECODE_OPT_EVIDENCE_FARKAS_GROUP);*out=evidence_slot_record(e,static_cast(id.kind),slot)) +API(lp_evidence_text,(gecode_opt_handle h,int32_t field,char* buffer,uint64_t capacity,uint64_t* needed), + auto b=get(h,Kind::LpEvidenceResult); + if(field==GECODE_OPT_EVIDENCE_MESSAGE)output_text(b->result.message,buffer,capacity,needed); + else if(field==GECODE_OPT_EVIDENCE_BASE_CHECK_MESSAGE)output_text(evidence(*b).primal_data().base_check.message,buffer,capacity,needed); + else if(field==GECODE_OPT_EVIDENCE_PRIMAL_MESSAGE||field==GECODE_OPT_EVIDENCE_FARKAS_MESSAGE){const auto& g=evidence_group(*b,field==GECODE_OPT_EVIDENCE_PRIMAL_MESSAGE?GECODE_OPT_EVIDENCE_PRIMAL_GROUP:GECODE_OPT_EVIDENCE_FARKAS_GROUP); + output_text(b->cleanup_stopped&&g.state!=O::LpEvidenceState::NotRequested?b->result.message:g.message,buffer,capacity,needed);} + else argument("unknown LP evidence text selector")) +API(lp_evidence_copy_stage,(gecode_opt_handle h,uint64_t index,gecode_opt_handle* output), + required(output);*output=0;auto b=get(h,Kind::LpEvidenceResult);at(evidence(*b).stages(),index); + auto child=std::make_shared();child->owner=std::move(b);child->index=static_cast(index); + *output=put(Kind::LpEvidenceStage,std::move(child))) +API(lp_evidence_stage_destroy,(gecode_opt_handle h),destroy(h,Kind::LpEvidenceStage)) +API(lp_evidence_stage_info,(gecode_opt_handle h,gecode_opt_evidence_stage_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::LpEvidenceStage);const auto& s=evidence_stage(*b); + *out={};out->struct_size=sizeof(*out);out->index=b->index;out->phase=evidence_phase(s.phase);out->attempted=s.attempted; + if(s.auxiliary_model){out->private_model_id=s.auxiliary_model->model_id;out->private_revision=s.auxiliary_model->revision; + out->row_count=s.auxiliary_model->rows.size();out->column_count=s.auxiliary_model->variables.size();} + out->nonzeros=s.nonzeros;out->candidate_examined=s.candidate_examined;if(s.candidate_examined)out->check=evidence_validation(s.auxiliary_check); + out->has_raw_result=s.auxiliary_result.has_value();if(s.auxiliary_result){const auto& r=*s.auxiliary_result;auto& raw=out->raw_result; + raw.struct_size=sizeof(raw);raw.model_id=r.model_id;raw.revision=r.revision;raw.value_count=r.values.size();raw.mask_count=r.active_variables.size(); + // Raw diagnostics preserve unknown integer codes instead of asserting that + // a rejected backend result satisfies the ordinary Result contract. + raw.termination_code=static_cast(r.termination);raw.guarantee_code=static_cast(r.guarantee); + raw.reported_solution_validated=r.solution_validated;raw.reported_start_submitted=r.start_submitted;raw.elapsed_seconds=r.elapsed_seconds; + raw.objective=optional(r.objective);raw.best_bound=optional(r.best_bound);raw.absolute_gap=optional(r.absolute_gap); + raw.relative_gap=optional(r.relative_gap);raw.native_gap=optional(r.native_backend_gap);}) +API(lp_evidence_stage_columns,(gecode_opt_handle h,gecode_opt_evidence_column_v1* buffer,uint64_t element_size,uint64_t capacity,uint64_t* needed), + if(element_size!=sizeof(*buffer))argument("LP evidence column element size does not match v1"); + auto b=get(h,Kind::LpEvidenceStage);const auto& s=evidence_stage(*b);const auto& e=evidence(*b->owner); + if(output_array(buffer,capacity,needed,s.columns.size()))for(std::size_t i=0;i=s.auxiliary_model->variables.size())throw O::ModelError("Malformed auxiliary column mapping"); + out.private_variable=identifier(s.auxiliary_model->variables[i].variable); + out.source={e.id(),static_cast(c.original_slot),c.kind==O::LpEvidenceColumnKind::RowSide?GECODE_OPT_ROW_ID:GECODE_OPT_VARIABLE_ID,0};buffer[i]=out;}) +API(lp_evidence_stage_raw_values,(gecode_opt_handle h,gecode_opt_evidence_raw_value_v1* buffer,uint64_t element_size,uint64_t capacity,uint64_t* needed), + if(element_size!=sizeof(*buffer))argument("LP evidence raw value element size does not match v1"); + auto b=get(h,Kind::LpEvidenceStage);const auto& s=evidence_stage(*b); + const auto n=s.auxiliary_result?std::max(s.auxiliary_result->values.size(),s.auxiliary_result->active_variables.size()):0; + if(output_array(buffer,capacity,needed,n))for(std::size_t i=0;i(h,Kind::LpEvidenceStage);const auto& s=evidence_stage(*b);const std::string* text=nullptr; + switch(field){case GECODE_OPT_EVIDENCE_RAW_BACKEND:if(s.auxiliary_result)text=&s.auxiliary_result->backend;break; + case GECODE_OPT_EVIDENCE_RAW_BACKEND_VERSION:if(s.auxiliary_result)text=&s.auxiliary_result->backend_version;break; + case GECODE_OPT_EVIDENCE_RAW_MESSAGE:if(s.auxiliary_result)text=&s.auxiliary_result->message;break; + case GECODE_OPT_EVIDENCE_STAGE_CHECK_MESSAGE:if(s.candidate_examined)text=&s.auxiliary_check.message;break; + default:argument("unknown LP evidence stage text selector");}output_text(text?*text:std::string(),buffer,capacity,needed)) + +#ifdef GECODE_OPTIMIZE_TEST_SENSITIVITY_BINDING +extern "C" void gecode_opt_test_sensitivity_binding_checkpoint(void); +#endif +namespace { +void sensitivity_binding_checkpoint(){ +#ifdef GECODE_OPTIMIZE_TEST_SENSITIVITY_BINDING + gecode_opt_test_sensitivity_binding_checkpoint(); +#endif +} +struct SensitivityBox { + std::shared_ptr source; + O::LpSensitivityResult result; + std::size_t preparation_visits=0; + bool cleanup_stopped=false; +}; +const O::LpSensitivity& sensitivity(const SensitivityBox& box){ + if(!box.result.sensitivity)throw ApiError{GECODE_OPT_NO_SENSITIVITY,"No historical LP sensitivity artifact"}; + return *box.result.sensitivity; +} +std::size_t sensitivity_limit(uint64_t n){ + if(n>std::numeric_limits::max())argument("sensitivity limit exceeds size_t");return static_cast(n); +} +template void sensitivity_input(const T& r){ + if(r.struct_size!=sizeof(r)||r.reserved)argument("sensitivity record size/reserved mismatch"); +} +O::LpSensitivityOptions sensitivity_options(const gecode_opt_sensitivity_options_v1* input){ + O::LpSensitivityOptions o;if(!input)return o; + sensitivity_input(*input);if(input->reserved_flags)argument("sensitivity options reserved flags must be zero"); + sensitivity_input(input->checks);sensitivity_input(input->limits); + o.backend=backend(input->backend);o.time_limit_seconds=input->time_limit_seconds; + if(input->cancellation)o.cancellation=get(input->cancellation,Kind::Cancellation); + const auto& c=input->checks;o.checks.primal_feasibility=c.primal_feasibility; + o.checks.kkt={c.dual_feasibility,c.stationarity,c.complementarity,c.objective_gap}; + o.checks.system_absolute=c.system_absolute;o.checks.system_relative=c.system_relative; + const auto& l=input->limits; + o.limits={sensitivity_limit(l.max_rows),sensitivity_limit(l.max_columns),sensitivity_limit(l.max_nonzeros), + sensitivity_limit(l.max_requests),sensitivity_limit(l.max_basis_solves),sensitivity_limit(l.max_factor_entries), + sensitivity_limit(l.max_retained_slots),sensitivity_limit(l.max_work)};return o; +} +gecode_opt_sensitivity_checks_options_v1 sensitivity_checks_options(const O::LpSensitivityTolerances& c){ + return {sizeof(gecode_opt_sensitivity_checks_options_v1),0,c.primal_feasibility,c.kkt.dual_feasibility, + c.kkt.stationarity,c.kkt.complementarity,c.kkt.objective_gap,c.system_absolute,c.system_relative}; +} +int32_t sensitivity_reason(O::LpSensitivityReason r){switch(r){ + case O::LpSensitivityReason::None:return GECODE_OPT_SENSITIVITY_REASON_NONE; + case O::LpSensitivityReason::NotRequested:return GECODE_OPT_SENSITIVITY_REASON_NOT_REQUESTED; + case O::LpSensitivityReason::Unsupported:return GECODE_OPT_SENSITIVITY_REASON_UNSUPPORTED; + case O::LpSensitivityReason::NotOptimal:return GECODE_OPT_SENSITIVITY_REASON_NOT_OPTIMAL; + case O::LpSensitivityReason::NoBasis:return GECODE_OPT_SENSITIVITY_REASON_NO_BASIS; + case O::LpSensitivityReason::InvalidSource:return GECODE_OPT_SENSITIVITY_REASON_INVALID_SOURCE; + case O::LpSensitivityReason::InvalidBasis:return GECODE_OPT_SENSITIVITY_REASON_INVALID_BASIS; + case O::LpSensitivityReason::ChangedBasis:return GECODE_OPT_SENSITIVITY_REASON_CHANGED_BASIS; + case O::LpSensitivityReason::FailedReferenceChecks:return GECODE_OPT_SENSITIVITY_REASON_REFERENCE_CHECKS; + case O::LpSensitivityReason::FailedLinearSolveChecks:return GECODE_OPT_SENSITIVITY_REASON_SYSTEM_CHECKS; + case O::LpSensitivityReason::FailedIntervalChecks:return GECODE_OPT_SENSITIVITY_REASON_INTERVAL_CHECKS; + case O::LpSensitivityReason::ResourceLimit:return GECODE_OPT_SENSITIVITY_REASON_RESOURCE_LIMIT; + case O::LpSensitivityReason::Stopped:return GECODE_OPT_SENSITIVITY_REASON_STOPPED; + case O::LpSensitivityReason::AllocationFailure:return GECODE_OPT_SENSITIVITY_REASON_ALLOCATION; + case O::LpSensitivityReason::BackendFailure:return GECODE_OPT_SENSITIVITY_REASON_BACKEND; +}argument("unknown sensitivity reason");} +int32_t sensitivity_completion(O::LpSensitivityCompletion c){switch(c){ + case O::LpSensitivityCompletion::Complete:return GECODE_OPT_SENSITIVITY_COMPLETE; + case O::LpSensitivityCompletion::Partial:return GECODE_OPT_SENSITIVITY_PARTIAL; + case O::LpSensitivityCompletion::Interrupted:return GECODE_OPT_SENSITIVITY_INTERRUPTED; + case O::LpSensitivityCompletion::Rejected:return GECODE_OPT_SENSITIVITY_ANALYSIS_REJECTED; +}argument("unknown sensitivity completion");} +int32_t sensitivity_state(O::LpSensitivityState s){switch(s){ + case O::LpSensitivityState::NotRequested:return GECODE_OPT_SENSITIVITY_NOT_REQUESTED; + case O::LpSensitivityState::Available:return GECODE_OPT_SENSITIVITY_AVAILABLE; + case O::LpSensitivityState::Unavailable:return GECODE_OPT_SENSITIVITY_UNAVAILABLE; + case O::LpSensitivityState::Rejected:return GECODE_OPT_SENSITIVITY_REJECTED; +}argument("unknown sensitivity state");} +gecode_opt_id sensitivity_entity(const O::LpSensitivityEntity& e){return std::visit([](const auto& v){return identifier(v);},e);} +gecode_opt_sensitivity_request_v1 sensitivity_request(const O::LpSensitivityParameter& p){ + gecode_opt_sensitivity_request_v1 r{};r.struct_size=sizeof(r); + if(const auto* v=std::get_if(&p)){r.kind=GECODE_OPT_SENSITIVITY_OBJECTIVE;r.entity=identifier(v->variable);} + else {r.kind=GECODE_OPT_SENSITIVITY_EQUALITY_RHS;r.entity=identifier(std::get(p).row);}return r; +} +O::LpSensitivityParameter sensitivity_request(const gecode_opt_sensitivity_request_v1& r){ + sensitivity_input(r);if(r.reserved_flags)argument("sensitivity request reserved flags must be zero"); + switch(r.kind){case GECODE_OPT_SENSITIVITY_OBJECTIVE:return O::LpObjectiveParameter{variable(r.entity)}; + case GECODE_OPT_SENSITIVITY_EQUALITY_RHS:return O::LpEqualityRhsParameter{row(r.entity)}; + default:argument("unknown sensitivity request kind");} +} +gecode_opt_sensitivity_end_v1 sensitivity_end(const O::LpRangeEnd& e){ + gecode_opt_sensitivity_end_v1 r{};r.struct_size=sizeof(r); + switch(e.kind){case O::LpRangeEndKind::Finite:r.kind=GECODE_OPT_RANGE_FINITE; + if(!e.value||!std::isfinite(*e.value))throw O::ModelError("Malformed finite sensitivity endpoint");break; + case O::LpRangeEndKind::NegativeInfinity:r.kind=GECODE_OPT_RANGE_NEGATIVE_INFINITY;break; + case O::LpRangeEndKind::PositiveInfinity:r.kind=GECODE_OPT_RANGE_POSITIVE_INFINITY;break; + default:throw O::ModelError("Unknown sensitivity endpoint kind");} + if(e.kind!=O::LpRangeEndKind::Finite&&e.value)throw O::ModelError("Infinite endpoint has a numeric value");r.value=optional(e.value);return r; +} +gecode_opt_sensitivity_limiter_v1 sensitivity_limiter(const O::LpSensitivityLimiter& l){ + gecode_opt_sensitivity_limiter_v1 r{};r.struct_size=sizeof(r);r.entity=sensitivity_entity(l.entity);r.dual_condition=l.dual_condition; + switch(l.side){case O::LpSensitivitySide::Lower:r.side=GECODE_OPT_SENSITIVITY_LOWER;break; + case O::LpSensitivitySide::Upper:r.side=GECODE_OPT_SENSITIVITY_UPPER;break; + case O::LpSensitivitySide::Fixed:r.side=GECODE_OPT_SENSITIVITY_FIXED;break; + case O::LpSensitivitySide::Free:r.side=GECODE_OPT_SENSITIVITY_FREE;break; + default:throw O::ModelError("Unknown sensitivity limiter side");}return r; +} +gecode_opt_sensitivity_entry_v1 sensitivity_entry(const SensitivityBox& box,const O::LpSensitivityParameter& p,const O::LpSensitivityEntry* entry){ + gecode_opt_sensitivity_entry_v1 r{};r.struct_size=sizeof(r);r.request=sensitivity_request(p);r.group.struct_size=sizeof(r.group); + r.lower.struct_size=sizeof(r.lower);r.upper.struct_size=sizeof(r.upper);r.lower_limiter.struct_size=sizeof(r.lower_limiter); + r.upper_limiter.struct_size=sizeof(r.upper_limiter);r.checks.struct_size=sizeof(r.checks); + r.group.state=GECODE_OPT_SENSITIVITY_NOT_REQUESTED;r.group.reason=GECODE_OPT_SENSITIVITY_REASON_NOT_REQUESTED; + if(!entry)return r;r.requested=1;r.index=static_cast(entry-sensitivity(box).entries().data()); + r.group.state=sensitivity_state(entry->group.state);r.group.reason=sensitivity_reason(entry->group.reason); + if(box.cleanup_stopped){r.group.state=GECODE_OPT_SENSITIVITY_UNAVAILABLE;r.group.reason=GECODE_OPT_SENSITIVITY_REASON_STOPPED;return r;} + if(!entry->interval){if(entry->group.state==O::LpSensitivityState::Available)throw O::ModelError("Available sensitivity has no interval");return r;} + if(entry->group.state!=O::LpSensitivityState::Available)throw O::ModelError("Sensitivity interval is present without Available state"); + const auto& v=*entry->interval;r.has_interval=1;r.anchor=v.anchor;r.lower=sensitivity_end(v.lower);r.upper=sensitivity_end(v.upper); + r.objective_slope=optional(v.objective_slope);r.has_lower_limiter=v.lower_limiter.has_value();r.has_upper_limiter=v.upper_limiter.has_value(); + if(v.lower_limiter)r.lower_limiter=sensitivity_limiter(*v.lower_limiter);if(v.upper_limiter)r.upper_limiter=sensitivity_limiter(*v.upper_limiter); + r.checks.inequalities=v.checks.inequalities;r.checks.accepted=v.checks.accepted;r.checks.lower_direction_checked=v.checks.lower_direction_checked; + r.checks.upper_direction_checked=v.checks.upper_direction_checked;r.checks.max_endpoint_violation=optional(v.checks.max_endpoint_violation);return r; +} +std::optional sensitivity_stop(const std::shared_ptr& token,double seconds,Clock::time_point start){ + if(token&&token->cancelled())return O::Termination::Cancelled; + if(std::chrono::duration(Clock::now()-start).count()>=seconds)return O::Termination::TimeLimit;return {}; +} +const char* sensitivity_cleanup_message="Whole LP sensitivity binding allowance stopped during input cleanup"; +void sensitivity_failure(SensitivityBox& b,O::LpSensitivityReason reason,const char* message,std::optional stop={}){ + b.result.reason=reason;b.result.message=message;b.result.stop_reason=stop; + b.result.completion=stop?O::LpSensitivityCompletion::Interrupted:O::LpSensitivityCompletion::Rejected; +} +} +API(sensitivity_options_default,(gecode_opt_sensitivity_options_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));O::LpSensitivityOptions o;*out={};out->struct_size=sizeof(*out);out->backend=GECODE_OPT_AUTO; + out->time_limit_seconds=o.time_limit_seconds;out->checks=sensitivity_checks_options(o.checks);const auto& l=o.limits; + out->limits={sizeof(out->limits),0,l.max_rows,l.max_columns,l.max_nonzeros,l.max_requests,l.max_basis_solves,l.max_factor_entries,l.max_retained_slots,l.max_work}) +API(analyze_lp_sensitivity,(gecode_opt_handle h,const gecode_opt_sensitivity_options_v1* input,gecode_opt_handle* output), + const auto start=Clock::now();required(output);*output=0;auto source=get(h,Kind::LpObservedResult); + auto b=std::make_shared();b->source=std::move(source);b->result.model_id=b->source->result.model_id;b->result.revision=b->source->result.revision; + double seconds=std::numeric_limits::infinity();std::shared_ptr token;bool valid_options=false; + {auto o=sensitivity_options(input);seconds=o.time_limit_seconds;token=o.cancellation; + const auto n=input?count(input->request_count,input->requests):0;length(n); + try {o.checks.validate();if(std::isnan(seconds)||seconds<0)throw O::ModelError("Invalid sensitivity time limit"); + if(!n)throw O::ModelError("Sensitivity requires a nonempty explicit parameter list");valid_options=true; + } catch(const O::ModelError& e){sensitivity_failure(*b,O::LpSensitivityReason::InvalidSource,e.what());} + if(valid_options){ + if(const auto stop=sensitivity_stop(token,seconds,start))sensitivity_failure(*b,O::LpSensitivityReason::Stopped,"Sensitivity binding input preparation stopped",stop); + else if(n>o.limits.max_requests)sensitivity_failure(*b,O::LpSensitivityReason::ResourceLimit,"Sensitivity request limit exceeded before marshalling",O::Termination::MemoryLimit); + else if(n>o.limits.max_work)sensitivity_failure(*b,O::LpSensitivityReason::ResourceLimit,"Sensitivity work limit exceeded before marshalling",O::Termination::IterationLimit); + else {o.parameters.reserve(n); + for(std::size_t i=0;ipreparation_visits;o.parameters.push_back(sensitivity_request(input->requests[i]));} + if(o.parameters.size()==n){o.limits.max_work-=b->preparation_visits; + if(std::isfinite(seconds))o.time_limit_seconds=std::max(0.0,seconds-std::chrono::duration(Clock::now()-start).count()); + b->result=O::analyze_lp_sensitivity(*b->source,o);} + } + } + } + sensitivity_binding_checkpoint(); + if(valid_options)if(const auto stop=sensitivity_stop(token,seconds,start)){b->cleanup_stopped=true; + sensitivity_failure(*b,O::LpSensitivityReason::Stopped,sensitivity_cleanup_message,stop);} + b->result.elapsed_seconds=std::chrono::duration(Clock::now()-start).count();*output=put(Kind::SensitivityResult,std::move(b))) +API(sensitivity_destroy,(gecode_opt_handle h),destroy(h,Kind::SensitivityResult)) +API(sensitivity_copy_source_observed,(gecode_opt_handle h,gecode_opt_handle* output), + required(output);*output=0;auto b=get(h,Kind::SensitivityResult); + *output=put(Kind::LpObservedResult,std::make_shared(*b->source))) +API(sensitivity_copy_basis,(gecode_opt_handle h,gecode_opt_handle* output), + required(output);*output=0;auto b=get(h,Kind::SensitivityResult); + if(!b->result.sensitivity||!b->result.sensitivity->basis())throw ApiError{GECODE_OPT_NO_BASIS,"No selected sensitivity basis"}; + auto out=std::make_shared();out->basis=b->result.sensitivity->basis();*output=put(Kind::Basis,std::move(out))) +API(sensitivity_info,(gecode_opt_handle h,gecode_opt_sensitivity_info_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::SensitivityResult);const auto& r=b->result;*out={};out->struct_size=sizeof(*out); + out->model_id=r.model_id;out->revision=r.revision;out->completion=sensitivity_completion(r.completion);out->reason=sensitivity_reason(r.reason); + out->has_stop_reason=r.stop_reason.has_value();if(r.stop_reason)out->stop_reason=termination(*r.stop_reason);out->guarantee=GECODE_OPT_NUMERICAL; + out->has_sensitivity=bool(r.sensitivity);out->elapsed_seconds=r.elapsed_seconds; + if(r.sensitivity){const auto& s=*r.sensitivity;out->has_basis=bool(s.basis());out->entry_count=s.entries().size();out->factor_order_count=s.factor_order().size(); + out->row_slots=s.active_rows().size();out->column_slots=s.active_columns().size();}) +API(sensitivity_work,(gecode_opt_handle h,gecode_opt_sensitivity_work_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::SensitivityResult);const auto& w=b->result.work; + *out={sizeof(*out),0,w.factor_setup_attempted,0,w.basis_solves,w.coordinator_visits,w.retained_slots,b->preparation_visits}) +API(sensitivity_checks_options,(gecode_opt_handle h,gecode_opt_sensitivity_checks_options_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::SensitivityResult);*out=sensitivity_checks_options(sensitivity(*b).tolerances())) +API(sensitivity_reference_checks,(gecode_opt_handle h,gecode_opt_sensitivity_reference_checks_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::SensitivityResult);const auto& c=sensitivity(*b).checks();*out={};out->struct_size=sizeof(*out); + out->primal=evidence_validation(c.primal);out->basis_point_matches=c.basis_point_matches;out->max_point_difference=optional(c.max_point_difference); + out->max_system_residual=optional(c.max_system_residual);out->max_scaled_system_residual=optional(c.max_scaled_system_residual); + const auto& k=c.kkt;auto& r=out->kkt;r.struct_size=sizeof(r);r.primal_valid=k.primal_valid;r.dual_signs_valid=k.dual_signs_valid; + r.stationarity_valid=k.stationarity_valid;r.complementarity_valid=k.complementarity_valid;r.gap_valid=k.gap_valid;r.accepted=k.accepted; + r.max_dual_sign_violation=optional(k.max_dual_sign_violation);r.max_stationarity=optional(k.max_stationarity); + r.max_complementarity=optional(k.max_complementarity);r.dual_objective_estimate=optional(k.dual_objective_estimate);r.normalized_gap=optional(k.normalized_gap)) +API(sensitivity_entry,(gecode_opt_handle h,uint64_t index,gecode_opt_sensitivity_entry_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::SensitivityResult);const auto& e=at(sensitivity(*b).entries(),index);*out=sensitivity_entry(*b,e.parameter,&e)) +API(sensitivity_entries,(gecode_opt_handle h,gecode_opt_sensitivity_entry_v1* buffer,uint64_t element_size,uint64_t capacity,uint64_t* needed), + if(element_size!=sizeof(*buffer))argument("Sensitivity entry element size mismatch");auto b=get(h,Kind::SensitivityResult);const auto& es=sensitivity(*b).entries(); + if(output_array(buffer,capacity,needed,es.size()))for(std::size_t i=0;i(h,Kind::SensitivityResult);const auto v=variable(id); + *out=sensitivity_entry(*b,O::LpObjectiveParameter{v},sensitivity(*b).objective(v))) +API(sensitivity_equality_rhs,(gecode_opt_handle h,gecode_opt_id id,gecode_opt_sensitivity_entry_v1* out,uint64_t size), + size_check(out,size,sizeof(*out));auto b=get(h,Kind::SensitivityResult);const auto r=row(id); + *out=sensitivity_entry(*b,O::LpEqualityRhsParameter{r},sensitivity(*b).equality_rhs(r))) +API(sensitivity_factor_order,(gecode_opt_handle h,gecode_opt_id* buffer,uint64_t capacity,uint64_t* needed), + auto b=get(h,Kind::SensitivityResult);const auto& order=sensitivity(*b).factor_order(); + if(output_array(buffer,capacity,needed,order.size()))for(std::size_t i=0;i(h,Kind::SensitivityResult);const auto& s=sensitivity(*b); + if(kind!=GECODE_OPT_VARIABLE_ID&&kind!=GECODE_OPT_ROW_ID)argument("Sensitivity mask must select Variable or Row"); + const auto& mask=kind==GECODE_OPT_VARIABLE_ID?s.active_columns():s.active_rows(); + if(output_array(buffer,capacity,needed,mask.size()))for(std::size_t i=0;i(h,Kind::SensitivityResult);const std::string* value=nullptr; + if(field!=GECODE_OPT_SENSITIVITY_ENTRY_MESSAGE&&field!=GECODE_OPT_SENSITIVITY_INTERVAL_MESSAGE&&index)argument("Sensitivity text index must be zero"); + switch(field){case GECODE_OPT_SENSITIVITY_MESSAGE:value=&b->result.message;break; + case GECODE_OPT_SENSITIVITY_BACKEND_VERSION:value=&sensitivity(*b).backend_version();break; + case GECODE_OPT_SENSITIVITY_PRIMAL_MESSAGE:value=&sensitivity(*b).checks().primal.message;break; + case GECODE_OPT_SENSITIVITY_KKT_MESSAGE:value=&sensitivity(*b).checks().kkt.message;break; + case GECODE_OPT_SENSITIVITY_ENTRY_MESSAGE:case GECODE_OPT_SENSITIVITY_INTERVAL_MESSAGE:{const auto& e=at(sensitivity(*b).entries(),index); + if(b->cleanup_stopped){output_text(sensitivity_cleanup_message,buffer,capacity,needed);return;} + if(field==GECODE_OPT_SENSITIVITY_ENTRY_MESSAGE)value=&e.group.message;else if(e.interval)value=&e.interval->checks.message;break;} + default:argument("Unknown sensitivity text field");}output_text(value?*value:std::string(),buffer,capacity,needed)) +#undef API diff --git a/gecode/optimize/c_api.h b/gecode/optimize/c_api.h new file mode 100644 index 0000000000..df350ad6d4 --- /dev/null +++ b/gecode/optimize/c_api.h @@ -0,0 +1,788 @@ +/* Version 1 C ABI. C99; no C++ headers or borrowed solver storage. */ +#ifndef GECODE_OPTIMIZE_C_API_H +#define GECODE_OPTIMIZE_C_API_H +#include +#if defined(_WIN32) +# if defined(GECODE_OPT_C_API_EXPORTS) +# define GECODE_OPT_API __declspec(dllexport) +# else +# define GECODE_OPT_API __declspec(dllimport) +# endif +#else +# define GECODE_OPT_API __attribute__((visibility("default"))) +#endif +#ifdef __cplusplus +# define GECODE_OPT_NOEXCEPT noexcept +extern "C" { +#else +# define GECODE_OPT_NOEXCEPT +#endif + +typedef uint64_t gecode_opt_handle; +enum { GECODE_OPT_VARIABLE_ID=1, GECODE_OPT_ROW_ID=2, + GECODE_OPT_GLOBAL_ID=3, GECODE_OPT_INDICATOR_ID=4 }; +typedef struct { uint64_t model_id; uint64_t slot; uint32_t kind, reserved; } gecode_opt_id; +typedef struct { gecode_opt_id variable; double coefficient; } gecode_opt_term; +typedef struct { gecode_opt_id variable; double value; } gecode_opt_start; + +enum { GECODE_OPT_OK=0, GECODE_OPT_INVALID_ARGUMENT=1, GECODE_OPT_INVALID_HANDLE=2, + GECODE_OPT_MODEL_ERROR=3, GECODE_OPT_OUT_OF_MEMORY=4, GECODE_OPT_INTERNAL_ERROR=5, + GECODE_OPT_BUFFER_TOO_SMALL=6, GECODE_OPT_NO_SOLUTION=7, GECODE_OPT_NO_OBSERVATIONS=8, GECODE_OPT_NO_BASIS=9 }; +enum { GECODE_OPT_CONTINUOUS=0, GECODE_OPT_INTEGER=1, GECODE_OPT_BINARY=2, + GECODE_OPT_SEMI_CONTINUOUS=3, GECODE_OPT_SEMI_INTEGER=4 }; +enum { GECODE_OPT_MINIMIZE=0, GECODE_OPT_MAXIMIZE=1 }; +enum { GECODE_OPT_AUTO=0, GECODE_OPT_HIGHS=1, GECODE_OPT_NATIVE=2 }; +enum { GECODE_OPT_NUMERICAL=0, GECODE_OPT_EXACT=1, GECODE_OPT_CERTIFIED=2 }; +enum { GECODE_OPT_UNKNOWN=0, GECODE_OPT_OPTIMAL=1, GECODE_OPT_INFEASIBLE=2, + GECODE_OPT_UNBOUNDED=3, GECODE_OPT_INFEASIBLE_OR_UNBOUNDED=4, GECODE_OPT_TIME_LIMIT=5, + GECODE_OPT_NODE_LIMIT=6, GECODE_OPT_MEMORY_LIMIT=7, GECODE_OPT_ITERATION_LIMIT=8, + GECODE_OPT_SOLUTION_LIMIT=9, GECODE_OPT_OBJECTIVE_LIMIT=10, GECODE_OPT_CANCELLED=11, + GECODE_OPT_NUMERICAL_FAILURE=12, GECODE_OPT_UNSUPPORTED=13, GECODE_OPT_INVALID_MODEL=14, + GECODE_OPT_BACKEND_ERROR=15 }; +enum { GECODE_OPT_OBJECTIVE=0, GECODE_OPT_BEST_BOUND=1, GECODE_OPT_ABSOLUTE_GAP=2, + GECODE_OPT_RELATIVE_GAP=3, GECODE_OPT_NATIVE_GAP=4 }; +enum { GECODE_OPT_BACKEND_NAME=0, GECODE_OPT_BACKEND_VERSION=1, GECODE_OPT_MESSAGE=2 }; + +typedef struct { + uint64_t struct_size; + int32_t backend, guarantee, threads, random_seed; + double time_limit_seconds, relative_gap, absolute_gap; + double feasibility_tolerance, integrality_tolerance; + uint64_t node_limit; + int32_t has_node_limit, reserved; + gecode_opt_handle cancellation; /* zero means no cancellation token */ + const gecode_opt_start* primal_start; + uint64_t primal_start_count; +} gecode_opt_options_v1; + +typedef struct { + uint64_t model_id, revision, variable_slots; + int32_t termination, guarantee, has_solution, solution_validated, start_submitted, reserved; + double elapsed_seconds; +} gecode_opt_result_info_v1; +typedef struct { + uint64_t solve_calls, model_loads, incremental_updates, unchanged_models; + uint64_t basis_warm_starts, incumbent_starts; +} gecode_opt_session_statistics_v1; + +/* Atomic bulk input records; exact struct_size and zero reserved required. + * Names are NUL-terminated UTF-8 (NULL means empty), copied during the call. + * CSR uses independent counts; names_count is zero or the row count. */ +typedef struct { + uint64_t struct_size; + int32_t type, reserved; + double lower, upper; + const char* name; +} gecode_opt_variable_spec_v1; +typedef struct { + uint64_t struct_size, reserved; + const gecode_opt_term* terms; + uint64_t term_count; + double lower, upper; + const char* name; +} gecode_opt_row_spec_v1; +typedef struct { + uint64_t struct_size, reserved; + const gecode_opt_id* columns; uint64_t columns_count; + const uint64_t* row_start; uint64_t row_start_count; + const uint64_t* column; uint64_t column_count; + const double* coefficient; uint64_t coefficient_count; + const double* lower; uint64_t lower_count; + const double* upper; uint64_t upper_count; + const char* const* names; uint64_t names_count; +} gecode_opt_sparse_row_batch_v1; + +/* Additive workflow records. No existing ABI-1 record changes layout. */ +typedef struct { int32_t present, reserved; double value; } gecode_opt_optional_number_v1; +enum { GECODE_OPT_POOL_INCOMPLETE=0, GECODE_OPT_POOL_REQUESTED_LIMIT=1, GECODE_OPT_POOL_EXHAUSTED=2 }; +enum { GECODE_OPT_RELAX_LOWER=0, GECODE_OPT_RELAX_UPPER=1 }; +enum { GECODE_OPT_REPAIR_MINIMUM_VIOLATION=0, GECODE_OPT_REPAIR_VIOLATION=1, + GECODE_OPT_REPAIR_ORIGINAL_OBJECTIVE=2 }; +enum { GECODE_OPT_REPAIR_MESSAGE=0, GECODE_OPT_REPAIR_WORKFLOW_MESSAGE=1, + GECODE_OPT_REPAIR_VALIDATION_MESSAGE=2, GECODE_OPT_REPAIR_ITEM_NAME=3, + GECODE_OPT_REPAIR_STAGE_NAME=4 }; +typedef struct { + uint64_t struct_size; + gecode_opt_options_v1 solve; + uint64_t max_solutions; + int32_t has_projection, reserved; + const gecode_opt_id* projection; + uint64_t projection_count; +} gecode_opt_pool_options_v1; +typedef struct { + uint64_t model_id, revision, projection_count, entry_count, attempt_count, ranked_prefix; + int32_t termination, completion, guarantee, reserved; + double elapsed_seconds; +} gecode_opt_pool_info_v1; +typedef struct { uint64_t projection_count; int32_t rank_established, reserved; } gecode_opt_pool_entry_info_v1; +typedef struct { + int32_t termination, guarantee, candidate_accepted, rank_established; + gecode_opt_optional_number_v1 objective, remaining_bound; +} gecode_opt_pool_attempt_info_v1; +typedef struct { + gecode_opt_id source; /* active Variable or Row identity */ + int32_t side, reserved; + double penalty; +} gecode_opt_relaxation_selection_v1; +typedef struct { + uint64_t struct_size; + gecode_opt_options_v1 solve; + const gecode_opt_relaxation_selection_v1* selections; + uint64_t selection_count; + int32_t optimize_original_objective, reserved; +} gecode_opt_repair_options_v1; +typedef struct { + uint64_t source_model_id, source_revision, private_model_id, private_revision; + uint64_t variable_slots, item_count, stage_count, completed_stages; + int32_t termination, guarantee, has_private_model, has_repair; + int32_t minimum_violation_established, original_objective_optimized; + int32_t workflow_termination, workflow_guarantee; + double elapsed_seconds, workflow_elapsed_seconds; +} gecode_opt_repair_info_v1; +typedef struct { + gecode_opt_id source, slack, penalty_row; + int32_t side, reserved; + double original_bound, penalty; + gecode_opt_optional_number_v1 activity, violation, weighted_violation, slack_value; +} gecode_opt_repair_item_info_v1; +typedef struct { + uint64_t index; + int32_t completed, reserved; + gecode_opt_optional_number_v1 retention_bound; +} gecode_opt_repair_stage_info_v1; +typedef struct { + int32_t valid, model_valid; + uint64_t violated_globals; + double max_bound_violation, max_row_violation, max_integrality_violation, max_indicator_violation; + gecode_opt_optional_number_v1 objective; +} gecode_opt_validation_info_v1; + +/* Distinct finite-box continuous QP input. Exact sizes and zero reserved fields + * are required. All square/term/name data is copied before an atomic objective + * replacement. Minimize adds positive weighted squares; maximize subtracts them. */ +typedef struct { + uint64_t struct_size, reserved; + const gecode_opt_term* terms; + uint64_t term_count; + double offset, weight; + const char* name; +} gecode_opt_weighted_square_v1; +typedef struct { + uint64_t struct_size, reserved; + gecode_opt_options_v1 solve; + uint64_t iteration_limit, max_auxiliary_variables, max_lifted_nonzeros; + double stationarity_tolerance, complementarity_tolerance, optimality_tolerance; +} gecode_opt_quadratic_options_v1; +typedef struct { + gecode_opt_result_info_v1 result; + uint64_t qp_iterations; + double regularization; +} gecode_opt_quadratic_info_v1; +typedef struct { + int32_t primal_valid, objective_valid, kkt_available, kkt_valid, bound_valid, reserved; + gecode_opt_optional_number_v1 max_stationarity, max_complementarity; + gecode_opt_optional_number_v1 original_objective, normalized_lower_bound, gap_upper_bound; + uint64_t square_count, gradient_slots; /* complete arrays only if objective_valid */ +} gecode_opt_quadratic_checks_v1; +enum { GECODE_OPT_QP_VENDOR_OBJECTIVE=5, GECODE_OPT_QP_VENDOR_DUAL_ESTIMATE=6 }; +enum { GECODE_OPT_QP_CHECK_MESSAGE=3 }; +enum { GECODE_OPT_QP_SQUARE_RESIDUALS=0, GECODE_OPT_QP_ORIGINAL_GRADIENT=1 }; + +/* Tokens are process-local, typed, never reused. Destroy invalidates a token; + * already-running operations retain shared ownership and may complete. + * Every nonnull pointer must identify valid accessible C storage. Arrays are + * copied before solving. NULL arrays are accepted only with zero count. + * Outputs are caller-owned. API errors differ from solver termination codes. + * last_error is thread-local, valid until the next API call on that thread. + */ +GECODE_OPT_API uint32_t gecode_opt_v1_abi_version(void) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API const char* gecode_opt_v1_last_error(void) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_options_default(gecode_opt_options_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_capabilities(int32_t backend, int32_t* available, int32_t* lp, int32_t* mip) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_create(gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_destroy(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_identity(gecode_opt_handle, uint64_t* model_id, uint64_t* revision) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_read(const char* filename, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_write(gecode_opt_handle, const char* filename) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_add_variable(gecode_opt_handle, int32_t type, double lower, double upper, const char* name, gecode_opt_id*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_add_row(gecode_opt_handle, const gecode_opt_term*, uint64_t count, double lower, double upper, const char* name, gecode_opt_id*) GECODE_OPT_NOEXCEPT; +/* These mutations have NO size-query mode. Output capacity must cover the + * input count (CSR lower_count) before posting. NULL output is valid only for + * zero capacity/count. Errors leave model/revision/output elements unchanged. + * Nonempty success advances revision once; valid empty batches do not. + * Output storage must not overlap input arrays/records/names. */ +GECODE_OPT_API int32_t gecode_opt_v1_model_add_variables(gecode_opt_handle, const gecode_opt_variable_spec_v1*, uint64_t count, gecode_opt_id* output, uint64_t capacity) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_add_rows(gecode_opt_handle, const gecode_opt_row_spec_v1*, uint64_t count, gecode_opt_id* output, uint64_t capacity) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_add_rows_sparse(gecode_opt_handle, const gecode_opt_sparse_row_batch_v1*, gecode_opt_id* output, uint64_t capacity) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_set_objective(gecode_opt_handle, const gecode_opt_term*, uint64_t count, int32_t sense, double offset) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_set_variable_bounds(gecode_opt_handle, gecode_opt_id, double lower, double upper) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_set_row_bounds(gecode_opt_handle, gecode_opt_id, double lower, double upper) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_set_coefficient(gecode_opt_handle, gecode_opt_id row, gecode_opt_id variable, double value) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_set_objective_coefficient(gecode_opt_handle, gecode_opt_id variable, double value) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_set_objective_offset(gecode_opt_handle, double offset) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_set_variable_name(gecode_opt_handle, gecode_opt_id, const char*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_set_row_name(gecode_opt_handle, gecode_opt_id, const char*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_remove_variable(gecode_opt_handle, gecode_opt_id) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_remove_row(gecode_opt_handle, gecode_opt_id) GECODE_OPT_NOEXCEPT; +/* Typed global records preserve aliases and explicit integer index bases. + * Table values are row-major; value_count must equal arity * tuple_count, + * including arity zero. Cumulative arrays have independently checked counts. */ +GECODE_OPT_API int32_t gecode_opt_v1_model_add_all_different(gecode_opt_handle, const gecode_opt_id* variables, uint64_t count, const char* name, gecode_opt_id*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_add_element(gecode_opt_handle, gecode_opt_id index, const gecode_opt_id* elements, uint64_t count, gecode_opt_id result, int64_t index_base, const char* name, gecode_opt_id*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_add_table(gecode_opt_handle, const gecode_opt_id* variables, uint64_t arity, const int64_t* values, uint64_t value_count, uint64_t tuple_count, const char* name, gecode_opt_id*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_add_cumulative(gecode_opt_handle, const gecode_opt_id* starts, uint64_t count, const int64_t* durations, uint64_t duration_count, const int64_t* heights, uint64_t height_count, int64_t capacity, const char* name, gecode_opt_id*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_add_circuit(gecode_opt_handle, const gecode_opt_id* successors, uint64_t count, int64_t index_base, const char* name, gecode_opt_id*) GECODE_OPT_NOEXCEPT; +/* Sparse deterministic automaton. States are zero based, symbols are signed + * exact integers within +/-2^53. Missing edges reject; duplicate (from,symbol) + * keys are invalid, even identical edges. Final-state duplicates are harmless. + * Empty words accept iff initial_state is final; repeated variables retain + * equality. All input arrays are copied. element_size and each struct_size + * must equal sizeof(gecode_opt_regular_transition_v1); reserved must be zero. + * A failed addition leaves the model unchanged and clears the output ID. */ +typedef struct { + uint64_t struct_size, reserved, from; + int64_t symbol; + uint64_t to; +} gecode_opt_regular_transition_v1; +GECODE_OPT_API int32_t gecode_opt_v1_model_add_regular(gecode_opt_handle, const gecode_opt_id* variables, uint64_t variable_count, + uint64_t state_count, uint64_t initial_state, const gecode_opt_regular_transition_v1* transitions, uint64_t transition_count, + uint64_t transition_element_size, const uint64_t* final_states, uint64_t final_count, const char* name, gecode_opt_id*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_remove_global(gecode_opt_handle, gecode_opt_id) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_set_global_name(gecode_opt_handle, gecode_opt_id, const char*) GECODE_OPT_NOEXCEPT; +/* Indicator posting is one atomic model edit. has_gate is 0/1 and gate is + * zeroed when absent. Generated rows remain private and protected. Removing + * the indicator removes its rows; a generated gate remains until removed. */ +GECODE_OPT_API int32_t gecode_opt_v1_model_add_indicator(gecode_opt_handle, gecode_opt_id activator, int32_t active_value, const gecode_opt_term*, uint64_t count, double lower, double upper, const char* name, gecode_opt_id* indicator, int32_t* has_gate, gecode_opt_id* gate) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_remove_indicator(gecode_opt_handle, gecode_opt_id) GECODE_OPT_NOEXCEPT; +/* Atomic ordinary-row posting; no helper handle/group removal is exposed. + * AND(empty)=true and OR(empty)=false; inputs/result must have Binary type. */ +GECODE_OPT_API int32_t gecode_opt_v1_model_add_boolean_and(gecode_opt_handle, gecode_opt_id result, const gecode_opt_id* inputs, uint64_t count, const char* name) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_model_add_boolean_or(gecode_opt_handle, gecode_opt_id result, const gecode_opt_id* inputs, uint64_t count, const char* name) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_cancellation_create(gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_cancellation_cancel(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_cancellation_is_cancelled(gecode_opt_handle, int32_t*) GECODE_OPT_NOEXCEPT; +/* Independent registry owner sharing the same thread-safe cancellation state. */ +GECODE_OPT_API int32_t gecode_opt_v1_cancellation_copy(gecode_opt_handle, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_cancellation_destroy(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_session_create(gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_session_destroy(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_session_reset(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_session_statistics(gecode_opt_handle, gecode_opt_session_statistics_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_solve(gecode_opt_handle model, const gecode_opt_options_v1*, gecode_opt_handle* result) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_session_solve(gecode_opt_handle session, gecode_opt_handle model, const gecode_opt_options_v1*, gecode_opt_handle* result) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_result_destroy(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_result_info(gecode_opt_handle, gecode_opt_result_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +/* present=0 and value=0 represent absence, never an ambiguous NaN sentinel. */ +GECODE_OPT_API int32_t gecode_opt_v1_result_number(gecode_opt_handle, int32_t field, int32_t* present, double* value) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_result_value(gecode_opt_handle, gecode_opt_id variable, double* value) GECODE_OPT_NOEXCEPT; +/* Query with NULL buffer(s), capacity0. required includes the string NUL. + * Insufficient capacity writes no array/string elements. Deleted/missing + * values are zero with distinct active/present masks. */ +GECODE_OPT_API int32_t gecode_opt_v1_result_values(gecode_opt_handle, double* values, uint8_t* active, uint8_t* present, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_result_text(gecode_opt_handle, int32_t field, char* buffer, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; + +/* Original-coordinate continuous-LP observations. These additive symbols keep + * ABI version 1. No observed-result token implicitly converts to Result; use + * copy_result for a new independent owner. Group availability is independent + * of primal termination. Basis/duals are numerical observations, not proofs. + * NULL options use defaults; outer/nested sizes must match exactly and reserved + * fields must be zero. Output size arguments are exact sizeof(record); returned + * struct_size is populated and all reserved fields are zero. Bulk arrays include + * inactive historical slots; individual row/column lookups reject tombstones. + * Absent observation owners return NO_OBSERVATIONS for observation accessors. + * Buffer queries use NULL/capacity zero; short buffers receive no elements. */ +enum { GECODE_OPT_LP_NOT_REQUESTED=0, GECODE_OPT_LP_AVAILABLE=1, + GECODE_OPT_LP_UNAVAILABLE=2, GECODE_OPT_LP_REJECTED=3 }; +enum { GECODE_OPT_LP_REASON_NONE=0, GECODE_OPT_LP_REASON_NOT_REQUESTED=1, + GECODE_OPT_LP_REASON_UNSUPPORTED=2, GECODE_OPT_LP_REASON_NO_BACKEND_SOLVE=3, + GECODE_OPT_LP_REASON_NO_PRIMAL_POINT=4, GECODE_OPT_LP_REASON_NOT_OPTIMAL=5, + GECODE_OPT_LP_REASON_NO_DUAL_POINT=6, GECODE_OPT_LP_REASON_NO_BASIS=7, + GECODE_OPT_LP_REASON_ELIDED_CONSTANT_ROWS=8, GECODE_OPT_LP_REASON_INTERRUPTED=9, + GECODE_OPT_LP_REASON_INVALID_BACKEND_DATA=10, GECODE_OPT_LP_REASON_FAILED_CHECKS=11, + GECODE_OPT_LP_REASON_ALLOCATION_FAILURE=12, GECODE_OPT_LP_REASON_INVALID_MODEL=13 }; +enum { GECODE_OPT_LP_BASIS_LOWER=0, GECODE_OPT_LP_BASIS_BASIC=1, + GECODE_OPT_LP_BASIS_UPPER=2, GECODE_OPT_LP_BASIS_ZERO=3, + GECODE_OPT_LP_BASIS_NONBASIC_UNSPECIFIED=4 }; +enum { GECODE_OPT_LP_DUAL_NONE=0, GECODE_OPT_LP_DUAL_BACKEND=1, + GECODE_OPT_LP_DUAL_DERIVED_CONSTANT_ROW=2 }; +enum { GECODE_OPT_LP_PRIMAL_ROWS=0, GECODE_OPT_LP_DUAL_POINT=1, GECODE_OPT_LP_BASIS=2 }; +enum { GECODE_OPT_LP_BACKEND_NAME=0, GECODE_OPT_LP_BACKEND_VERSION=1, + GECODE_OPT_LP_PRIMAL_MESSAGE=2, GECODE_OPT_LP_DUAL_MESSAGE=3, + GECODE_OPT_LP_BASIS_MESSAGE=4, GECODE_OPT_LP_CHECK_MESSAGE=5 }; +enum { GECODE_OPT_LP_CAP_BACKEND=0, GECODE_OPT_LP_CAP_VERSION=1, GECODE_OPT_LP_CAP_LIMITATION=2 }; +typedef struct { + uint64_t struct_size, reserved; + gecode_opt_options_v1 solve; + int32_t duals, basis; + double dual_feasibility, stationarity, complementarity, objective_gap; +} gecode_opt_lp_options_v1; +typedef struct { + uint64_t struct_size, reserved; + int32_t available, duals, basis_export, reserved_flags; + uint64_t limitation_count; +} gecode_opt_lp_capabilities_v1; +typedef struct { + uint64_t struct_size, reserved; + gecode_opt_result_info_v1 result; + int32_t has_observations, reserved_flags; + uint64_t model_id, revision, row_slots, column_slots; +} gecode_opt_lp_info_v1; +typedef struct { + uint64_t struct_size, reserved; + double dual_feasibility, stationarity, complementarity, objective_gap, primal_check_tolerance; + gecode_opt_optional_number_v1 backend_primal_tolerance, backend_dual_tolerance; +} gecode_opt_lp_metadata_v1; +typedef struct { uint64_t struct_size, reserved; int32_t state, reason; } gecode_opt_lp_group_v1; +typedef struct { + uint64_t struct_size, reserved; + int32_t active, dual_source, has_basis, basis; + gecode_opt_optional_number_v1 activity, lower_slack, upper_slack, dual; +} gecode_opt_lp_row_v1; +typedef struct { + uint64_t struct_size, reserved; + int32_t active, has_basis, basis, reserved_flags; + gecode_opt_optional_number_v1 reduced_cost; +} gecode_opt_lp_column_v1; +typedef struct { + uint64_t struct_size, reserved; + int32_t primal_valid, dual_signs_valid, stationarity_valid, complementarity_valid, + gap_valid, accepted; + gecode_opt_optional_number_v1 max_dual_sign_violation, max_stationarity, + max_complementarity, dual_objective_estimate, normalized_gap; +} gecode_opt_lp_checks_v1; +GECODE_OPT_API int32_t gecode_opt_v1_lp_capabilities(gecode_opt_lp_capabilities_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_capability_text(int32_t field, uint64_t index, char*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_options_default(gecode_opt_lp_options_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_solve_lp_observed(gecode_opt_handle model, const gecode_opt_lp_options_v1*, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_session_solve_lp_observed(gecode_opt_handle session, gecode_opt_handle model, const gecode_opt_lp_options_v1*, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_observed_result_destroy(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_observed_result_copy_result(gecode_opt_handle, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_observed_result_info(gecode_opt_handle, gecode_opt_lp_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_observed_result_metadata(gecode_opt_handle, gecode_opt_lp_metadata_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_observed_result_group(gecode_opt_handle, int32_t group, gecode_opt_lp_group_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_observed_result_checks(gecode_opt_handle, gecode_opt_lp_checks_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_observed_result_row(gecode_opt_handle, gecode_opt_id, gecode_opt_lp_row_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_observed_result_column(gecode_opt_handle, gecode_opt_id, gecode_opt_lp_column_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_observed_result_rows(gecode_opt_handle, gecode_opt_lp_row_v1*, uint64_t element_size, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_observed_result_columns(gecode_opt_handle, gecode_opt_lp_column_v1*, uint64_t element_size, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_observed_result_text(gecode_opt_handle, int32_t field, char*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; + +/* Owning LP basis submission. Factories copy source snapshots and require + * original-slot counts, with -1 only for inactive slots. Status buffers use + * VARIABLE_ID or ROW_ID as entity_kind. Empty arrays are valid for empty models. + * Factories do not solve; singularity may be repaired/rejected upon submission. + * Submission requires identical source identity/revision and active content. + * All options use the existing LP options layout. No primal_start is allowed. + * Solver termination and submission state are independent. Copied child tokens + * own their data and outlive every parent/model/session token. Absent requested + * basis returns NO_BASIS; a missing observation artifact is not an invalid token. + * Info outputs require exact size; reserved output fields are always zero. */ +enum { GECODE_OPT_BASIS_CALLER=0, GECODE_OPT_BASIS_OBSERVATIONS=1 }; +enum { GECODE_OPT_BASIS_NOT_ATTEMPTED=0, GECODE_OPT_BASIS_ACCEPTED=1, + GECODE_OPT_BASIS_REPAIRED=2, GECODE_OPT_BASIS_REJECTED=3, + GECODE_OPT_BASIS_INTERRUPTED=4 }; +typedef struct { + uint64_t struct_size, reserved, model_id, revision, row_slots, column_slots; + int32_t origin, reserved_flags; +} gecode_opt_basis_info_v1; +typedef struct { + uint64_t struct_size, reserved; + gecode_opt_result_info_v1 result; + uint64_t requested_model_id, requested_revision; + int32_t has_requested_basis, state, backend_attempted, + has_statuses_changed, statuses_changed, reserved_flags; +} gecode_opt_basis_result_info_v1; +GECODE_OPT_API int32_t gecode_opt_v1_basis_from_observed(gecode_opt_handle, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_basis_from_model(gecode_opt_handle, const int32_t* columns, uint64_t column_count, const int32_t* rows, uint64_t row_count, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_basis_destroy(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_basis_info(gecode_opt_handle, gecode_opt_basis_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_basis_statuses(gecode_opt_handle, int32_t entity_kind, int32_t*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_basis_row(gecode_opt_handle, gecode_opt_id, int32_t*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_basis_column(gecode_opt_handle, gecode_opt_id, int32_t*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_solve_lp_with_basis(gecode_opt_handle model, gecode_opt_handle basis, const gecode_opt_lp_options_v1*, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_session_solve_lp_with_basis(gecode_opt_handle session, gecode_opt_handle model, gecode_opt_handle basis, const gecode_opt_lp_options_v1*, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_basis_result_destroy(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_basis_result_info(gecode_opt_handle, gecode_opt_basis_result_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_basis_result_message(gecode_opt_handle, char*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_basis_result_copy_observed(gecode_opt_handle, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_basis_result_copy_basis(gecode_opt_handle, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; + +/* Quadratic tokens never convert to ordinary Model or Result tokens. Every + * ordinary solve/session/workflow rejects a QP model token. NULL options use + * defaults. Unsupported solver policies remain explicit termination values. + * Info/check outputs require exact sizeof(record), like the other v1 outputs. + * Array queries follow result_values/text conventions above; absent complete + * objective evaluation yields zero array length, distinguished by objective_valid. + * QP result_number accepts ordinary scalar fields plus the two vendor fields. + * KKT residuals are absent unless kkt_available; bound/gap presence is explicit. + * gap_upper_bound cancels the objective offset before rounding and need not + * equal the scalar absolute_gap. Neither implies an Exact guarantee. */ +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_capabilities(int32_t* available) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_options_default(gecode_opt_quadratic_options_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_model_create(gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_model_destroy(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_model_identity(gecode_opt_handle, uint64_t* model_id, uint64_t* revision) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_model_add_continuous(gecode_opt_handle, double lower, double upper, const char* name, gecode_opt_id*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_model_add_row(gecode_opt_handle, const gecode_opt_term*, uint64_t count, double lower, double upper, const char* name, gecode_opt_id*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_model_set_objective(gecode_opt_handle, const gecode_opt_weighted_square_v1*, uint64_t square_count, const gecode_opt_term* linear, uint64_t linear_count, int32_t sense, double offset) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_model_set_variable_bounds(gecode_opt_handle, gecode_opt_id, double lower, double upper) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_model_set_row_bounds(gecode_opt_handle, gecode_opt_id, double lower, double upper) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_model_set_coefficient(gecode_opt_handle, gecode_opt_id row, gecode_opt_id variable, double value) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_model_remove_variable(gecode_opt_handle, gecode_opt_id) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_model_remove_row(gecode_opt_handle, gecode_opt_id) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_solve(gecode_opt_handle model, const gecode_opt_quadratic_options_v1*, gecode_opt_handle* result) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_result_destroy(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_result_info(gecode_opt_handle, gecode_opt_quadratic_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_result_checks(gecode_opt_handle, gecode_opt_quadratic_checks_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_result_number(gecode_opt_handle, int32_t field, int32_t* present, double* value) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_result_value(gecode_opt_handle, gecode_opt_id variable, double* value) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_result_values(gecode_opt_handle, double* values, uint8_t* active, uint8_t* present, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_result_array(gecode_opt_handle, int32_t field, double* values, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_quadratic_result_text(gecode_opt_handle, int32_t field, char* buffer, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; + +/* Pools retain original slot identities. Entries return NEW owning Result + * tokens with Unknown termination and no original-model optimality bound. + * Attempt bounds refer to the remaining, restricted model, never the original. + * max_solutions stops enumeration without proving exhaustion. NULL options use + * defaults. has_projection=0 requires NULL projection and count zero. */ +GECODE_OPT_API int32_t gecode_opt_v1_pool_options_default(gecode_opt_pool_options_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_pool_solve(gecode_opt_handle model, const gecode_opt_pool_options_v1*, gecode_opt_handle* pool) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_pool_destroy(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_pool_info(gecode_opt_handle, gecode_opt_pool_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_pool_projection(gecode_opt_handle, gecode_opt_id*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_pool_entry_info(gecode_opt_handle, uint64_t index, gecode_opt_pool_entry_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_pool_entry_result(gecode_opt_handle, uint64_t index, gecode_opt_handle* result) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_pool_entry_projection(gecode_opt_handle, uint64_t index, int64_t*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_pool_attempt_info(gecode_opt_handle, uint64_t index, gecode_opt_pool_attempt_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_pool_message(gecode_opt_handle, char*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +/* Repairs never manufacture a feasible/optimal original-model Result. The + * final/stage Result tokens belong to the PRIVATE model and own independent + * copies, surviving repair/model destruction. Source values and validation + * are separate. Source-variable masks include historical deleted slots. + * Mapping supplies zero private IDs when no private model exists. */ +GECODE_OPT_API int32_t gecode_opt_v1_repair_options_default(gecode_opt_repair_options_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_repair_solve(gecode_opt_handle model, const gecode_opt_repair_options_v1*, gecode_opt_handle* repair) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_repair_destroy(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_repair_info(gecode_opt_handle, gecode_opt_repair_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_repair_number(gecode_opt_handle, int32_t field, int32_t* present, double* value) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_repair_original_value(gecode_opt_handle, gecode_opt_id variable, double*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_repair_original_values(gecode_opt_handle, double*, uint8_t* active, uint8_t* present, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_repair_variable_map(gecode_opt_handle, gecode_opt_id* source, gecode_opt_id* private_ids, uint8_t* active, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_repair_validation(gecode_opt_handle, gecode_opt_validation_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_repair_item_info(gecode_opt_handle, uint64_t index, gecode_opt_repair_item_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_repair_stage_info(gecode_opt_handle, uint64_t index, gecode_opt_repair_stage_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_repair_stage_result(gecode_opt_handle, uint64_t index, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_repair_final_result(gecode_opt_handle, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_repair_violation_lock(gecode_opt_handle, int32_t* present, gecode_opt_id*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_repair_objective_values(gecode_opt_handle, double*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +/* index must be zero for message/workflow/validation fields; item/stage names + * require an in-range index. Strings/arrays use the same stable size protocol. */ +GECODE_OPT_API int32_t gecode_opt_v1_repair_text(gecode_opt_handle, int32_t field, uint64_t index, char*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; + +/* Owning serial scenario batches (ordinary linear models only). ABI 1 remains + * additive. No caller Session is mutated; materialize() is C++-only. */ +typedef struct { uint64_t batch_id, index; } gecode_opt_scenario_id; +enum { GECODE_OPT_SCENARIO_AUTOMATIC=0, GECODE_OPT_SCENARIO_COLD=1 }; +enum { GECODE_OPT_SCENARIO_REJECTED=0, GECODE_OPT_SCENARIO_INTERRUPTED=1, GECODE_OPT_SCENARIO_COMPLETE=2 }; +enum { GECODE_OPT_SCENARIO_NOT_STARTED=0, GECODE_OPT_SCENARIO_ATTEMPTED=1 }; +typedef struct { + uint64_t struct_size, reserved; + gecode_opt_id entity; + int32_t has_lower, has_upper; + double lower, upper; +} gecode_opt_scenario_bounds_v1; +typedef struct { + uint64_t struct_size, reserved; + const char* name; + const gecode_opt_term* objective_coefficients; + uint64_t objective_count; + gecode_opt_optional_number_v1 objective_offset; + const gecode_opt_scenario_bounds_v1* variable_bounds; + uint64_t variable_count; + const gecode_opt_scenario_bounds_v1* row_bounds; + uint64_t row_count; +} gecode_opt_scenario_definition_v1; +typedef struct { + uint64_t struct_size, reserved; + gecode_opt_options_v1 solve; + int32_t reuse, reserved_flags; + uint64_t max_scenarios, max_patch_entries, max_saved_value_slots, max_work; +} gecode_opt_scenario_options_v1; +typedef struct { + uint64_t struct_size, reserved, model_id, revision, batch_id, scenario_count, outcome_count; + int32_t has_batch, completion, has_stop_reason, stop_reason; + int32_t has_offending_scenario, all_resolved; + uint64_t offending_scenario, attempted, resolved, work; + double elapsed_seconds; + gecode_opt_session_statistics_v1 reuse_statistics; +} gecode_opt_scenario_info_v1; +typedef struct { + uint64_t struct_size, reserved; + gecode_opt_scenario_id scenario; + int32_t state, has_result, has_check, reserved_flags; + gecode_opt_result_info_v1 result; + gecode_opt_session_statistics_v1 reuse_delta; + double elapsed_seconds; +} gecode_opt_scenario_outcome_v1; +typedef struct { + uint64_t struct_size, reserved; + int32_t has_check, identity_valid, candidate_examined, objective_matches, exact_witness_validated, reserved_flags; + /* Meaningful only when candidate_examined; otherwise all zero/absent. */ + gecode_opt_validation_info_v1 validation; +} gecode_opt_scenario_check_v1; +typedef struct { + uint64_t struct_size, reserved, objective_count, variable_count, row_count; + gecode_opt_optional_number_v1 objective_offset; +} gecode_opt_scenario_definition_info_v1; +GECODE_OPT_API int32_t gecode_opt_v1_scenario_options_default(gecode_opt_scenario_options_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +/* All definitions/arrays/names are copied before any solve. Input sizes must + * equal v1, reserved fields zero, presence flags exactly 0/1. */ +GECODE_OPT_API int32_t gecode_opt_v1_solve_scenarios(gecode_opt_handle model, const gecode_opt_scenario_definition_v1*, uint64_t count, uint64_t element_size, const gecode_opt_scenario_options_v1*, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_scenario_batch_destroy(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_scenario_batch_info(gecode_opt_handle, gecode_opt_scenario_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_scenario_batch_id(gecode_opt_handle, uint64_t index, gecode_opt_scenario_id*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_scenario_batch_outcome(gecode_opt_handle, gecode_opt_scenario_id, gecode_opt_scenario_outcome_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_scenario_batch_check(gecode_opt_handle, gecode_opt_scenario_id, gecode_opt_scenario_check_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +/* Copy is an independent ordinary Result with PRIVATE owner/revision. Save a + * map() output to use its values after closing the batch; original IDs differ. */ +GECODE_OPT_API int32_t gecode_opt_v1_scenario_batch_copy_result(gecode_opt_handle, gecode_opt_scenario_id, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_scenario_batch_map(gecode_opt_handle, gecode_opt_id original, gecode_opt_id* private_id) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_scenario_batch_value(gecode_opt_handle, gecode_opt_scenario_id, gecode_opt_id original, double*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_scenario_batch_message(gecode_opt_handle, char*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +enum { GECODE_OPT_SCENARIO_NAME=0, GECODE_OPT_SCENARIO_VALIDATION_MESSAGE=1 }; +GECODE_OPT_API int32_t gecode_opt_v1_scenario_batch_text(gecode_opt_handle, gecode_opt_scenario_id, int32_t field, char*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_scenario_batch_definition(gecode_opt_handle, gecode_opt_scenario_id, gecode_opt_scenario_definition_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_scenario_batch_objective(gecode_opt_handle, gecode_opt_scenario_id, gecode_opt_term*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +/* entity_kind selects VARIABLE_ID or ROW_ID. Query size with NULL/capacity 0; + * short buffers remain unchanged. Returned bounds have original source IDs. */ +GECODE_OPT_API int32_t gecode_opt_v1_scenario_batch_bounds(gecode_opt_handle, gecode_opt_scenario_id, int32_t entity_kind, gecode_opt_scenario_bounds_v1*, uint64_t element_size, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; + +/* O3: additional-work numerical evidence. No source-model Result conversion. */ +enum { GECODE_OPT_NO_EVIDENCE=10 }; +enum { GECODE_OPT_EVIDENCE_AUTOMATIC=0, GECODE_OPT_EVIDENCE_PRIMAL_RAY=1, GECODE_OPT_EVIDENCE_FARKAS=2, GECODE_OPT_EVIDENCE_BOTH=3 }; +enum { GECODE_OPT_EVIDENCE_NOT_REQUESTED=0, GECODE_OPT_EVIDENCE_AVAILABLE=1, GECODE_OPT_EVIDENCE_UNAVAILABLE=2, GECODE_OPT_EVIDENCE_REJECTED=3 }; +enum { GECODE_OPT_EVIDENCE_REASON_NONE=0, GECODE_OPT_EVIDENCE_REASON_NOT_REQUESTED=1, + GECODE_OPT_EVIDENCE_REASON_UNSUPPORTED=2, GECODE_OPT_EVIDENCE_REASON_NO_FEASIBLE_BASE=3, + GECODE_OPT_EVIDENCE_REASON_NO_IMPROVEMENT=4, GECODE_OPT_EVIDENCE_REASON_NO_CONTRADICTION=5, + GECODE_OPT_EVIDENCE_REASON_STOPPED=6, GECODE_OPT_EVIDENCE_REASON_INVALID_BACKEND=7, + GECODE_OPT_EVIDENCE_REASON_FAILED_CHECKS=8, GECODE_OPT_EVIDENCE_REASON_INCONSISTENT=9, + GECODE_OPT_EVIDENCE_REASON_INVALID_MODEL=10, GECODE_OPT_EVIDENCE_REASON_RESOURCE_LIMIT=11, + GECODE_OPT_EVIDENCE_REASON_ALLOCATION=12 }; +enum { GECODE_OPT_EVIDENCE_COMPLETE=0, GECODE_OPT_EVIDENCE_INTERRUPTED=1, GECODE_OPT_EVIDENCE_ANALYSIS_REJECTED=2 }; +enum { GECODE_OPT_EVIDENCE_FEASIBLE_BASE=0, GECODE_OPT_EVIDENCE_RECESSION=1, GECODE_OPT_EVIDENCE_FARKAS_PHASE=2 }; +enum { GECODE_OPT_EVIDENCE_LOWER=0, GECODE_OPT_EVIDENCE_UPPER=1 }; +enum { GECODE_OPT_EVIDENCE_SOURCE_VARIABLE=0, GECODE_OPT_EVIDENCE_ROW_SIDE=1, GECODE_OPT_EVIDENCE_VARIABLE_SIDE=2 }; +enum { GECODE_OPT_EVIDENCE_PRIMAL_GROUP=0, GECODE_OPT_EVIDENCE_FARKAS_GROUP=1 }; +enum { GECODE_OPT_EVIDENCE_BASE_VALUE=0, GECODE_OPT_EVIDENCE_DIRECTION_VALUE=1 }; +typedef struct { + uint64_t struct_size, reserved; + gecode_opt_options_v1 solve; + int32_t request, reserved_flags; + double recession, stationarity, minimum_improvement, minimum_contradiction; + uint64_t max_auxiliary_variables, max_auxiliary_rows, max_auxiliary_nonzeros; + uint64_t max_retained_slots, max_work, max_auxiliary_solves; +} gecode_opt_evidence_options_v1; +typedef struct { + uint64_t struct_size, reserved, model_id, revision; + int32_t has_evidence, completion, has_stop_reason, stop_reason; + uint64_t row_slots, column_slots, stage_count, attempted_calls, work; + double elapsed_seconds; +} gecode_opt_evidence_info_v1; +typedef struct { + uint64_t struct_size, reserved; + int32_t state, reason; +} gecode_opt_evidence_group_v1; +typedef struct { + uint64_t struct_size, reserved; + double recession, stationarity, minimum_improvement, minimum_contradiction, primal_tolerance; +} gecode_opt_evidence_metadata_v1; +typedef struct { + uint64_t struct_size, reserved; + int32_t has_base_check, reserved_flags; + gecode_opt_validation_info_v1 base_check; + gecode_opt_optional_number_v1 direction_scale, normalized_objective_slope; + gecode_opt_optional_number_v1 max_variable_recession_violation, max_row_recession_violation; +} gecode_opt_evidence_primal_v1; +typedef struct { + uint64_t struct_size, reserved; + gecode_opt_optional_number_v1 multiplier_scale, contradiction_margin, max_stationarity; +} gecode_opt_evidence_farkas_v1; +/* Diagnostic slots may remain populated after rejection. Group Available alone + * denotes accepted numerical evidence. Inactive/absent data has present=0. */ +typedef struct { + uint64_t struct_size, reserved; + gecode_opt_id source; + int32_t active, has_side, side, reserved_flags; + gecode_opt_optional_number_v1 base_value, direction, multiplier, contribution, selected_bound; +} gecode_opt_evidence_slot_v1; +/* Explicitly reported raw auxiliary fields: never ordinary has_solution(). + * Integer termination/guarantee codes can be unknown malformed backend values. */ +typedef struct { + uint64_t struct_size, reserved, model_id, revision, value_count, mask_count; + int32_t termination_code, guarantee_code, reported_solution_validated, reported_start_submitted; + double elapsed_seconds; + gecode_opt_optional_number_v1 objective, best_bound, absolute_gap, relative_gap, native_gap; +} gecode_opt_evidence_raw_result_v1; +typedef struct { + uint64_t struct_size, reserved, index, private_model_id, private_revision, row_count, column_count, nonzeros; + int32_t phase, attempted, has_raw_result, candidate_examined; + gecode_opt_validation_info_v1 check; + gecode_opt_evidence_raw_result_v1 raw_result; +} gecode_opt_evidence_stage_v1; +typedef struct { + uint64_t struct_size, reserved; + gecode_opt_id private_variable, source; + int32_t kind, has_side, side, reserved_flags; +} gecode_opt_evidence_column_v1; +typedef struct { + uint64_t struct_size, reserved, slot; + gecode_opt_optional_number_v1 reported_value; + int32_t has_reported_mask, reported_mask; +} gecode_opt_evidence_raw_value_v1; +GECODE_OPT_API int32_t gecode_opt_v1_evidence_options_default(gecode_opt_evidence_options_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_analyze_lp_evidence(gecode_opt_handle model, const gecode_opt_evidence_options_v1*, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_destroy(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_info(gecode_opt_handle, gecode_opt_evidence_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_group(gecode_opt_handle, int32_t group, gecode_opt_evidence_group_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_metadata(gecode_opt_handle, gecode_opt_evidence_metadata_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_primal(gecode_opt_handle, gecode_opt_evidence_primal_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_farkas(gecode_opt_handle, gecode_opt_evidence_farkas_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_slot(gecode_opt_handle, gecode_opt_id source, gecode_opt_evidence_slot_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_slots(gecode_opt_handle, int32_t entity_kind, gecode_opt_evidence_slot_v1*, uint64_t element_size, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +/* These typed accepted getters additionally require effective Available. */ +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_value(gecode_opt_handle, gecode_opt_id source, int32_t field, double*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_multiplier(gecode_opt_handle, gecode_opt_id source, gecode_opt_evidence_slot_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +enum { GECODE_OPT_EVIDENCE_MESSAGE=0, GECODE_OPT_EVIDENCE_PRIMAL_MESSAGE=1, + GECODE_OPT_EVIDENCE_FARKAS_MESSAGE=2, GECODE_OPT_EVIDENCE_BASE_CHECK_MESSAGE=3 }; +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_text(gecode_opt_handle, int32_t field, char*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +/* Independent owning typed child; no ordinary Result conversion. */ +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_copy_stage(gecode_opt_handle, uint64_t index, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_stage_destroy(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_stage_info(gecode_opt_handle, gecode_opt_evidence_stage_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_stage_columns(gecode_opt_handle, gecode_opt_evidence_column_v1*, uint64_t element_size, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_stage_raw_values(gecode_opt_handle, gecode_opt_evidence_raw_value_v1*, uint64_t element_size, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +enum { GECODE_OPT_EVIDENCE_RAW_BACKEND=0, GECODE_OPT_EVIDENCE_RAW_BACKEND_VERSION=1, + GECODE_OPT_EVIDENCE_RAW_MESSAGE=2, GECODE_OPT_EVIDENCE_STAGE_CHECK_MESSAGE=3 }; +GECODE_OPT_API int32_t gecode_opt_v1_lp_evidence_stage_text(gecode_opt_handle, int32_t field, char*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; + +/* Owning numerical sensitivity of the exact supplied O1 result and selected + * basis. No ordinary Result conversion, implicit solve, or replacement basis. + * All new records require exact sizes and zero reserved input fields. Requests + * are explicit/nonempty/unique. Infinity is an endpoint tag, not a parameter. + * Getters copy historical data and perform no factorization or solver work. + * Whole-operation stops revoke every interval, retaining source diagnostics. + * Valid unrequested lookup sets requested=0; foreign/deleted IDs are errors. */ +enum { GECODE_OPT_NO_SENSITIVITY=11 }; +enum { GECODE_OPT_SENSITIVITY_OBJECTIVE=0, GECODE_OPT_SENSITIVITY_EQUALITY_RHS=1 }; +enum { GECODE_OPT_SENSITIVITY_NOT_REQUESTED=0, GECODE_OPT_SENSITIVITY_AVAILABLE=1, + GECODE_OPT_SENSITIVITY_UNAVAILABLE=2, GECODE_OPT_SENSITIVITY_REJECTED=3 }; +enum { GECODE_OPT_SENSITIVITY_COMPLETE=0, GECODE_OPT_SENSITIVITY_PARTIAL=1, + GECODE_OPT_SENSITIVITY_INTERRUPTED=2, GECODE_OPT_SENSITIVITY_ANALYSIS_REJECTED=3 }; +enum { GECODE_OPT_SENSITIVITY_REASON_NONE=0, GECODE_OPT_SENSITIVITY_REASON_NOT_REQUESTED=1, + GECODE_OPT_SENSITIVITY_REASON_UNSUPPORTED=2, GECODE_OPT_SENSITIVITY_REASON_NOT_OPTIMAL=3, + GECODE_OPT_SENSITIVITY_REASON_NO_BASIS=4, GECODE_OPT_SENSITIVITY_REASON_INVALID_SOURCE=5, + GECODE_OPT_SENSITIVITY_REASON_INVALID_BASIS=6, GECODE_OPT_SENSITIVITY_REASON_CHANGED_BASIS=7, + GECODE_OPT_SENSITIVITY_REASON_REFERENCE_CHECKS=8, GECODE_OPT_SENSITIVITY_REASON_SYSTEM_CHECKS=9, + GECODE_OPT_SENSITIVITY_REASON_INTERVAL_CHECKS=10, GECODE_OPT_SENSITIVITY_REASON_RESOURCE_LIMIT=11, + GECODE_OPT_SENSITIVITY_REASON_STOPPED=12, GECODE_OPT_SENSITIVITY_REASON_ALLOCATION=13, + GECODE_OPT_SENSITIVITY_REASON_BACKEND=14 }; +enum { GECODE_OPT_RANGE_FINITE=0, GECODE_OPT_RANGE_NEGATIVE_INFINITY=1, + GECODE_OPT_RANGE_POSITIVE_INFINITY=2 }; +enum { GECODE_OPT_SENSITIVITY_LOWER=0, GECODE_OPT_SENSITIVITY_UPPER=1, + GECODE_OPT_SENSITIVITY_FIXED=2, GECODE_OPT_SENSITIVITY_FREE=3 }; +typedef struct { + uint64_t struct_size, reserved; + int32_t kind, reserved_flags; + gecode_opt_id entity; +} gecode_opt_sensitivity_request_v1; +typedef struct { + uint64_t struct_size, reserved; + double primal_feasibility, dual_feasibility, stationarity, complementarity, + objective_gap, system_absolute, system_relative; +} gecode_opt_sensitivity_checks_options_v1; +typedef struct { + uint64_t struct_size, reserved, max_rows, max_columns, max_nonzeros, max_requests, + max_basis_solves, max_factor_entries, max_retained_slots, max_work; +} gecode_opt_sensitivity_limits_v1; +typedef struct { + uint64_t struct_size, reserved; + int32_t backend, reserved_flags; + double time_limit_seconds; + gecode_opt_handle cancellation; + gecode_opt_sensitivity_checks_options_v1 checks; + gecode_opt_sensitivity_limits_v1 limits; + const gecode_opt_sensitivity_request_v1* requests; + uint64_t request_count; +} gecode_opt_sensitivity_options_v1; +typedef struct { + uint64_t struct_size, reserved, model_id, revision; + int32_t completion, reason, has_stop_reason, stop_reason, has_sensitivity, + has_basis, guarantee, reserved_flags; + uint64_t entry_count, factor_order_count, row_slots, column_slots; + double elapsed_seconds; +} gecode_opt_sensitivity_info_v1; +typedef struct { + uint64_t struct_size, reserved; + int32_t factor_setup_attempted, reserved_flags; + uint64_t basis_solves, coordinator_visits, retained_slots, preparation_visits; +} gecode_opt_sensitivity_work_v1; +typedef struct { uint64_t struct_size, reserved; int32_t state, reason; } gecode_opt_sensitivity_group_v1; +typedef struct { + uint64_t struct_size, reserved; + int32_t kind, reserved_flags; + gecode_opt_optional_number_v1 value; +} gecode_opt_sensitivity_end_v1; +typedef struct { + uint64_t struct_size, reserved; + gecode_opt_id entity; + int32_t side, dual_condition; +} gecode_opt_sensitivity_limiter_v1; +typedef struct { + uint64_t struct_size, reserved, inequalities; + int32_t accepted, lower_direction_checked, upper_direction_checked, reserved_flags; + gecode_opt_optional_number_v1 max_endpoint_violation; +} gecode_opt_sensitivity_interval_checks_v1; +typedef struct { + uint64_t struct_size, reserved; + gecode_opt_sensitivity_request_v1 request; + gecode_opt_sensitivity_group_v1 group; + uint64_t index; + int32_t requested, has_interval, has_lower_limiter, has_upper_limiter; + double anchor; + gecode_opt_sensitivity_end_v1 lower, upper; + gecode_opt_optional_number_v1 objective_slope; + gecode_opt_sensitivity_limiter_v1 lower_limiter, upper_limiter; + gecode_opt_sensitivity_interval_checks_v1 checks; +} gecode_opt_sensitivity_entry_v1; +typedef struct { + uint64_t struct_size, reserved; + gecode_opt_validation_info_v1 primal; + gecode_opt_lp_checks_v1 kkt; + int32_t basis_point_matches, reserved_flags; + gecode_opt_optional_number_v1 max_point_difference, max_system_residual, + max_scaled_system_residual; +} gecode_opt_sensitivity_reference_checks_v1; +GECODE_OPT_API int32_t gecode_opt_v1_sensitivity_options_default(gecode_opt_sensitivity_options_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_analyze_lp_sensitivity(gecode_opt_handle observed, const gecode_opt_sensitivity_options_v1*, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_sensitivity_destroy(gecode_opt_handle) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_sensitivity_info(gecode_opt_handle, gecode_opt_sensitivity_info_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_sensitivity_work(gecode_opt_handle, gecode_opt_sensitivity_work_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_sensitivity_checks_options(gecode_opt_handle, gecode_opt_sensitivity_checks_options_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_sensitivity_reference_checks(gecode_opt_handle, gecode_opt_sensitivity_reference_checks_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_sensitivity_copy_source_observed(gecode_opt_handle, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_sensitivity_copy_basis(gecode_opt_handle, gecode_opt_handle*) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_sensitivity_entry(gecode_opt_handle, uint64_t index, gecode_opt_sensitivity_entry_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_sensitivity_entries(gecode_opt_handle, gecode_opt_sensitivity_entry_v1*, uint64_t element_size, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_sensitivity_objective(gecode_opt_handle, gecode_opt_id, gecode_opt_sensitivity_entry_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_sensitivity_equality_rhs(gecode_opt_handle, gecode_opt_id, gecode_opt_sensitivity_entry_v1*, uint64_t size) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_sensitivity_factor_order(gecode_opt_handle, gecode_opt_id*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +GECODE_OPT_API int32_t gecode_opt_v1_sensitivity_active_slots(gecode_opt_handle, int32_t entity_kind, uint8_t*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; +enum { GECODE_OPT_SENSITIVITY_MESSAGE=0, GECODE_OPT_SENSITIVITY_BACKEND_VERSION=1, + GECODE_OPT_SENSITIVITY_ENTRY_MESSAGE=2, GECODE_OPT_SENSITIVITY_INTERVAL_MESSAGE=3, + GECODE_OPT_SENSITIVITY_PRIMAL_MESSAGE=4, GECODE_OPT_SENSITIVITY_KKT_MESSAGE=5 }; +GECODE_OPT_API int32_t gecode_opt_v1_sensitivity_text(gecode_opt_handle, int32_t field, uint64_t index, char*, uint64_t capacity, uint64_t* required) GECODE_OPT_NOEXCEPT; + +#ifdef __cplusplus +} +#endif +#endif diff --git a/gecode/optimize/constraints.cpp b/gecode/optimize/constraints.cpp new file mode 100644 index 0000000000..02047f30ba --- /dev/null +++ b/gecode/optimize/constraints.cpp @@ -0,0 +1,407 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { +namespace { +constexpr double infinity = std::numeric_limits::infinity(); + +void bounds(double lower, double upper) { + if (std::isnan(lower) || std::isnan(upper) || lower == infinity || + upper == -infinity || lower > upper) + throw ModelError("Invalid indicator lower/upper bounds"); +} + +double effective_lower(const VariableData& variable) { + return variable.type == VariableType::SemiContinuous || + variable.type == VariableType::SemiInteger ? 0.0 : variable.lower; +} + +double outward(double value, bool up) { + if (!std::isfinite(value)) + throw ModelError("Indicator activity/M overflow; tighten domains or rescale the model"); + const double result = std::nextafter(value, up ? infinity : -infinity); + if (!std::isfinite(result)) + throw ModelError("Indicator activity/M cannot be enclosed finitely; rescale the model"); + return result; +} + +double add_bound(double a, double b, bool up) { + if (a == 0.0) return b; + if (b == 0.0) return a; + if (a == -b) return 0.0; + return outward(a + b, up); +} + +double product_bound(double coefficient, double endpoint, bool up) { + if (!std::isfinite(endpoint)) + throw ModelError("Indicator needs a finite inactive-domain activity bound; tighten the relevant variable bounds"); + if (endpoint == 0.0) return 0.0; + if (endpoint == 1.0) return coefficient; + if (endpoint == -1.0) return -coefficient; + if (coefficient == 1.0) return endpoint; + if (coefficient == -1.0) return -endpoint; + return outward(coefficient * endpoint, up); +} + +struct Derived { + std::optional lower_m; + std::optional upper_m; +}; + +Derived derive(const IndicatorData& data) { + Derived result; + for (bool upper : {false, true}) { + const double bound = upper ? data.upper : data.lower; + if (!std::isfinite(bound)) continue; + double activity = 0.0; + std::size_t domain = 0; + for (const auto& term : data.terms) { + double lower, higher; + if (term.variable == data.activator) { + lower = higher = data.active_value ? 0.0 : 1.0; + } else { + if (domain >= data.domains.size() || data.domains[domain].variable != term.variable) + throw ModelError("Indicator domain dependencies do not match original terms"); + lower = data.domains[domain].lower; + higher = data.domains[domain].upper; + ++domain; + } + const bool use_upper = (term.coefficient > 0.0) == upper; + const double contribution = product_bound(term.coefficient, + use_upper ? higher : lower, upper); + activity = add_bound(activity, contribution, upper); + } + double m = 0.0; + if (upper ? activity > bound : bound > activity) + m = upper ? add_bound(activity, -bound, true) : add_bound(bound, -activity, true); + if (upper) result.upper_m = m; + else result.lower_m = m; + } + return result; +} + +bool needs_gate(const IndicatorData& data) { + return data.lower_m.value_or(0.0) > 0.0 || data.upper_m.value_or(0.0) > 0.0; +} + +struct LoweredRow { + std::vector terms; + double lower; + double upper; + const char* suffix; +}; + +std::vector lower_rows(const IndicatorData& data) { + std::vector result; + if (data.inactive_gate) { + std::vector terms{{data.activator, data.active_value ? 1.0 : -1.0}, + {*data.inactive_gate, 1.0}}; + std::sort(terms.begin(), terms.end(), [](const Term& a, const Term& b) { + return a.variable.id < b.variable.id; + }); + const double rhs = data.active_value ? 1.0 : 0.0; + result.push_back({std::move(terms), rhs, rhs, "gate"}); + } + for (bool upper : {false, true}) { + const auto& m = upper ? data.upper_m : data.lower_m; + if (!m) continue; + auto terms = data.terms; + if (*m > 0.0) { + if (!data.inactive_gate) throw ModelError("Indicator M requires an inactive gate"); + terms.push_back({*data.inactive_gate, upper ? -*m : *m}); + std::sort(terms.begin(), terms.end(), [](const Term& a, const Term& b) { + return a.variable.id < b.variable.id; + }); + } + result.push_back({std::move(terms), upper ? -infinity : data.lower, + upper ? data.upper : infinity, upper ? "upper" : "lower"}); + } + return result; +} + +bool same_terms(const std::vector& a, const std::vector& b) { + if (a.size() != b.size()) return false; + for (std::size_t i = 0; i < a.size(); ++i) + if (a[i].variable != b[i].variable || a[i].coefficient != b[i].coefficient) + return false; + return true; +} + +const VariableData& checked_variable(const ModelSnapshot& model, Variable handle, + bool active = true) { + if (handle.model_id != model.model_id || handle.id >= model.variables.size() || + (active && !model.variables[static_cast(handle.id)].active)) + throw ModelError("Indicator references a foreign, absent, or deleted variable"); + return model.variables[static_cast(handle.id)]; +} + +void require_binary(const VariableData& variable) { + if (variable.type != VariableType::Binary) + throw ModelError("Logical helpers require Binary variables; use add_binary"); +} +} // namespace + +namespace Detail { + +/** Private two-phase append: allocations complete before any observable change. */ +class ConstraintBatch { + Model& model_; + std::vector variables_; + std::vector rows_; + std::vector indicators_; + + template + struct Append { + std::vector& target; + std::vector& additions; + std::vector replacement; + bool replace = false; + Append(std::vector& target, std::vector& additions) + : target(target), additions(additions) { + static_assert(std::is_nothrow_move_constructible::value, "Commit must not throw"); + if (additions.size() > target.max_size() - target.size()) + throw ModelError("Logical formulation exceeds container limits"); + const auto desired = target.size() + additions.size(); + if (desired <= target.capacity()) return; + replace = true; + const auto extra = std::min(target.size(), target.max_size() - target.size()); + replacement.reserve(std::max(desired, target.size() + extra)); + replacement.insert(replacement.end(), target.begin(), target.end()); + for (auto& value : additions) replacement.push_back(std::move(value)); + } + void commit() noexcept { + if (replace) target.swap(replacement); + else for (auto& value : additions) target.push_back(std::move(value)); + } + }; + +public: + explicit ConstraintBatch(Model& model) : model_(model) { + model_.require_revision_capacity(); + } + std::vector normalize(const std::vector& terms) const { + return model_.normalize(terms, &variables_); + } + Variable binary(std::string name, Indicator origin) { + if (variables_.size() >= std::numeric_limits::max() - model_.variables_.size()) + throw ModelError("Variable slot space exhausted"); + Variable handle{model_.id(), static_cast(model_.variables_.size() + variables_.size())}; + variables_.push_back({handle, VariableType::Binary, 0.0, 1.0, std::move(name), true, origin}); + return handle; + } + Indicator indicator_handle() const { + if (indicators_.size() >= std::numeric_limits::max() - model_.indicators_.size()) + throw ModelError("Indicator slot space exhausted"); + return {model_.id(), static_cast(model_.indicators_.size() + indicators_.size())}; + } + Constraint row(const std::vector& terms, double lower, double upper, + std::string name, std::optional origin = {}) { + bounds(lower, upper); + if (rows_.size() >= std::numeric_limits::max() - model_.rows_.size()) + throw ModelError("Constraint slot space exhausted"); + auto normalized = normalize(terms); + Constraint handle{model_.id(), static_cast(model_.rows_.size() + rows_.size())}; + rows_.push_back({handle, std::move(normalized), lower, upper, std::move(name), true, origin}); + return handle; + } + void indicator(IndicatorData data) { indicators_.push_back(std::move(data)); } + void commit() { + if (variables_.empty() && rows_.empty() && indicators_.empty()) return; + model_.require_revision_capacity(); + Append variables(model_.variables_, variables_); + Append rows(model_.rows_, rows_); + Append indicators(model_.indicators_, indicators_); + variables.commit(); + rows.commit(); + indicators.commit(); + ++model_.revision_; + } + static void remove(Model& model, Indicator handle) { + model.require_revision_capacity(); + if (handle.model_id != model.id() || handle.id >= model.indicators_.size() || + !model.indicators_[static_cast(handle.id)].active) + throw ModelError("Indicator handle is foreign, invalid, or removed"); + auto& data = model.indicators_[static_cast(handle.id)]; + for (auto row : data.generated_rows) { + auto& generated = model.rows_[static_cast(row.id)]; + generated.active = false; + generated.terms.clear(); + } + data.active = false; + ++model.revision_; + } +}; + +void validate_indicators(const ModelSnapshot& model) { + std::set owned_rows; + std::set owned_gates; + for (std::size_t i = 0; i < model.indicators.size(); ++i) { + const auto& data = model.indicators[i]; + if (data.indicator.model_id != model.model_id || data.indicator.id != i) + throw ModelError("Indicator identity does not match its model/slot"); + if (data.inactive_gate) { + const auto& gate = checked_variable(model, *data.inactive_gate, data.active); + if (!owned_gates.insert(data.inactive_gate->id).second || !gate.indicator_origin || + gate.indicator_origin->model_id != model.model_id || gate.indicator_origin->id != i) + throw ModelError("Indicator gate identity is invalid, shared, or missing its origin"); + } + for (auto handle : data.generated_rows) { + if (handle.model_id != model.model_id || handle.id >= model.rows.size() || + !owned_rows.insert(handle.id).second) + throw ModelError("Indicator generated row identity is invalid or duplicated"); + const auto& row = model.rows[static_cast(handle.id)]; + if (!row.indicator_origin || row.indicator_origin->model_id != model.model_id || + row.indicator_origin->id != i || row.active != data.active) + throw ModelError("Indicator/generated row lifecycle mismatch"); + } + if (!data.active) continue; + require_binary(checked_variable(model, data.activator)); + bounds(data.lower, data.upper); + std::optional previous; + std::size_t domain = 0; + for (const auto& term : data.terms) { + const auto& variable = checked_variable(model, term.variable); + if (!std::isfinite(term.coefficient) || term.coefficient == 0.0 || + (previous && term.variable.id <= *previous)) + throw ModelError("Indicator original terms are not canonical"); + previous = term.variable.id; + if (term.variable == data.activator) continue; + if (domain >= data.domains.size() || data.domains[domain].variable != term.variable) + throw ModelError("Indicator domain dependencies do not match original terms"); + const auto& captured = data.domains[domain++]; + bounds(captured.lower, captured.upper); + if (effective_lower(variable) < captured.lower || variable.upper > captured.upper) + throw ModelError("Model bounds exceed the domains used by an indicator; rebuild the formulation"); + } + if (domain != data.domains.size()) + throw ModelError("Indicator has unrelated domain dependencies"); + const auto required = derive(data); + for (bool upper : {false, true}) { + const auto& supplied = upper ? data.upper_m : data.lower_m; + const auto& minimum = upper ? required.upper_m : required.lower_m; + if (supplied.has_value() != minimum.has_value() || + (supplied && (!std::isfinite(*supplied) || *supplied < *minimum))) + throw ModelError("Indicator M does not safely relax the captured inactive domain"); + } + if (data.inactive_gate.has_value() != needs_gate(data)) + throw ModelError("Indicator inactive gate does not match its relaxation"); + if (data.inactive_gate) { + const auto& gate = checked_variable(model, *data.inactive_gate); + require_binary(gate); + if (gate.lower != 0.0 || gate.upper != 1.0 || + *data.inactive_gate == data.activator || + std::any_of(data.terms.begin(), data.terms.end(), [&](const Term& term) { + return term.variable == *data.inactive_gate; + })) + throw ModelError("Indicator gate is aliased or its domain has changed"); + } + const auto expected = lower_rows(data); + if (expected.size() != data.generated_rows.size()) + throw ModelError("Indicator is missing generated rows"); + for (std::size_t r = 0; r < expected.size(); ++r) { + const auto& row = model.rows[static_cast(data.generated_rows[r].id)]; + if (row.lower != expected[r].lower || row.upper != expected[r].upper || + !same_terms(row.terms, expected[r].terms)) + throw ModelError("Indicator generated row no longer matches its original meaning"); + } + } + for (const auto& row : model.rows) + if (row.indicator_origin && owned_rows.count(row.constraint.id) == 0) + throw ModelError("Generated indicator row is missing its original metadata"); + for (const auto& variable : model.variables) + if (variable.indicator_origin && owned_gates.count(variable.variable.id) == 0) + throw ModelError("Generated indicator gate is missing its original metadata"); +} +} // namespace Detail + +IndicatorFormulation add_indicator(Model& model, Variable activator, + bool active_value, const std::vector& terms, + double lower, double upper, std::string name) { + Detail::ConstraintBatch batch(model); + require_binary(model.variable(activator)); + bounds(lower, upper); + IndicatorData data; + data.indicator = batch.indicator_handle(); + data.activator = activator; + data.active_value = active_value; + data.terms = batch.normalize(terms); + data.lower = lower; + data.upper = upper; + if (!std::isfinite(lower) && !std::isfinite(upper)) data.terms.clear(); + for (const auto& term : data.terms) + if (term.variable != activator) { + const auto& variable = model.variable(term.variable); + data.domains.push_back({term.variable, effective_lower(variable), variable.upper}); + } + const auto derived = derive(data); + data.lower_m = derived.lower_m; + data.upper_m = derived.upper_m; + const std::string prefix = name.empty() ? "indicator_" + std::to_string(data.indicator.id) : name; + if (needs_gate(data)) data.inactive_gate = batch.binary(prefix + "_inactive", data.indicator); + for (const auto& row : lower_rows(data)) + data.generated_rows.push_back(batch.row(row.terms, row.lower, row.upper, + prefix + "_" + row.suffix, data.indicator)); + IndicatorFormulation result{data.indicator, data.inactive_gate, data.generated_rows, + data.lower_m, data.upper_m}; + batch.indicator(std::move(data)); + batch.commit(); + return result; +} + +void remove_indicator(Model& model, Indicator indicator) { + Detail::ConstraintBatch::remove(model, indicator); +} + +namespace { +std::vector boolean(Model& model, Variable result, + std::vector inputs, bool conjunction, + std::string name) { + Detail::ConstraintBatch batch(model); + require_binary(model.variable(result)); + for (auto input : inputs) require_binary(model.variable(input)); + std::sort(inputs.begin(), inputs.end(), [](Variable a, Variable b) { return a.id < b.id; }); + inputs.erase(std::unique(inputs.begin(), inputs.end()), inputs.end()); + if (inputs.size() > 9007199254740991ULL) + throw ModelError("Boolean cardinality exceeds exact double integer range"); + const std::string prefix = name.empty() ? (conjunction ? "and" : "or") : name; + std::vector rows; + if (inputs.empty()) { + const double value = conjunction ? 1.0 : 0.0; + rows.push_back(batch.row({{result, 1.0}}, value, value, prefix + "_empty")); + } else if (inputs.size() == 1) { + rows.push_back(batch.row({{result, 1.0}, {inputs.front(), -1.0}}, 0, 0, prefix + "_equal")); + } else { + for (std::size_t i = 0; i < inputs.size(); ++i) + rows.push_back(batch.row({{result, 1.0}, {inputs[i], -1.0}}, + conjunction ? -infinity : 0.0, + conjunction ? 0.0 : infinity, + prefix + "_input_" + std::to_string(i))); + std::vector aggregate{{result, 1.0}}; + for (auto input : inputs) aggregate.push_back({input, -1.0}); + rows.push_back(batch.row(aggregate, + conjunction ? 1.0 - static_cast(inputs.size()) : -infinity, + conjunction ? infinity : 0.0, prefix + "_aggregate")); + } + batch.commit(); + return rows; +} +} // namespace + +std::vector add_boolean_and(Model& model, Variable result, + const std::vector& inputs, + std::string name) { + return boolean(model, result, inputs, true, std::move(name)); +} +std::vector add_boolean_or(Model& model, Variable result, + const std::vector& inputs, + std::string name) { + return boolean(model, result, inputs, false, std::move(name)); +} + +}} diff --git a/gecode/optimize/constraints.hpp b/gecode/optimize/constraints.hpp new file mode 100644 index 0000000000..74059f77cd --- /dev/null +++ b/gecode/optimize/constraints.hpp @@ -0,0 +1,51 @@ +/* Logical modeling helpers with explicit original-model semantics. */ +#ifndef GECODE_OPTIMIZE_CONSTRAINTS_HPP +#define GECODE_OPTIMIZE_CONSTRAINTS_HPP + +#include + +namespace Gecode { namespace Optimize { + +struct IndicatorFormulation { + Indicator indicator; + std::optional inactive_gate; + std::vector rows; + std::optional lower_m; + std::optional upper_m; +}; + +/** + * Add activator == active_value => lower <= sum(terms) <= upper. + * The activator must have Binary type. Every required inactive activity bound + * must be finite. M is derived conservatively; no guessed constant is used. + * The returned gate keeps original coefficients and row bounds unchanged. + * Original logical metadata is retained for independent solution validation. + * Successful posting is one revision; errors leave the model unchanged. + */ +IndicatorFormulation add_indicator(Model& model, Variable activator, + bool active_value, + const std::vector& terms, + double lower, double upper, + std::string name = {}); + +/** Remove logical metadata and all its generated rows in one revision. + * The auxiliary gate remains a model variable and can be removed once unused. + */ +void remove_indicator(Model& model, Indicator indicator); + +/** result == AND(inputs); AND(empty) is true. All variables must be Binary. */ +std::vector add_boolean_and(Model& model, Variable result, + const std::vector& inputs, + std::string name = {}); +/** result == OR(inputs); OR(empty) is false. All variables must be Binary. */ +std::vector add_boolean_or(Model& model, Variable result, + const std::vector& inputs, + std::string name = {}); + +namespace Detail { +/** Structural validation of untrusted original indicator metadata/lowerings. */ +void validate_indicators(const ModelSnapshot& model); +} + +}} +#endif diff --git a/gecode/optimize/diagnostics.cpp b/gecode/optimize/diagnostics.cpp new file mode 100644 index 0000000000..b437952012 --- /dev/null +++ b/gecode/optimize/diagnostics.cpp @@ -0,0 +1,306 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { +namespace { +constexpr double infinity = std::numeric_limits::infinity(); + +std::vector partition(const ModelSnapshot& model) { + std::vector parent(model.variables.size()); + std::iota(parent.begin(), parent.end(), 0); + auto root = [&](std::size_t i) { + while (parent[i] != i) { parent[i] = parent[parent[i]]; i = parent[i]; } + return i; + }; + auto join = [&](Variable a, Variable b) { + const auto x = root(a.id), y = root(b.id); + parent[std::max(x, y)] = std::min(x, y); + }; + std::vector guarded(model.variables.size(), false); + std::vector generated(model.rows.size(), false); + for (const auto& indicator : model.indicators) if (indicator.active) { + guarded[indicator.activator.id] = true; + auto include = [&](Variable variable) { + guarded[variable.id] = true; + join(indicator.activator, variable); + }; + if (indicator.inactive_gate) include(*indicator.inactive_gate); + for (const auto& term : indicator.terms) include(term.variable); + for (auto row : indicator.generated_rows) generated[row.id] = true; + } + std::vector groups; + for (const auto& row : model.rows) if (row.active && !generated[row.constraint.id]) { + ConflictGroup group; + group.kind = ConflictGroupKind::Row; + group.row = row.constraint; + group.name = row.name.empty() ? "row " + std::to_string(row.constraint.id) : row.name; + groups.push_back(std::move(group)); + } + for (const auto& variable : model.variables) { + if (!variable.active || guarded[variable.variable.id]) continue; + auto append = [&](ConflictGroupKind kind, const char* suffix) { + ConflictGroup group; + group.kind = kind; + group.variable = variable.variable; + group.name = (variable.name.empty() ? "variable " + std::to_string(variable.variable.id) + : variable.name) + suffix; + groups.push_back(std::move(group)); + }; + if (variable.type == VariableType::Binary || variable.type == VariableType::SemiContinuous || + variable.type == VariableType::SemiInteger) { + append(ConflictGroupKind::VariableDomain, " domain"); + } else { + if (std::isfinite(variable.lower)) append(ConflictGroupKind::LowerBound, " lower bound"); + if (std::isfinite(variable.upper)) append(ConflictGroupKind::UpperBound, " upper bound"); + if (variable.type == VariableType::Integer) append(ConflictGroupKind::Integrality, " integrality"); + } + } + std::map components; + for (const auto& indicator : model.indicators) if (indicator.active) { + auto& group = components[root(indicator.activator.id)]; + group.kind = ConflictGroupKind::IndicatorComponent; + group.indicators.push_back(indicator.indicator); + group.generated_rows.insert(group.generated_rows.end(), indicator.generated_rows.begin(), + indicator.generated_rows.end()); + } + for (const auto& variable : model.variables) if (guarded[variable.variable.id]) + components.at(root(variable.variable.id)).grouped_variables.push_back(variable.variable); + for (auto& entry : components) { + auto& group = entry.second; + group.name = "indicator component " + std::to_string(group.indicators.front().id); + groups.push_back(std::move(group)); + } + return groups; +} + +void relax(ModelSnapshot& model, const ConflictGroup& group) { + auto free_domain = [&](Variable handle) { + auto& variable = model.variables[handle.id]; + variable.type = VariableType::Continuous; + variable.lower = -infinity; + variable.upper = infinity; + }; + auto erase_row = [&](Constraint handle) { + auto& row = model.rows[handle.id]; + row.active = false; + row.terms.clear(); + }; + switch (group.kind) { + case ConflictGroupKind::Row: erase_row(*group.row); break; + case ConflictGroupKind::LowerBound: model.variables[group.variable->id].lower = -infinity; break; + case ConflictGroupKind::UpperBound: model.variables[group.variable->id].upper = infinity; break; + case ConflictGroupKind::Integrality: model.variables[group.variable->id].type = VariableType::Continuous; break; + case ConflictGroupKind::VariableDomain: free_domain(*group.variable); break; + case ConflictGroupKind::IndicatorComponent: + for (auto indicator : group.indicators) model.indicators[indicator.id].active = false; + for (auto row : group.generated_rows) erase_row(row); + for (auto variable : group.grouped_variables) free_domain(variable); + break; + } +} + +ModelSnapshot subset(const ModelSnapshot& original, const std::vector& groups, + const std::vector& kept) { + ModelSnapshot model = original; + model.objective = {}; + for (std::size_t i = 0; i < groups.size(); ++i) if (!kept[i]) relax(model, groups[i]); + return model; +} + +enum class Decision { Unknown, Feasible, Infeasible }; + +ConflictStatus stopped_status(const ConflictResult& result, Termination reason) { + if (result.infeasibility_established) return ConflictStatus::Incomplete; + if (reason == Termination::Unsupported) return ConflictStatus::Unsupported; + if (reason == Termination::InvalidModel) return ConflictStatus::InvalidModel; + if (reason == Termination::BackendError || reason == Termination::NumericalFailure || + reason == Termination::MemoryLimit) return ConflictStatus::Error; + return ConflictStatus::Unknown; +} + +ConflictResult run(const ModelSnapshot& original, const ConflictOptions& options, SolveBudget& budget) { + ConflictResult output; + output.model_id = original.model_id; + output.revision = original.revision; + std::vector groups; + std::vector kept; + auto stop = [&](Termination reason, std::string message) { + output.termination = reason; + output.status = stopped_status(output, reason); + output.message = std::move(message); + }; + auto finish = [&]() { + if (output.infeasibility_established) { + // Compact the existing allocation: reporting an interrupted conflict must + // not need another allocation proportional to its number of groups. + std::size_t retained = 0; + for (std::size_t i = 0; i < groups.size(); ++i) if (kept[i]) { + if (retained != i) groups[retained] = std::move(groups[i]); + ++retained; + } + groups.resize(retained); + output.groups = std::move(groups); + } + if (budget.expired() && (output.status == ConflictStatus::Irreducible || + output.status == ConflictStatus::Feasible)) { + stop(budget.stop_reason().value_or(Termination::Unknown), + "Shared conflict budget stopped before completion"); + output.feasible_witness.clear(); + } + output.elapsed_seconds = budget.elapsed_seconds(); + return std::move(output); + }; + try { + validate_structure(original); + if (std::any_of(original.globals.begin(),original.globals.end(),[](const auto& record){return record.active;})) { + stop(Termination::Unsupported,"Conflict analysis of native globals requires a global-aware feasibility oracle"); + return finish(); + } + if (options.solve.guarantee != Guarantee::Numerical || options.solve.node_limit) { + stop(Termination::Unsupported, + "Conflict analysis supports Numerical evidence and time/cancellation budgets; node budgets need consumed-node reporting"); + return finish(); + } + if (budget.expired()) { + stop(budget.stop_reason().value_or(Termination::Unknown), "Conflict budget stopped before analysis"); + return finish(); + } + groups = partition(original); + kept.assign(groups.size(), true); + auto oracle = [&](const ModelSnapshot& model, std::vector& witness) { + if (budget.expired()) { + stop(budget.stop_reason().value_or(Termination::Unknown), "Shared conflict budget stopped"); + return Decision::Unknown; + } + auto solve_options = options.solve; + solve_options.primal_start.clear(); + solve_options.relative_gap = solve_options.absolute_gap = 0.0; + solve_options.cancellation = budget.cancellation(); + solve_options.time_limit_seconds = budget.remaining_seconds(); + ++output.oracle_calls; + auto result = solve(model, solve_options); + output.backend = result.backend; + output.backend_version = result.backend_version; + if (budget.expired()) { + stop(budget.stop_reason().value_or(Termination::Unknown), "Feasibility oracle returned after the shared budget stopped"); + return Decision::Unknown; + } + if (result.model_id != model.model_id || result.revision != model.revision || + result.guarantee != Guarantee::Numerical) { + stop(Termination::NumericalFailure, "Feasibility oracle returned incompatible identity or evidence"); + return Decision::Unknown; + } + // Interrupted/ambiguous oracles are never advanced, even with an incumbent. + if (result.termination != Termination::Optimal && result.termination != Termination::Infeasible) { + stop(result.termination, "Feasibility oracle did not decide the subset: " + result.message); + return Decision::Unknown; + } + if (result.termination == Termination::Infeasible) { + if (result.has_solution()) { + stop(Termination::NumericalFailure, "Infeasible oracle result also carries a validated solution"); + return Decision::Unknown; + } + return Decision::Infeasible; + } + const auto check = validate(model, result.values, options.solve.feasibility_tolerance, + options.solve.integrality_tolerance); + if (!result.has_solution() || !check.valid) { + stop(Termination::NumericalFailure, "Feasible oracle candidate failed independent subset validation"); + return Decision::Unknown; + } + if (budget.expired()) { + stop(budget.stop_reason().value_or(Termination::Unknown), "Shared conflict budget stopped during candidate validation"); + return Decision::Unknown; + } + witness = std::move(result.values); + return Decision::Feasible; + }; + std::vector witness; + const auto initial = oracle(subset(original, groups, kept), witness); + if (initial == Decision::Unknown) return finish(); + if (initial == Decision::Feasible) { + output.status = ConflictStatus::Feasible; + output.termination = Termination::Optimal; + output.message = "Original constraints have an independently validated numerical feasible assignment"; + output.feasible_witness = std::move(witness); + return finish(); + } + output.infeasibility_established = true; + for (std::size_t i = 0; i < groups.size(); ++i) { + kept[i] = false; + Decision decision; + try { decision = oracle(subset(original, groups, kept), witness); } + catch (...) { kept[i] = true; throw; } + if (decision != Decision::Infeasible) kept[i] = true; + if (decision == Decision::Unknown) return finish(); + if (decision == Decision::Feasible) { + groups[i].necessity_verified = true; + if (options.retain_deletion_witnesses) groups[i].deletion_witness = std::move(witness); + } + } + // Feasible deletion witnesses remain feasible as other groups are relaxed. + // Recheck retained witnesses against the final conflict, independently. + for (std::size_t i = 0; i < groups.size(); ++i) if (kept[i]) { + if (budget.expired()) { + stop(budget.stop_reason().value_or(Termination::Unknown), "Shared conflict budget stopped during final checks"); + return finish(); + } + if (options.retain_deletion_witnesses) { + auto removed = kept; + removed[i] = false; + const auto check = validate(subset(original, groups, removed), groups[i].deletion_witness, + options.solve.feasibility_tolerance, options.solve.integrality_tolerance); + if (!check.valid) { + stop(Termination::NumericalFailure, "Final deletion witness no longer validates; conflict is not promoted"); + return finish(); + } + } + } + if (std::none_of(kept.begin(), kept.end(), [](bool keep) { return keep; })) { + stop(Termination::NumericalFailure, "Oracle reported an unconstrained continuous system infeasible"); + return finish(); + } + output.status = ConflictStatus::Irreducible; + output.termination = Termination::Infeasible; + output.message = "Numerically infeasible and deletion-minimal at the reported group granularity"; + } catch (const ModelError& error) { + stop(Termination::InvalidModel, error.what()); + } catch (const std::bad_alloc&) { + stop(Termination::MemoryLimit, "Conflict analysis allocation failed"); + } catch (const std::exception& error) { + stop(Termination::BackendError, error.what()); + } + return finish(); +} + +ConflictResult invalid(ModelId id, Revision revision, Termination reason, const std::string& message) { + ConflictResult result; + result.model_id = id; + result.revision = revision; + result.termination = reason; + result.status = stopped_status(result, reason); + result.message = message; + return result; +} +} + +ConflictResult analyze_conflict(const ModelSnapshot& model, const ConflictOptions& options) { + try { SolveBudget budget(options.solve); return run(model, options, budget); } + catch (const ModelError& error) { return invalid(model.model_id, model.revision, Termination::InvalidModel, error.what()); } + catch (const std::bad_alloc&) { return invalid(model.model_id, model.revision, Termination::MemoryLimit, "Conflict allocation failed"); } +} + +ConflictResult analyze_conflict(const Model& model, const ConflictOptions& options) { + try { SolveBudget budget(options.solve); return run(model.snapshot(), options, budget); } + catch (const ModelError& error) { return invalid(model.id(), model.revision(), Termination::InvalidModel, error.what()); } + catch (const std::bad_alloc&) { return invalid(model.id(), model.revision(), Termination::MemoryLimit, "Conflict allocation failed"); } +} + +}} diff --git a/gecode/optimize/diagnostics.hpp b/gecode/optimize/diagnostics.hpp new file mode 100644 index 0000000000..cf6432ff80 --- /dev/null +++ b/gecode/optimize/diagnostics.hpp @@ -0,0 +1,76 @@ +/* Numerical conflicts over explicit original-model groups. */ +#ifndef GECODE_OPTIMIZE_DIAGNOSTICS_HPP +#define GECODE_OPTIMIZE_DIAGNOSTICS_HPP + +#include + +namespace Gecode { namespace Optimize { + +enum class ConflictStatus { + Unknown, Feasible, Irreducible, Incomplete, Unsupported, InvalidModel, Error +}; + +enum class ConflictGroupKind { + Row, LowerBound, UpperBound, Integrality, VariableDomain, IndicatorComponent +}; + +struct ConflictGroup { + ConflictGroupKind kind = ConflictGroupKind::Row; + std::string name; + std::optional row; + std::optional variable; + // An indicator component owns these logical records, their generated rows, + // and the complete domains of all variables participating in the component. + std::vector indicators; + std::vector generated_rows; + std::vector grouped_variables; + bool necessity_verified = false; + // Optional feasible assignment after removing this group from the conflict. + // Original slot order, with tombstones; it need not satisfy this group. + std::vector deletion_witness; +}; + +struct ConflictOptions { + SolveOptions solve; + bool retain_deletion_witnesses = false; +}; + +struct ConflictResult { + ModelId model_id = 0; + Revision revision = 0; + ConflictStatus status = ConflictStatus::Unknown; + // Completion: Optimal for Feasible, Infeasible for Irreducible. Otherwise + // the stopping/error reason. This is separate from conflict completeness. + Termination termination = Termination::Unknown; + Guarantee guarantee = Guarantee::Numerical; + std::string backend; + std::string backend_version; + std::string message; + bool infeasibility_established = false; + std::size_t oracle_calls = 0; + double elapsed_seconds = 0.0; + // Empty until original feasibility has been decided as infeasible. During + // interruption this contains the last numerically established infeasible set. + std::vector groups; + // Present only for a feasible original constraint system; original slots. + std::vector feasible_witness; + bool irreducible() const noexcept { return status == ConflictStatus::Irreducible; } +}; + +/** + * Deterministic deletion filtering with numerical LP/MILP feasibility oracles. + * The original objective is replaced privately by zero. All calls share one + * end-to-end time/cancellation budget. Starts are ignored and gaps forced zero. + * Any node limit, Exact or Certified request is explicitly Unsupported. + * See DIAGNOSTICS.md for group granularity and indicator domain ownership. + * Irreducible means deletion-minimal at that granularity, not minimum size or + * an exact infeasibility certificate. Interrupted or ambiguous oracles never + * establish infeasibility or irreducibility. + */ +ConflictResult analyze_conflict(const ModelSnapshot& model, + const ConflictOptions& options = {}); +ConflictResult analyze_conflict(const Model& model, + const ConflictOptions& options = {}); + +}} +#endif diff --git a/gecode/optimize/flatzinc.cpp b/gecode/optimize/flatzinc.cpp new file mode 100644 index 0000000000..ee21b15b68 --- /dev/null +++ b/gecode/optimize/flatzinc.cpp @@ -0,0 +1,766 @@ +/* Original-record compiler and checker, independent of native parser posting. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { +namespace F=Gecode::FlatZinc::Capture; +namespace Detail { +struct FlatZincArtifact { + F::Records source; + ModelSnapshot model; + std::vector mapping; +}; +namespace { +using I=std::int64_t; +constexpr I exact_limit=9007199254740992LL; +struct Failure { FlatZincCompileStatus status; std::string message; F::Location location; }; +[[noreturn]] void bad(const std::string& s) {throw Failure{FlatZincCompileStatus::InvalidInput,s,{}};} +[[noreturn]] void unsupported(const std::string& s) {throw Failure{FlatZincCompileStatus::Unsupported,s,{}};} +[[noreturn]] void limit(const std::string& s) {throw Failure{FlatZincCompileStatus::ResourceLimit,s,{}};} +I plus(I a,I b) { + if((b>0&&a>std::numeric_limits::max()-b)||(b<0&&a::min()-b)) + unsupported("integer arithmetic exceeds the exact compiler range"); + return a+b; +} +I neg(I a) {if(a==std::numeric_limits::min())unsupported("integer negation overflow");return -a;} +I times(I a,I b) { + if(!a||!b)return 0; + if((a==-1&&b==std::numeric_limits::min())||(b==-1&&a==std::numeric_limits::min()))unsupported("integer product overflow"); + if(a>0?(b>0?a>std::numeric_limits::max()/b:b::min()/a): + (b>0?a::min()/b:a::max()/b))unsupported("integer product overflow"); + return a*b; +} +double number(I a) {if(a < -exact_limit || a > exact_limit)unsupported("integer cannot be represented exactly as a model number");return static_cast(a);} +using Key=std::pair; +Key key(F::Reference r) { + switch(r.type){case F::Type::Integer:return {0,r.index};case F::Type::Boolean:return {1,r.index}; + case F::Type::Float:case F::Type::Set:unsupported("FlatZinc float and set variables are not supported by this compiler");} + bad("invalid variable namespace"); +} +struct Meter { + const FlatZincCompileOptions& options; SolveBudget budget; std::size_t work=0; + static SolveOptions solve_options(const FlatZincCompileOptions& o) {SolveOptions s;s.time_limit_seconds=o.time_limit_seconds;s.cancellation=o.cancellation;return s;} + explicit Meter(const FlatZincCompileOptions& o):options(o),budget(solve_options(o)) {} + void check() const { + if(budget.cancelled())throw Failure{FlatZincCompileStatus::Cancelled,"FlatZinc compilation cancelled",{}}; + if(budget.time_limit_reached())throw Failure{FlatZincCompileStatus::TimeLimit,"FlatZinc compilation deadline",{}}; + } + void add(std::size_t n=1) {check();if(n>options.max_work-work)limit("FlatZinc compiler work limit");work+=n;} +}; +void shape(const F::Value& v,Meter& m,std::size_t depth=0) { + if(depth>m.options.max_value_depth)limit("FlatZinc value nesting limit"); + m.add();m.add(v.text.size());m.add(v.set.values.size()); + switch(v.kind){case F::ValueKind::Integer:case F::ValueKind::Boolean:case F::ValueKind::Float: + case F::ValueKind::Set:case F::ValueKind::Reference:case F::ValueKind::Array:case F::ValueKind::Atom: + case F::ValueKind::String:case F::ValueKind::Call:break;default:bad("invalid captured value tag");} + // Account for even inactive public fields before copying untrusted records. + for(const auto& item:v.elements)shape(item,m,depth+1); +} +void constraint_shape(const F::Constraint& c,Meter& m) { + m.add();m.add(c.id.size());m.add(c.location.source.size()); + for(const auto& a:c.arguments)shape(a,m); + for(const auto& a:c.annotations)shape(a,m); +} +void all_shapes(const F::Records& r,Meter& m) { + if(r.raw_variables.size()>m.options.max_variables||r.variables.size()>m.options.max_variables)limit("FlatZinc variable limit"); + for(const auto* vars:{&r.raw_variables,&r.variables})for(const auto& v:*vars) { + m.add();m.add(v.name.size());m.add(v.domain.integers.values.size());shape(v.value,m); + } + for(const auto* rows:{&r.raw_domains,&r.raw_constraints,&r.domains,&r.constraints}) { + if(rows->size()>m.options.max_constraints)limit("FlatZinc constraint record limit"); + for(const auto& c:*rows)constraint_shape(c,m); + } + m.add(r.coverage.size());m.add(r.source.size());shape(r.solve.objective,m);m.add(r.solve.location.source.size()); + for(const auto& a:r.solve.annotations)shape(a,m); + for(const auto& a:r.declaration_annotations) { + m.add();m.add(a.name.size());m.add(a.location.source.size());for(const auto& x:a.annotations)shape(x,m); + } + for(const auto& o:r.output){m.add();m.add(o.name.size());shape(o.expression,m);} +} +std::pair interval(const F::SetLiteral& s,Meter* meter=nullptr) { + if(s.interval){number(s.lower);number(s.upper);return {s.lower,s.upper};} + auto values=s.values; + if(meter)meter->add(values.size()); + if(values.empty())return {1,0}; + std::sort(values.begin(),values.end());values.erase(std::unique(values.begin(),values.end()),values.end()); + for(std::size_t i=0;i& array(const F::Value& v) {if(v.kind!=F::ValueKind::Array)bad("expected a captured array");return v.elements;} +I literal(const F::Value& v) {if(v.kind!=F::ValueKind::Integer)bad("expected an integer literal");number(v.integer);return v.integer;} +void arity(const F::Constraint& c,std::size_t n){if(c.arguments.size()!=n)bad("wrong arity for "+c.id);} +void annotation(const F::Value& v,bool declaration=false) { + if(v.kind!=F::ValueKind::Atom&&v.kind!=F::ValueKind::Call)unsupported("unrecognized FlatZinc annotation form"); + const auto& n=v.text; + // MiniZinc 2.10.1 stdlib/stdlib_ann.mzn, context annotations: + // https://github.com/MiniZinc/libminizinc/blob/2.10.1/share/minizinc/std/stdlib/stdlib_ann.mzn + // Compiler context markers do not change the posted relation or its exact + // original-source check. Admit only the named atoms, never unknown calls. + if(n=="var_is_introduced"||n=="is_defined_var"||n=="domain"||n=="bounds"||n=="boundsD"||n=="boundsR"||n=="boundsZ"|| + n=="ctx_pos"||n=="ctx_neg"||n=="ctx_mix") { + if(v.kind!=F::ValueKind::Atom||!v.elements.empty())bad("malformed marker annotation");return; + } + if(n=="defines_var") {if(v.kind!=F::ValueKind::Call||v.elements.size()!=1||v.elements[0].kind!=F::ValueKind::Reference)bad("malformed defines_var annotation");return;} + if(n=="mzn_path") {if(v.kind!=F::ValueKind::Call||v.elements.size()!=1||v.elements[0].kind!=F::ValueKind::String)bad("malformed mzn_path annotation");return;} + if(declaration&&n=="output_var") {if(v.kind!=F::ValueKind::Atom||!v.elements.empty())bad("malformed output_var annotation");return;} + if(declaration&&n=="output_array") { + if(v.kind!=F::ValueKind::Call||v.elements.size()!=1)bad("malformed output_array annotation"); + const auto& dimensions=array(v.elements[0]);if(dimensions.empty())bad("output_array needs at least one dimension"); + for(const auto& d:dimensions){if(d.kind!=F::ValueKind::Set)bad("output dimension is not an integer set");interval(d.set);} + return; + } + unsupported("unhandled FlatZinc annotation: "+n); +} +struct Bounds { + std::optional lower,upper; bool empty=false; + // A finite member list is never replaced by its hull until contiguity is proved. + // Intervals stay symbolic, so a sparse set can span the full exact source range. + std::optional> members; + void intersect(I lo,I hi){empty=empty||lo>hi;lower=lower?std::max(*lower,lo):lo;upper=upper?std::min(*upper,hi):hi;if(*lower>*upper)empty=true;} + void intersect(const F::SetLiteral& input,Meter& meter) { + if(input.interval){number(input.lower);number(input.upper);intersect(input.lower,input.upper);return;} + if(input.values.size()>meter.options.max_nonzeros)limit("finite domain member storage limit"); + meter.add(input.values.size()); + for(const auto value:input.values){meter.check();number(value);} + auto allowed=input.values; + // Comparisons and filtering are charged, not just the final member count. + // Any thrown limit discards private compiler state, never the source records. + std::sort(allowed.begin(),allowed.end(),[&](I a,I b){meter.add();return asize(),allowed.size());meter.add(reserve); + std::vector common;common.reserve(reserve); + std::size_t i=0,j=0; + while(isize()&&jempty())intersect(1,0); + else intersect(members->front(),members->back()); + } + void finish(Meter& meter) { + if(!members)return; + std::size_t kept=0; + for(const auto value:*members){meter.add();if(!empty&&(!lower||value>=*lower)&&(!upper||value<=*upper))(*members)[kept++]=value;} + members->resize(kept); + if(!kept){intersect(1,0);members.reset();return;} + intersect(members->front(),members->back()); + bool contiguous=true; + for(std::size_t i=1;i coefficient; I constant=0;}; +void combine(Form& a,const Form& b,I multiplier=1) { + a.constant=plus(a.constant,times(b.constant,multiplier)); + for(const auto& e:b.coefficient) {auto& value=a.coefficient[e.first];value=plus(value,times(e.second,multiplier));if(!value)a.coefficient.erase(e.first);} +} +bool identifier(const std::string& s) { + if(s.empty())return false; + const auto first=[](unsigned char c){return(c>='a'&&c<='z')||(c>='A'&&c<='Z')||c=='_';}; + if(!first(s[0]))return false; + for(unsigned char c:s)if(!first(c)&&!(c>='0'&&c<='9'))return false; + return true; +} +const F::Value* output_array(const F::Records& source,const std::string& name) { + const F::Value* found=nullptr; + for(const auto& d:source.declaration_annotations)if(d.name==name)for(const auto& a:d.annotations)if(a.text=="output_array") { + if(found)bad("duplicate output_array annotation");found=&a; + } + return found; +} +const std::vector& output_values(const F::Value& value) { + const auto& a=array(value); + // Legacy printer layout contains fixed string fragments around one value array. + if(std::all_of(a.begin(),a.end(),[](const F::Value& x){return x.kind==F::ValueKind::Reference||x.kind==F::ValueKind::Integer||x.kind==F::ValueKind::Boolean;}))return a; + const std::vector* found=nullptr; + for(const auto& v:a){if(v.kind==F::ValueKind::Array){if(found)bad("ambiguous output value arrays");found=&v.elements;}else if(v.kind!=F::ValueKind::String)bad("unsupported output layout");} + if(!found)bad("output layout has no typed values");return *found; +} +} + +struct FlatZincCompiler { + const F::Records& source; const FlatZincCompileOptions& options; Meter meter; + std::map position; + std::vector parent; + std::vector domains; + std::vector handles; + Model model;std::size_t nonzeros=0,rows=0,variables=0; + std::map constants; + explicit FlatZincCompiler(const F::Records& s,const FlatZincCompileOptions& o):source(s),options(o),meter(o) {} + std::size_t lookup(F::Reference ref) const {auto p=position.find(key(ref));if(p==position.end())bad("unknown captured variable reference");return p->second;} + std::size_t representative(std::size_t n) { + auto root=n;std::size_t steps=0; + while(parent[root]!=root){meter.add();if(++steps>parent.size())bad("cyclic captured variable alias");root=parent[root];} + while(parent[n]!=n){meter.add();auto next=parent[n];parent[n]=root;n=next;}return root; + } + void typed(const F::Value& v,F::Type type) const { + if(v.kind==F::ValueKind::Reference){if(v.reference.type!=type)bad("captured operand has the wrong variable namespace");lookup(v.reference);return;} + if(type==F::Type::Integer&&v.kind==F::ValueKind::Integer){number(v.integer);return;} + if(type==F::Type::Boolean&&v.kind==F::ValueKind::Boolean)return; + bad("captured operand has the wrong literal type"); + } + Form term(const F::Value& v,F::Type type) { + typed(v,type);meter.add();Form f; + if(v.kind==F::ValueKind::Reference)f.coefficient.emplace(handles[lookup(v.reference)].id,1); + else f.constant=type==F::Type::Integer?v.integer:static_cast(v.boolean); + return f; + } + Form difference(const F::Value& a,const F::Value& b,F::Type type) {auto f=term(a,type);combine(f,term(b,type),-1);return f;} + void post(Form f,std::optional lo,std::optional hi) { + meter.add();if(rows==options.max_constraints)limit("generated FlatZinc row limit"); + if(f.coefficient.size()>options.max_nonzeros-nonzeros)limit("generated FlatZinc nonzero limit"); + meter.add(f.coefficient.size());std::vector terms;terms.reserve(f.coefficient.size()); + for(const auto& e:f.coefficient)terms.push_back({{model.id(),e.first},number(e.second)}); + const auto low=lo?number(plus(*lo,neg(f.constant))):-std::numeric_limits::infinity(); + const auto high=hi?number(plus(*hi,neg(f.constant))):std::numeric_limits::infinity(); + model.add_row(terms,low,high);++rows;nonzeros+=terms.size(); + } + void capacity(std::size_t added_variables,std::size_t added_rows,std::size_t added_nonzeros) { + meter.add(); + if(added_variables>options.max_variables-variables)limit("generated FlatZinc variable limit"); + if(added_rows>options.max_constraints-rows)limit("generated FlatZinc constraint limit"); + if(added_nonzeros>options.max_nonzeros-nonzeros)limit("generated FlatZinc nonzero limit"); + } + Variable global_term(const F::Value& value) { + typed(value,F::Type::Integer);meter.add(); + if(value.kind==F::ValueKind::Reference)return handles[lookup(value.reference)]; + const auto found=constants.find(value.integer);if(found!=constants.end())return found->second; + capacity(1,0,0);auto v=model.add_integer(number(value.integer),number(value.integer));++variables; + constants.emplace(value.integer,v);return v; + } + I fixed_integer(const F::Value& value,const char* role) { + typed(value,F::Type::Integer);meter.add(); + if(value.kind==F::ValueKind::Integer)return value.integer; + const auto& original=domains[representative(lookup(value.reference))]; + // A contradictory domain uses a dummy model slot; it proves no fixed + // source parameter. Ordinary equality rows are never propagated here. + if(original.empty||!original.lower||!original.upper||*original.lower!=*original.upper) + unsupported(std::string("cumulative ")+role+" must be a literal or an original singleton integer domain"); + return *original.lower; + } + void implication(const F::Value& guard,bool active,Form form,std::optional lo,std::optional hi) { + typed(guard,F::Type::Boolean);meter.add(); + if(guard.kind==F::ValueKind::Boolean){if(guard.boolean==active)post(std::move(form),lo,hi);return;} + const auto control=handles[lookup(guard.reference)]; + const auto& domain=model.variable(control); + if(domain.lower==domain.upper){if((domain.lower==1)==active)post(std::move(form),lo,hi);return;} + // Reserve the maximum footprint before the transactional helper allocates: + // one gate, its two-entry equality, and one row per finite side. + const std::size_t sides=std::size_t(bool(lo))+std::size_t(bool(hi)); + if(form.coefficient.size()>options.max_nonzeros)limit("indicator expression exceeds nonzero limit"); + const std::size_t width=form.coefficient.size()+1; + if(width>std::numeric_limits::max()/sides || sides*width>std::numeric_limits::max()-2) + limit("indicator nonzero count overflow"); + capacity(1,1+sides,2+sides*width);meter.add(2+sides*width); + std::vector terms;terms.reserve(form.coefficient.size()); + for(const auto& e:form.coefficient)terms.push_back({{model.id(),e.first},number(e.second)}); + const double lower=lo?number(plus(*lo,neg(form.constant))):-std::numeric_limits::infinity(); + const double upper=hi?number(plus(*hi,neg(form.constant))):std::numeric_limits::infinity(); + const auto added=add_indicator(model,control,active,terms,lower,upper); + if(added.inactive_gate)++variables; + rows+=added.rows.size();for(const auto r:added.rows)nonzeros+=model.row(r).terms.size(); + } + void restrict_domain(const F::Constraint& c) { + if(c.id!="int_in")return;arity(c,2); + if(c.arguments[1].kind!=F::ValueKind::Set)bad("int_in requires an integer set literal"); + const auto& value=c.arguments[0]; + if(value.kind==F::ValueKind::Reference)domains[representative(lookup(value.reference))].intersect(c.arguments[1].set,meter); + else { + if(value.kind!=F::ValueKind::Integer&&value.kind!=F::ValueKind::Boolean)bad("int_in has a noninteger operand"); + // Admit every set value even when the eventual constant predicate is false. + Bounds checked;checked.intersect(c.arguments[1].set,meter); + } + } + void declarations() { + parent.resize(source.raw_variables.size());std::iota(parent.begin(),parent.end(),0);domains.resize(parent.size());handles.resize(parent.size()); + for(std::size_t i=0;i(v.value.boolean);domains[root].intersect(value,value);} + } + for(const auto* input:{&source.raw_domains,&source.raw_constraints})for(const auto& c:*input) { + try {restrict_domain(c);}catch(Failure& f){f.location=c.location;throw;} + } + for(std::size_t i=0;i=0 && hi<=1); + handles[i]=model.add_variable(binary?VariableType::Binary:VariableType::Integer,number(lo),number(hi),v.name); + ++variables; + if(d.empty)post({},1,1); + else if(d.members) { + if(d.members->size()==std::numeric_limits::max())limit("finite domain cell count overflow"); + const auto cells=d.members->size()+1;capacity(0,1,cells);meter.add(cells); + std::vector> tuples;tuples.reserve(d.members->size()); + for(const auto value:*d.members){meter.add();tuples.push_back({value});} + add_table(model,{handles[i]},tuples);++rows;nonzeros+=cells; + } + } + for(std::size_t i=0;i(0):std::nullopt,0); + if(id=="int_le_reif")implication(a[2],false,std::move(f),1,{}); + return; + } + if(id=="int_lin_le_reif"||id=="int_lin_le_imp") { + arity(c,4);const auto& coefficients=array(a[0]);const auto& operands=array(a[1]); + if(coefficients.size()!=operands.size())bad("linear coefficient/operand lengths differ"); + Form f;for(std::size_t i=0;ioptions.max_nonzeros||flat.size()>options.max_nonzeros-values.size())limit("table payload cell limit"); + const auto cells=values.size()+flat.size();capacity(0,1,cells);meter.add(cells); + // Validate all parameter cells before creating private literal slots. + for(const auto& entry:flat){meter.add();literal(entry);} + std::vector arguments;arguments.reserve(values.size()); + for(const auto& value:values)arguments.push_back(global_term(value)); + const auto tuple_count=flat.size()/values.size();meter.add(tuple_count); + std::vector> tuples;tuples.reserve(tuple_count); + for(std::size_t offset=0;offset tuple;tuple.reserve(values.size()); + for(std::size_t j=0;jstates)bad("regular initial state is outside 1..Q"); + if(a[5].kind!=F::ValueKind::Set)bad("regular final states must be a literal integer set"); + const I expected=times(states,symbols); + if(static_cast(expected)>std::numeric_limits::max()) + unsupported("regular transition matrix count exceeds native size arithmetic"); + if(flat.size()!=static_cast(expected))bad("regular transition cell count must equal Q*S"); + const auto& final_set=a[5].set;std::size_t final_count=0; + if(final_set.interval){ + number(final_set.lower);number(final_set.upper); + if(final_set.lower<=final_set.upper){ + if(final_set.lower<1||final_set.upper>states)bad("regular final state is outside 1..Q"); + const auto count=static_cast(plus(plus(final_set.upper,neg(final_set.lower)),1)); + if(count>std::numeric_limits::max())unsupported("regular final set exceeds native size arithmetic"); + final_count=static_cast(count); + } + }else{ + final_count=final_set.values.size(); + for(const auto state:final_set.values){meter.add();number(state);if(state<1||state>states)bad("regular final state is outside 1..Q");} + } + // Bound retained input cells and worst-case expanded sparse edges before + // any payload allocation or parameter-set interval expansion. + std::size_t cells=0; + for(const auto count:{std::size_t(3),word.size(),flat.size(),final_count}){ + if(count>options.max_nonzeros-cells)limit("regular payload cell limit");cells+=count; + } + capacity(0,1,cells);meter.add(cells); + std::size_t edge_count=0; + for(const auto& value:flat){meter.add();const I target=literal(value);if(target<0||target>states)bad("regular transition target is outside 0..Q");if(target)++edge_count;} + if(edge_count>(options.max_nonzeros-cells)/3)limit("regular sparse transition cell limit"); + cells+=3*edge_count;capacity(0,1,cells);meter.add(3*edge_count); + for(const auto& value:word){meter.add();typed(value,F::Type::Integer);} + std::vector transitions;transitions.reserve(edge_count); + for(std::size_t i=0;i(i/static_cast(symbols)), + static_cast(i%static_cast(symbols))+1,static_cast(target-1)}); + } + std::vector finals;finals.reserve(final_count); + for(std::size_t i=0;i(i)):final_set.values[i];finals.push_back(static_cast(state-1));} + std::vector variables;variables.reserve(word.size());for(const auto& value:word)variables.push_back(global_term(value)); + add_regular(model,variables,static_cast(states),static_cast(initial-1),transitions,finals); + ++rows;nonzeros+=cells;return; + } + if(id=="gecode_circuit") { + arity(c,2);const I offset=literal(a[0]);const auto& values=array(a[1]); + if(values.empty())bad("gecode_circuit requires a nonempty successor array"); + if(offset<0)unsupported("gecode_circuit requires a nonnegative literal offset"); + if(values.size()-1>static_cast(std::numeric_limits::max())) + unsupported("circuit index count exceeds exact integer arithmetic"); + number(plus(offset,static_cast(values.size()-1))); + capacity(0,1,values.size());meter.add(values.size()); + // Every position is a source node, including repeated handles/literals. + // Do not deduplicate successors or infer a base from output dimensions. + for(const auto& value:values){meter.add();typed(value,F::Type::Integer);} + std::vector successors;successors.reserve(values.size()); + for(const auto& value:values)successors.push_back(global_term(value)); + add_circuit(model,successors,offset);++rows;nonzeros+=successors.size();return; + } + if(id=="gecode_cumulatives"||id=="cumulatives") { + if(a.size()==6||a.size()==7)unsupported("multi-machine cumulative signatures are not supported"); + arity(c,4);const auto& starts=array(a[0]);const auto& duration_values=array(a[1]);const auto& height_values=array(a[2]); + if(starts.size()!=duration_values.size()||starts.size()!=height_values.size()) + bad("cumulative starts, durations and heights must have equal lengths"); + if(!options.max_nonzeros||starts.size()>(options.max_nonzeros-1)/3) + limit("cumulative payload cell limit"); + const auto cells=3*starts.size()+1;capacity(0,1,cells);meter.add(cells); + const I bound=fixed_integer(a[3],"capacity"); + if(bound<0)unsupported("cumulative capacity must be nonnegative in the fixed-data interface"); + std::vector durations,heights;durations.reserve(starts.size());heights.reserve(starts.size()); + for(std::size_t i=0;i arguments;arguments.reserve(starts.size()); + for(const auto& start:starts)arguments.push_back(global_term(start)); + add_cumulative(model,arguments,durations,heights,bound);++rows;nonzeros+=cells;return; + } + if(id=="all_different_int") { + arity(c,1);const auto& values=array(a[0]);capacity(0,1,values.size());meter.add(values.size()); + std::vector terms;terms.reserve(values.size());for(const auto& v:values)terms.push_back(global_term(v)); + add_all_different(model,terms);++rows;nonzeros+=terms.size();return; + } + if(id=="array_int_element"||id=="array_var_int_element") { + arity(c,3);const auto& values=array(a[1]); + if(id=="array_int_element")for(const auto& v:values) + if(v.kind!=F::ValueKind::Integer)bad("array_int_element requires a parameter integer array"); + if(values.size()>options.max_nonzeros || values.size()>std::numeric_limits::max()-2) + limit("element expression exceeds nonzero limit"); + capacity(0,1,values.size()+2);meter.add(values.size()+2); + auto index=global_term(a[0]),result=global_term(a[2]); + std::vector terms;terms.reserve(values.size());for(const auto& v:values)terms.push_back(global_term(v)); + add_element(model,index,terms,result,1);++rows;nonzeros+=terms.size()+2;return; + } + if(id=="int_in") { + arity(c,2);if(a[1].kind!=F::ValueKind::Set)bad("int_in requires a set"); + const auto type=a[0].kind==F::ValueKind::Boolean?F::Type::Boolean:a[0].kind==F::ValueKind::Reference?a[0].reference.type:F::Type::Integer; + const auto f=term(a[0],type); + // Every reference restriction was intersected exactly before declarations. + // Its raw predicate remains in the owning source and independent checker. + if(a[0].kind==F::ValueKind::Reference)return; + bool member=false;const auto& set=a[1].set; + if(set.interval)member=f.constant>=set.lower&&f.constant<=set.upper; + else for(const auto value:set.values){meter.add();if(value==f.constant)member=true;} + if(!member)post({},1,1);return; + } + if(id=="int_eq"||id=="int_le"||id=="int_lt"||id=="int_ge"||id=="int_gt"||id=="bool_eq"||id=="bool_le") { + arity(c,2);auto f=difference(a[0],a[1],id.rfind("bool_",0)==0?F::Type::Boolean:F::Type::Integer); + if(id=="int_eq"||id=="bool_eq")post(f,0,0); + else if(id=="int_le"||id=="bool_le")post(f,{},0); + else if(id=="int_lt")post(f,{},-1); + else if(id=="int_ge")post(f,0,{});else post(f,1,{});return; + } + if(id=="int_plus"||id=="int_minus"||id=="bool2int"||id=="bool_not") { + if(id=="int_plus"||id=="int_minus"){arity(c,3);auto f=term(a[0],F::Type::Integer);combine(f,term(a[1],F::Type::Integer),id=="int_plus"?1:-1);combine(f,term(a[2],F::Type::Integer),-1);post(f,0,0);} + else {arity(c,2);auto f=term(a[0],F::Type::Boolean);combine(f,term(a[1],id=="bool2int"?F::Type::Integer:F::Type::Boolean),id=="bool2int"?-1:1);post(f,id=="bool2int"?0:1,id=="bool2int"?0:1);}return; + } + if(id=="int_lin_eq"||id=="int_lin_le"||id=="bool_lin_eq"||id=="bool_lin_le") { + arity(c,3);const auto& coeff=array(a[0]);const auto& values=array(a[1]);if(coeff.size()!=values.size())bad("linear coefficient/operand lengths differ"); + Form f;for(std::size_t i=0;i(0):std::nullopt,0);return; + } + if(id=="bool_and"||id=="bool_or"||id=="array_bool_and"||id=="array_bool_or") { + const bool is_array=id.rfind("array_",0)==0,and_op=id.find("and")!=std::string::npos; + arity(c,is_array?2:3);const auto values=is_array?array(a[0]):std::vector{a[0],a[1]};auto result=term(a[is_array?1:2],F::Type::Boolean);Form sum; + for(const auto& value:values){auto operand=term(value,F::Type::Boolean);combine(sum,operand);auto f=result;combine(f,operand,-1);if(and_op)post(f,{},0);else post(f,0,{});} + auto last=result;combine(last,sum,-1); + if(and_op)post(last,plus(1,neg(static_cast(values.size()))),{});else post(last,{},0);return; + } + if(id=="bool_clause") { + arity(c,2);const auto& positive=array(a[0]);const auto& negative=array(a[1]);Form f; + for(const auto& x:positive)combine(f,term(x,F::Type::Boolean)); + for(const auto& x:negative)combine(f,term(x,F::Type::Boolean),-1); + post(f,plus(1,neg(static_cast(negative.size()))),{});return; + } + unsupported("unsupported complete FlatZinc predicate: "+id); + } + void outputs() { + std::set names; + for(const auto& d:source.declaration_annotations)for(const auto& a:d.annotations){annotation(a,true);annotation_references(a);} + for(const auto& o:source.output) { + if(!identifier(o.name)||!names.insert(o.name).second)bad("invalid or duplicate FlatZinc output name"); + const auto scalar=[&](const F::Value& value){ + if(value.kind==F::ValueKind::Reference)lookup(value.reference); + else if(value.kind==F::ValueKind::Integer)number(value.integer); + else if(value.kind!=F::ValueKind::Boolean)unsupported("unsupported FlatZinc output value"); + }; + if(const auto* ann=output_array(source,o.name)) { + const auto& dimensions=array(ann->elements[0]);I count=1; + for(const auto& d:dimensions){auto range=interval(d.set,&meter);count=times(count,range.first>range.second?0:plus(plus(range.second,neg(range.first)),1));} + const auto& values=output_values(o.expression); + if(count<0||static_cast(count)!=values.size())bad("FlatZinc output array dimensions do not match its values"); + for(const auto& v:values)scalar(v); + }else scalar(o.expression); + } + for(const auto& d:source.declaration_annotations)for(const auto& a:d.annotations) + if((a.text=="output_var"||a.text=="output_array")&&!names.count(d.name))bad("requested FlatZinc output is missing from the captured plan"); + } + CompiledFlatZinc run() { + all_shapes(source,meter);meter.check();declarations();outputs(); + // Search controls are not implemented by recording their text. + if(!source.solve.annotations.empty())unsupported("FlatZinc search annotations are not supported by the explicit compiler"); + for(const auto* input:{&source.raw_domains,&source.raw_constraints})for(const auto& c:*input) { + try {compile_constraint(c);}catch(Failure& f){f.location=c.location;throw;} + } + switch(source.solve.method){ + case F::Method::Satisfy:if(source.solve.has_objective)bad("satisfaction request has an objective");model.minimize({});break; + case F::Method::Minimize:case F::Method::Maximize:{ + if(!source.solve.has_objective)bad("optimization request lacks its original objective"); + auto f=term(source.solve.objective,F::Type::Integer);std::vector terms; + for(const auto& e:f.coefficient)terms.push_back({{model.id(),e.first},number(e.second)}); + model.set_objective(terms,source.solve.method==F::Method::Minimize?ObjectiveSense::Minimize:ObjectiveSense::Maximize,number(f.constant));break; + } + default:bad("invalid FlatZinc solve method"); + } + // Reserve copy work before publishing owned source/model data. + meter.add(meter.work);auto data=std::make_shared();data->source=source;data->model=model.snapshot(); + data->mapping.reserve(handles.size());for(std::size_t i=0;imapping.push_back({source.raw_variables[i].reference,handles[i]}); + meter.check();return CompiledFlatZinc(std::move(data)); + } +}; + +namespace { +struct SourceEvaluator { + std::map values; + I value(const F::Value& v) const { + switch(v.kind){ + case F::ValueKind::Integer:return v.integer; + case F::ValueKind::Boolean:return v.boolean?1:0; + case F::ValueKind::Reference:{auto p=values.find(key(v.reference));if(p==values.end())bad("missing original variable value");return p->second;} + default:bad("noninteger original expression"); + } + } + bool member(I x,const F::SetLiteral& set) const { + return set.interval?(x>=set.lower&&x<=set.upper):std::find(set.values.begin(),set.values.end(),x)!=set.values.end(); + } + bool relation(const F::Constraint& c) const { + const auto& a=c.arguments;const auto& id=c.id; + if(id=="int_le_reif")return (value(a[0])<=value(a[1]))==(value(a[2])!=0); + if(id=="int_le_imp")return value(a[2])==0||value(a[0])<=value(a[1]); + if(id=="int_eq_imp")return value(a[2])==0||value(a[0])==value(a[1]); + if(id=="int_lin_le_reif"||id=="int_lin_le_imp") { + if(id=="int_lin_le_imp"&&value(a[3])==0)return true; + I sum=0;for(std::size_t i=0;isymbols)return false; + if(state<1||state>states)bad("unadmitted original regular state"); + const I index=plus(times(state-1,symbols),symbol-1); + if(index<0||static_cast(index)>=flat.size())bad("unadmitted original regular matrix shape"); + state=flat[static_cast(index)].integer;if(!state)return false; + } + return member(state,a[5].set); + } + if(id=="gecode_circuit") { + const I offset=a[0].integer;const auto& successors=a[1].elements; + if(successors.empty())bad("unadmitted empty original circuit"); + // Read raw source terms in their original order. Range-check before + // subtraction/indexing; never use the compiled CircuitData as evidence. + std::vector visited(successors.size(),false);std::size_t node=0; + for(std::size_t step=0;step(index)>=successors.size())return false; + node=static_cast(index); + } + return node==0; + } + if(id=="gecode_cumulatives"||id=="cumulatives") { + // Fixed parameters are read again from their original source slots. The + // compiler's embedded constants and generated global are not evidence. + struct Event {I time,height;bool start;}; + const auto size=a[0].elements.size();const I bound=value(a[3]); + if(size>std::numeric_limits::max()/2)bad("original cumulative event count overflow"); + std::vector events;events.reserve(2*size); + for(std::size_t i=0;ibound-used)return false;used=plus(used,event.height);} + else used=plus(used,neg(event.height)); + } + return true; + } + if(id=="all_different_int") { + std::set assigned;for(const auto& v:a[0].elements)if(!assigned.insert(value(v)).second)return false;return true; + } + if(id=="array_int_element"||id=="array_var_int_element") { + const I index=value(a[0]);if(index<1||static_cast(index)>a[1].elements.size())return false; + return value(a[1].elements[static_cast(index-1)])==value(a[2]); + } + if(id=="int_in")return member(value(a[0]),a[1].set); + if(id=="int_eq"||id=="bool_eq")return value(a[0])==value(a[1]); + if(id=="int_le"||id=="bool_le")return value(a[0])<=value(a[1]); + if(id=="int_lt")return value(a[0])=value(a[1]); + if(id=="int_gt")return value(a[0])>value(a[1]); + if(id=="int_plus")return plus(value(a[0]),value(a[1]))==value(a[2]); + if(id=="int_minus")return plus(value(a[0]),neg(value(a[1])))==value(a[2]); + if(id=="bool2int")return value(a[0])==value(a[1]); + if(id=="bool_not")return value(a[0])!=value(a[1]); + if(id=="int_lin_eq"||id=="int_lin_le"||id=="bool_lin_eq"||id=="bool_lin_le") { + I sum=0;for(std::size_t i=0;i(id=="bool_and"?(left&&right):(left||right)); + } + if(id=="array_bool_and"||id=="array_bool_or") { + const bool conjunction=id=="array_bool_and";bool actual=conjunction; + for(const auto& x:a[0].elements)actual=conjunction?(actual&&value(x)!=0):(actual||value(x)!=0); + return value(a[1])==static_cast(actual); + } + if(id=="bool_clause") { + for(const auto& x:a[0].elements)if(value(x)==1)return true; + for(const auto& x:a[1].elements)if(value(x)==0)return true; + return false; + } + bad("unadmitted predicate reached original-source evaluator"); + } + void declarations(const F::Records& source) const { + for(const auto& v:source.raw_variables) { + const auto current=values.at(key(v.reference)); + if(v.reference.type==F::Type::Boolean&&(current<0||current>1))bad("original Boolean domain violation"); + if(v.alias){if(current!=values.at(key(v.target)))bad("original declaration alias violation");continue;} + if(v.domain.present&&!member(current,v.domain.integers))bad("original declared domain violation"); + if(v.assigned&¤t!=value(v.value))bad("original fixed value violation"); + } + for(const auto* input:{&source.raw_domains,&source.raw_constraints})for(const auto& c:*input) + if(!relation(c))bad("original FlatZinc predicate is false: "+c.id); + } +}; +} +} // Detail + +CompiledFlatZinc::CompiledFlatZinc(std::shared_ptr data):data_(std::move(data)) {} +const ModelSnapshot& CompiledFlatZinc::model() const {if(!data_)throw ModelError("moved-from compiled FlatZinc artifact");return data_->model;} +const F::Records& CompiledFlatZinc::source() const {if(!data_)throw ModelError("moved-from compiled FlatZinc artifact");return data_->source;} +const std::vector& CompiledFlatZinc::variables() const {if(!data_)throw ModelError("moved-from compiled FlatZinc artifact");return data_->mapping;} +FlatZincCompileResult compile_flatzinc(const F::Records& source,const FlatZincCompileOptions& options) { + FlatZincCompileResult result; + try { + if(std::isnan(options.time_limit_seconds)||options.time_limit_seconds<0)throw ModelError("invalid FlatZinc compile time limit"); + Detail::FlatZincCompiler compiler(source,options); + try {result.compiled=compiler.run();result.status=FlatZincCompileStatus::Complete;result.message="complete bounded integer/Boolean FlatZinc source compilation";} + catch(...){result.work=compiler.meter.work;throw;} + result.work=compiler.meter.work; + }catch(const Detail::Failure& error){result.compiled.reset();result.status=error.status;result.message=error.message;result.location=error.location;} + catch(const std::bad_alloc&){result.compiled.reset();result.status=FlatZincCompileStatus::ResourceLimit;result.message="FlatZinc compiler allocation failed";} + catch(const ModelError& e){result.compiled.reset();result.status=FlatZincCompileStatus::InvalidInput;result.message=e.what();} + catch(const std::exception& e){result.compiled.reset();result.status=FlatZincCompileStatus::Error;result.message=e.what();} + return result; +} +FlatZincValidation validate_flatzinc(const CompiledFlatZinc& compiled,const SolveResult& result,double tolerance) { + FlatZincValidation check; + try { + if(!std::isfinite(tolerance)||tolerance<0||tolerance>=0.5)throw ModelError("invalid original-source rounding tolerance"); + const auto& model=compiled.model(); + if(result.model_id!=model.model_id||result.revision!=model.revision)Detail::bad("foreign or stale FlatZinc result"); + if(!result.has_solution()||result.values.size()!=model.variables.size()||result.active_variables.size()!=model.variables.size())Detail::bad("missing full FlatZinc witness"); + std::vector rounded=result.values; + for(std::size_t i=0;i(Detail::exact_limit)||integer>static_cast(Detail::exact_limit)||std::abs(integer-x)>tolerance)Detail::bad("FlatZinc witness is not a representable integer"); + rounded[i]=integer; + } + if(!validate(model,rounded,0,0).valid)Detail::bad("rounded witness violates the complete compiled model"); + Detail::SourceEvaluator evaluator;check.source_values.reserve(compiled.variables().size()); + for(const auto& map:compiled.variables()) { + if(map.variable.model_id!=model.model_id||map.variable.id>=rounded.size())Detail::bad("invalid compiled source mapping"); + auto value=static_cast(rounded[map.variable.id]);evaluator.values.emplace(Detail::key(map.source),value);check.source_values.push_back(value); + } + evaluator.declarations(compiled.source()); + const auto objective=compiled.source().solve.method==F::Method::Satisfy?Detail::I(0):evaluator.value(compiled.source().solve.objective); + const auto expected=Detail::number(objective); + if(!result.objective||std::abs(*result.objective-expected)>tolerance)Detail::bad("solver objective disagrees with the original FlatZinc objective"); + check.original_objective=expected;check.valid=true;check.message="all original domains and predicates hold on the rounded source assignment"; + }catch(const Detail::Failure& e){check.message=e.message;} + catch(const std::exception& e){check.message=e.what();} + if(!check.valid){check.original_objective.reset();check.source_values.clear();} + return check; +} +std::string format_flatzinc_solution(const CompiledFlatZinc& compiled,const SolveResult& result,double tolerance) { + const auto checked=validate_flatzinc(compiled,result,tolerance); + if(!checked.valid)throw ModelError("cannot format an invalid FlatZinc solution: "+checked.message); + Detail::SourceEvaluator evaluator; + for(std::size_t i=0;i +#include +#include + +namespace Gecode { namespace Optimize { + +enum class FlatZincCompileStatus { + Complete, Unsupported, InvalidInput, ResourceLimit, TimeLimit, Cancelled, Error +}; +struct FlatZincCompileOptions { + std::size_t max_variables=1000000, max_constraints=1000000; + std::size_t max_nonzeros=2000000, max_work=16000000, max_value_depth=64; + double time_limit_seconds=std::numeric_limits::infinity(); + std::shared_ptr cancellation; +}; +struct FlatZincVariableMapping { + FlatZinc::Capture::Reference source; + Variable variable; +}; +namespace Detail { struct FlatZincArtifact; struct FlatZincCompiler; } +/** Immutable original source and compiled model; no parser or Space lifetime. */ +class CompiledFlatZinc { + std::shared_ptr data_; + explicit CompiledFlatZinc(std::shared_ptr data); + friend struct Detail::FlatZincCompiler; +public: + const ModelSnapshot& model() const; + const FlatZinc::Capture::Records& source() const; + const std::vector& variables() const; +}; +struct FlatZincCompileResult { + FlatZincCompileStatus status=FlatZincCompileStatus::InvalidInput; + std::optional compiled; + FlatZinc::Capture::Location location; + std::string message; + std::size_t work=0; +}; +/** + * Compiles authoritative raw_variables/raw_domains/raw_constraints. Normalized + * parser records are audit data, never a replacement for an original predicate. + * Only a fully admitted model is published. No backend is called here. + * Admits bounded integer/Boolean linear/Boolean relations, <= reification and + * implication, integer equality implication, AllDifferent, 1-based integer + * Element, positive-arity gecode_table_int and bounded finite integer domains + * with holes; explicit-offset nonempty Circuit, fixed four-argument Cumulative, + * and six-argument Regular with literal automaton parameters are also admitted. + * Equality reification, other global/signature forms, floats/sets and unknown + * controls remain Unsupported. Invalid literal schemas are InvalidInput. + * Private gates and fixed literal slots are not source outputs. + */ +FlatZincCompileResult compile_flatzinc(const FlatZinc::Capture::Records& source, + const FlatZincCompileOptions& options={}); +struct FlatZincValidation { + bool valid=false; + std::optional original_objective; + /** Values in variables() order, including each repeated alias. */ + std::vector source_values; + std::string message; +}; +/** Recheck identity, all model slots and every original source relation exactly + * after tolerance-qualified integer rounding. Does not certify the solve bound. + */ +FlatZincValidation validate_flatzinc(const CompiledFlatZinc&, const SolveResult&, + double integrality_tolerance=1e-6); +/** Buffered assignments and a solution separator; never an optimality or + * enumeration-completion marker. Rejects invalid full-model witnesses. + */ +std::string format_flatzinc_solution(const CompiledFlatZinc&, const SolveResult&, + double integrality_tolerance=1e-6); + +}} +#endif diff --git a/gecode/optimize/globals.cpp b/gecode/optimize/globals.cpp new file mode 100644 index 0000000000..308d447e92 --- /dev/null +++ b/gecode/optimize/globals.cpp @@ -0,0 +1,198 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { +GlobalConstraint add_all_different(Model& m,const std::vector& v,std::string name) { + return m.add_global(AllDifferentData{v},std::move(name)); +} +GlobalConstraint add_element(Model& m,Variable index,const std::vector& elements, + Variable result,std::int64_t base,std::string name) { + return m.add_global(ElementData{index,elements,result,base},std::move(name)); +} +GlobalConstraint add_table(Model& m,const std::vector& v, + const std::vector>& tuples,std::string name) { + return m.add_global(TableData{v,tuples},std::move(name)); +} +GlobalConstraint add_cumulative(Model& m,const std::vector& starts, + const std::vector& durations, + const std::vector& heights, + std::int64_t capacity,std::string name) { + return m.add_global(CumulativeData{starts,durations,heights,capacity},std::move(name)); +} +GlobalConstraint add_circuit(Model& m,const std::vector& successors, + std::int64_t base,std::string name) { + return m.add_global(CircuitData{successors,base},std::move(name)); +} +GlobalConstraint add_regular(Model& m,const std::vector& variables, + std::uint64_t state_count,std::uint64_t initial_state, + const std::vector& transitions, + const std::vector& final_states,std::string name) { + return m.add_global(RegularData{variables,state_count,initial_state,transitions,final_states},std::move(name)); +} + +namespace Detail { +namespace { +constexpr std::int64_t exact_limit=INT64_C(9007199254740992); +void constant(std::int64_t value) { + if(value < -exact_limit || value > exact_limit) + throw ModelError("Global integer constants must be exactly representable within +/-2^53"); +} +void index_range(std::int64_t base,std::size_t size) { + constant(base); + if(size>static_cast(exact_limit) || + (size && base>exact_limit-static_cast(size-1))) + throw ModelError("Global index range exceeds exact integer representation"); +} +} + +std::vector global_variables(const GlobalPayload& payload) { + if(payload.valueless_by_exception()) throw ModelError("Global constraint has no payload"); + return std::visit([](const auto& data) { + using T=std::decay_t; + if constexpr(std::is_same_v || std::is_same_v || std::is_same_v) return data.variables; + else if constexpr(std::is_same_v) { + auto out=data.elements;out.push_back(data.index);out.push_back(data.result);return out; + } else if constexpr(std::is_same_v) return data.starts; + else return data.successors; + },payload); +} + +void validate_global_payload(const GlobalPayload& payload,ModelId owner, + const std::vector& variables,bool require_active) { + for(const auto handle:global_variables(payload)) { + if(handle.model_id!=owner || handle.id>=variables.size() || + (require_active && !variables[handle.id].active)) + throw ModelError("Global constraint references a foreign, invalid, or deleted variable"); + const auto type=variables[handle.id].type; + if(type!=VariableType::Integer && type!=VariableType::Binary && type!=VariableType::SemiInteger) + throw ModelError("Global constraints require integer, binary, or semi-integer variables"); + } + std::visit([](const auto& data) { + using T=std::decay_t; + if constexpr(std::is_same_v) index_range(data.index_base,data.elements.size()); + else if constexpr(std::is_same_v) { + for(const auto& tuple:data.tuples) { + if(tuple.size()!=data.variables.size()) throw ModelError("Table tuple arity mismatch"); + for(const auto value:tuple) constant(value); + } + } else if constexpr(std::is_same_v) { + if(data.starts.size()!=data.durations.size() || data.starts.size()!=data.heights.size()) + throw ModelError("Cumulative starts, durations and heights must have equal lengths"); + constant(data.capacity); + if(data.capacity<0) throw ModelError("Cumulative capacity must be nonnegative"); + for(const auto duration:data.durations) { + constant(duration);if(duration<0) throw ModelError("Task durations must be nonnegative"); + } + for(const auto height:data.heights) { + constant(height);if(height<0) throw ModelError("Task heights must be nonnegative"); + } + } else if constexpr(std::is_same_v) { + if(data.successors.empty()) throw ModelError("A circuit needs at least one successor"); + index_range(data.index_base,data.successors.size()); + } else if constexpr(std::is_same_v) { + if(!data.state_count||data.initial_state>=data.state_count) + throw ModelError("Regular state count must be positive and initial state in range"); + std::set> keys; + for(const auto& transition:data.transitions){ + constant(transition.symbol); + if(transition.from>=data.state_count||transition.to>=data.state_count) + throw ModelError("Regular transition state is outside the declared state range"); + if(!keys.emplace(transition.from,transition.symbol).second) + throw ModelError("Regular transition keys (from,symbol) must be unique"); + } + for(auto final:data.final_states)if(final>=data.state_count) + throw ModelError("Regular final state is outside the declared state range"); + } + },payload); +} + +void validate_globals(const ModelSnapshot& model) { + for(std::size_t i=0;i& values, + double tolerance,std::string& reason) { + bool representable=true; + const auto value=[&](Variable variable) { + const double raw=values.at(static_cast(variable.id)); + if(!std::isfinite(raw) || std::abs(raw)>static_cast(exact_limit) || + std::abs(raw-std::round(raw))>tolerance) { + representable=false;return std::int64_t{0}; + } + return static_cast(std::round(raw)); + }; + // Check representability before branching/indexing on any rounded value. + for(const auto variable:global_variables(payload)) (void)value(variable); + if(!representable) {reason="global value is not an exact integer within +/-2^53";return false;} + const bool valid=std::visit([&](const auto& data)->bool { + using T=std::decay_t; + if constexpr(std::is_same_v) { + std::vector assigned; + for(const auto variable:data.variables) assigned.push_back(value(variable)); + std::sort(assigned.begin(),assigned.end()); + return std::adjacent_find(assigned.begin(),assigned.end())==assigned.end(); + } else if constexpr(std::is_same_v) { + const auto index=value(data.index); + if(index(index-data.index_base)>=data.elements.size()) return false; + return value(data.result)==value(data.elements[static_cast(index-data.index_base)]); + } else if constexpr(std::is_same_v) { + for(const auto& tuple:data.tuples) { + bool equal=true; + for(std::size_t i=0;i) { + std::vector> events; + for(std::size_t i=0;i0 && event.second>data.capacity-used) return false; + used+=event.second; + } + return true; + } else if constexpr(std::is_same_v) { + std::map,std::uint64_t> transitions; + for(const auto& edge:data.transitions)transitions.emplace(std::make_pair(edge.from,edge.symbol),edge.to); + auto state=data.initial_state; + for(auto variable:data.variables){ + const auto edge=transitions.find({state,value(variable)}); + if(edge==transitions.end())return false; + state=edge->second; + } + return std::find(data.final_states.begin(),data.final_states.end(),state)!=data.final_states.end(); + } else { + std::vector visited(data.successors.size(),false); + std::size_t node=0; + for(std::size_t step=0;step(successor-data.index_base)>=visited.size()) return false; + node=static_cast(successor-data.index_base); + } + return node==0; + } + },payload); + if(!valid) reason="original global constraint violated"; + return valid; +} +} +}} diff --git a/gecode/optimize/globals.hpp b/gecode/optimize/globals.hpp new file mode 100644 index 0000000000..892f3374f4 --- /dev/null +++ b/gecode/optimize/globals.hpp @@ -0,0 +1,47 @@ +#ifndef GECODE_OPTIMIZE_GLOBALS_HPP +#define GECODE_OPTIMIZE_GLOBALS_HPP +#include + +namespace Gecode { namespace Optimize { +/// Retain pairwise inequality of the supplied integer-variable slots. +GlobalConstraint add_all_different(Model&, const std::vector&, std::string name = {}); +/// Retain result == elements[index-index_base], including repeated handles. +GlobalConstraint add_element(Model&, Variable index, const std::vector& elements, + Variable result, std::int64_t index_base = 0, std::string name = {}); +/** Retain membership in a positive integer tuple table with matching arity. + * An empty tuple list is false. A zero-variable table containing an empty + * tuple is true; the separate FlatZinc compiler requires positive arity. + */ +GlobalConstraint add_table(Model&, const std::vector&, + const std::vector>&, std::string name = {}); +/** Mandatory fixed-duration tasks, half-open intervals; zero duration/height uses no resource. */ +GlobalConstraint add_cumulative(Model&, const std::vector& starts, + const std::vector& durations, + const std::vector& heights, + std::int64_t capacity, std::string name = {}); +/** One cycle through a nonempty successor array, with explicit index base. */ +GlobalConstraint add_circuit(Model&, const std::vector& successors, + std::int64_t index_base = 0, std::string name = {}); +/** Retain membership in a deterministic finite automaton's language. + * States are in [0,state_count); missing transitions reject. Transition keys + * (from,symbol) must be unique; repeated final states have set semantics. + * Empty words accept exactly when initial_state is final. No epsilon edges. + * Posting validates original metadata; native solving applies additional + * integer, array-size and automaton-storage limits before compilation. + */ +GlobalConstraint add_regular(Model&, const std::vector& variables, + std::uint64_t state_count, std::uint64_t initial_state, + const std::vector& transitions, + const std::vector& final_states, + std::string name = {}); + +namespace Detail { +std::vector global_variables(const GlobalPayload&); +void validate_global_payload(const GlobalPayload&, ModelId, const std::vector&, + bool require_active); +void validate_globals(const ModelSnapshot&); +bool global_satisfied(const GlobalPayload&, const std::vector&, double integrality_tolerance, + std::string& reason); +} +}} +#endif diff --git a/gecode/optimize/io.cpp b/gecode/optimize/io.cpp new file mode 100644 index 0000000000..889cda105d --- /dev/null +++ b/gecode/optimize/io.cpp @@ -0,0 +1,590 @@ +/* Lossless, deliberately bounded LP/free-MPS I/O. No backend parser is used. */ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#include +#else +#include +#include +#endif + +namespace Gecode { namespace Optimize { +namespace { +constexpr double inf = std::numeric_limits::infinity(); +using Terms = std::vector>; + +[[noreturn]] void fail(const std::string& message) { + throw ModelError("LP/MPS I/O: " + message); +} +std::string lower(std::string text) { + for (char& c : text) if (c >= 'A' && c <= 'Z') c += 'a' - 'A'; + return text; +} +std::string trim(const std::string& s) { + const auto first = s.find_first_not_of(" \t\r\n"); + return first == std::string::npos ? "" : s.substr(first, s.find_last_not_of(" \t\r\n")-first+1); +} +bool identifier(const std::string& s) { + const auto alpha = [](char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; }; + if (s.empty() || !alpha(s.front())) return false; + return std::all_of(s.begin()+1, s.end(), [&](char c) { return alpha(c) || (c >= '0' && c <= '9'); }); +} +std::string format(const std::string& filename) { + const auto extension = lower(std::filesystem::path(filename).extension().string()); + if (extension != ".lp" && extension != ".mps") fail("only uncompressed .lp and .mps are supported"); + return extension; +} +std::vector words(const std::string& line) { + std::istringstream in(line); in.imbue(std::locale::classic()); + std::vector result; + for (std::string word; in >> word;) result.push_back(word); + return result; +} +long double number(std::string text, bool infinity = false) { + const auto lc = lower(text); + if (infinity && (lc == "inf" || lc == "+inf" || lc == "infinity" || lc == "+infinity")) return inf; + if (infinity && (lc == "-inf" || lc == "-infinity")) return -inf; + if (text.empty()) fail("missing number"); + for (auto& c : text) if (c == 'd' || c == 'D') c = 'e'; + std::istringstream in(text); in.imbue(std::locale::classic()); + long double value = 0; + in >> value; + if (!in || !in.eof() || !std::isfinite(value)) fail("invalid finite number: " + text); + return value; +} +double narrow(long double value) { + if (!std::isfinite(value) || std::fabs(value) > std::numeric_limits::max()) + fail("numeric value or arithmetic result exceeds double range"); + const double result = static_cast(value); + if (value != 0 && result == 0) fail("nonzero value underflows double range"); + return result; +} +double scalar(const std::string& value, bool infinity = false) { + const auto n = number(value, infinity); + return std::isinf(n) ? static_cast(n) : narrow(n); +} +std::string hex(const std::string& s) { + static constexpr char digits[] = "0123456789abcdef"; + if (s.empty()) return "-"; + std::string result; + for (unsigned char c : s) { result += digits[c >> 4]; result += digits[c & 15]; } + return result; +} +std::string unhex(const std::string& s) { + if (s == "-") return ""; + if (s.empty() || s.size()%2) fail("malformed name metadata"); + const auto digit = [](char c) -> int { + if (c >= '0' && c <= '9') return c-'0'; + if (c >= 'a' && c <= 'f') return c-'a'+10; + fail("malformed name metadata"); + }; + std::string result; + for (std::size_t i=0; i(16*digit(s[i])+digit(s[i+1])); + return result; +} + +struct DraftVar { + std::string name; + VariableType type = VariableType::Continuous; + double lb = 0, ub = inf; + bool lower_set = false, upper_set = false, marker = false, any_bound = false; +}; +struct DraftRow { std::string name; Terms terms; double lb = -inf, ub = inf; }; +Terms canonical(const Terms& terms) { + std::map> sums; + for(const auto& term:terms) { + auto& pair=sums[term.first];const long double next=pair.first+term.second; + pair.second+=std::fabs(pair.first)>=std::fabs(term.second)?(pair.first-next)+term.second:(term.second-next)+pair.first; + pair.first=next;if(!std::isfinite(next)||!std::isfinite(pair.second))fail("coefficient sum overflow"); + } + Terms result;for(const auto& sum:sums){const double value=narrow(sum.second.first+sum.second.second);if(value!=0)result.push_back({sum.first,value});} + return result; +} +struct Draft { + std::vector vars; + std::map indices; + std::vector rows; + Terms objective; + double offset = 0; + ObjectiveSense sense = ObjectiveSense::Minimize; + std::map,std::string> names; + std::map ranges; + std::size_t variable(const std::string& name) { + if (!identifier(name)) fail("unsupported identifier: " + name); + const auto found = indices.find(name); + if (found != indices.end()) return found->second; + const auto id = vars.size(); vars.push_back({name}); indices.emplace(name,id); return id; + } + void metadata(const std::string& text) { + const auto w=words(text); + if (w.empty() || w[0].rfind("GECODE_",0)!=0) return; + if (w[0]=="GECODE_NAME" && w.size()==4 && (w[1]=="V" || w[1]=="R") && identifier(w[2])) { + if (!names.emplace(std::make_pair(w[1],w[2]),unhex(w[3])).second) fail("duplicate name metadata"); + } else if (w[0]=="GECODE_RANGE" && w.size()==3 && identifier(w[1]) && identifier(w[2])) { + if (!ranges.emplace(w[1],w[2]).second) fail("duplicate range metadata"); + } else fail("unsupported or malformed Gecode metadata"); + } + Model build() { + // LP ranged rows are serialized as two portable inequalities. Only a + // matching pair with exactly equal canonical expressions is recombined. + Model model; + std::vector handles; + for (auto& var : vars) { + const auto named=names.find({"V",var.name}); + handles.push_back(model.add_variable(var.type,var.lb,var.ub,named==names.end()?var.name:named->second)); + if (named!=names.end()) names.erase(named); + } + const auto terms = [&](const Terms& input) { + std::vector output; + for (const auto& t : input) output.push_back({handles.at(t.first),t.second}); + return output; + }; + std::map row_names; + for (std::size_t i=0;i omitted; + for (const auto& range : ranges) { + const auto li=row_names.find(range.first), ui=row_names.find(range.second); + if(li==row_names.end()||ui==row_names.end())fail("range metadata references absent row"); + auto lo=rows.begin()+static_cast(li->second),hi=rows.begin()+static_cast(ui->second); + if (lo==hi || !std::isfinite(lo->lb) || lo->ub!=inf || + hi->lb!=-inf || !std::isfinite(hi->ub) || omitted.count(lo->name) || + !omitted.insert(hi->name).second) fail("invalid ranged-row metadata"); + if(canonical(lo->terms)!=canonical(hi->terms))fail("range metadata expressions differ"); + lo->ub=hi->ub; + } + for (const auto& row : rows) { + if (omitted.count(row.name)) continue; + const auto named=names.find({"R",row.name}); + model.add_row(terms(row.terms),row.lb,row.ub,named==names.end()?row.name:named->second); + if (named!=names.end()) names.erase(named); + } + if (!names.empty()) fail("name metadata references absent entity"); + model.set_objective(terms(objective),sense,offset); + validate_structure(model.snapshot()); + return model; + } +}; + +std::vector lex(const std::string& text) { + std::vector result; + for (std::size_t i=0;i') { + if(i+1>=text.size()||text[i+1]!='=') fail("strict or incomplete LP comparison"); + result.push_back(text.substr(i,2));i+=2;continue; + } + const auto begin=i; + if((c>='0'&&c<='9')||c=='.') { + while(i='0'&&text[i]<='9')||text[i]=='.')) ++i; + if(i='0'&&text[i]<='9')++i; + if(i==exponent)fail("invalid LP exponent"); + } + if(i='a'&&text[i]<='z')||(text[i]>='A'&&text[i]<='Z')||text[i]=='_')) + fail("LP numeric coefficients need whitespace before variable names"); + auto value=text.substr(begin,i-begin); (void)number(value); result.push_back(value); continue; + } + if((c>='A'&&c<='Z')||(c>='a'&&c<='z')||c=='_') { + ++i;while(i='A'&&text[i]<='Z')||(text[i]>='a'&&text[i]<='z')|| + (text[i]>='0'&&text[i]<='9')||text[i]=='_'))++i; + result.push_back(text.substr(begin,i-begin));continue; + } + fail("unsupported LP syntax (nonlinear/SOS/quoted names are not supported)"); + } + return result; +} +double signed_number(const std::vector& tokens,std::size_t& p,bool infinity=false) { + std::string sign; + if(p=std::fabs(value)?(sum-next)+value:(value-next)+sum; + sum=next; + if(!std::isfinite(sum)||!std::isfinite(correction))fail("LP expression offset overflow"); + } + double value() const {return narrow(sum+correction);} +}; +std::pair expression(Draft& draft,const std::vector& tokens,std::size_t first,std::size_t last) { + Terms terms; Offset offset; + bool initial=true; + while(first1&&t[1]==":"?2:0; + auto e=expression(draft,t,start,t.size());draft.objective=std::move(e.first);draft.offset=e.second.value();objective_parsed=true;}; + const auto parse_row=[&](const std::string& text) { + const auto t=lex(text);std::size_t first=0; + std::string name="row_"+std::to_string(draft.rows.size()); + if(t.size()>1&&t[1]==":"){name=t[0];first=2;} + auto relation=std::find_if(t.begin()+static_cast(first),t.end(),[](const auto& s){return s=="<="||s==">="||s=="=";}); + if(relation==t.end())fail("LP row missing comparison"); + const auto at=static_cast(relation-t.begin()); + auto e=expression(draft,t,first,at);std::size_t pos=at+1;const double rhs=signed_number(t,pos,true); + if(pos!=t.size())fail("LP rows require one comparison and numeric RHS"); + // Keep the compensation through RHS subtraction: narrowing the LHS + // constant first loses the residual in x + 1 - 1e16 >= -1e16. + if(std::isfinite(rhs))e.second.add(-static_cast(rhs)); + const double side=std::isinf(rhs)?rhs:-e.second.value(); + DraftRow row{name,std::move(e.first),-inf,inf}; + if(*relation=="<="||*relation=="=")row.ub=side; + if(*relation==">="||*relation=="=")row.lb=side; + draft.rows.push_back(std::move(row)); + }; + for(std::string line;std::getline(input,line);) { + line=trim(line);if(line.empty())continue; + if(line.front()=='\\'){draft.metadata(trim(line.substr(1)));continue;} + if(end_seen)fail("content follows LP End"); + if(line.find('\\')!=std::string::npos)line=trim(line.substr(0,line.find('\\'))); + const auto keyword=lower(line);Section next=Section::None; + if(keyword=="minimize"||keyword=="minimum"||keyword=="min"||keyword=="maximize"||keyword=="maximum"||keyword=="max") { + if(objective_seen)fail("multiple LP objectives"); objective_seen=true; + draft.sense=keyword.rfind("max",0)==0?ObjectiveSense::Maximize:ObjectiveSense::Minimize;next=Section::Objective; + } else if(keyword=="subject to"||keyword=="such that"||keyword=="st"||keyword=="s.t."||keyword=="s.t") { + if(rows_seen)fail("duplicate LP constraint section");rows_seen=true;next=Section::Rows; + } else if(keyword=="bounds"||keyword=="bound")next=Section::Bounds; + else if(keyword=="binary"||keyword=="binaries"||keyword=="bin")next=Section::Binary; + else if(keyword=="general"||keyword=="generals"||keyword=="gen"||keyword=="integer"||keyword=="integers")next=Section::General; + else if(keyword=="semi"||keyword=="semis"||keyword=="semi-continuous")next=Section::Semi; + else if(keyword=="end"){next=Section::End;end_seen=true;} + else if(keyword=="sos"||keyword=="pwl"||keyword=="indicators")fail("unsupported LP section: "+line); + if(next!=Section::None){if(!pending.empty())fail("incomplete LP row"); + if(!objective_seen)fail("LP objective must be first"); + if(section==Section::Objective&&!objective_parsed)parse_objective();section=next;continue;} + if(section==Section::Objective){objective+=" "+line;continue;} + if(section==Section::Rows) { + pending+=" "+line; + const auto t=lex(pending);auto rel=std::find_if(t.begin(),t.end(),[](const auto& s){return s=="<="||s==">="||s=="=";}); + if(rel!=t.end()&&rel+1!=t.end()&&t.back()!="+"&&t.back()!="-"){parse_row(pending);pending.clear();} + continue; + } + const auto t=lex(line); + if(section==Section::Bounds) { + if(t.size()==2&&identifier(t[0])&&lower(t[1])=="free") { + auto& v=draft.vars[draft.variable(t[0])];if(v.lower_set||v.upper_set)fail("duplicate LP bound side"); + v.lb=-inf;v.ub=inf;v.lower_set=v.upper_set=true;continue; + } + std::size_t p=0;std::string name;double lb=-inf,ub=inf;bool set_lb=false,set_ub=false; + if(!t.empty()&&identifier(t[0])&&lower(t[0])!="inf"&&lower(t[0])!="infinity") { + name=t[p++];if(p>=t.size())fail("incomplete LP bound");const auto op=t[p++]; + const double n=signed_number(t,p,true); + if(op=="<="||op=="="){ub=n;set_ub=true;}if(op==">="||op=="="){lb=n;set_lb=true;} + if(!set_lb&&!set_ub)fail("invalid LP bound comparison"); + } else { + lb=signed_number(t,p,true);if(p>=t.size()||t[p++]!="<=")fail("unsupported LP bound direction"); + if(p>=t.size())fail("missing LP bound variable");name=t[p++];set_lb=true; + if(p rows;std::vector senses;std::map ranges; + std::set rhs_seen;std::set> bound_seen; + std::string objective,rhs_name,range_name,bound_name;bool integer=false,ended=false,columns=false; + std::set
sections; + for(std::string line;std::getline(input,line);) { + line=trim(line);if(line.empty())continue;if(line.front()=='*'){d.metadata(trim(line.substr(1)));continue;} + if(ended)fail("content follows MPS ENDATA");const auto w=words(line);const auto key=lower(w[0]);Section next=Section::None; + if(key=="name"&&w.size()<=2)next=Section::Name; + else if(key=="objsense"&&w.size()<=2)next=Section::Sense; + else if(w.size()==1) { + if(key=="rows")next=Section::Rows;else if(key=="columns")next=Section::Columns; + else if(key=="rhs")next=Section::Rhs;else if(key=="ranges")next=Section::Ranges; + else if(key=="bounds")next=Section::Bounds;else if(key=="endata")next=Section::End; + else if(sec!=Section::Sense)fail("unsupported MPS section: "+w[0]); + } + if(next!=Section::None) { + if(!sections.insert(next).second)fail("duplicate MPS section"); + if(next==Section::Columns){if(objective.empty())fail("MPS requires objective N row before COLUMNS");columns=true;} + if(next==Section::Rows&&columns)fail("MPS ROWS out of order"); + if((next==Section::Rhs||next==Section::Ranges||next==Section::Bounds)&&!columns)fail("MPS sections out of order"); + if(sec==Section::Columns&&integer)fail("unclosed MPS integer marker");sec=next; + if(next==Section::End){ended=true;continue;} + if(next!=Section::Sense||w.size()==1)continue; + } + if(sec==Section::Sense) { + const auto value=lower(w.back());if(value!="min"&&value!="max")fail("invalid MPS objective sense"); + d.sense=value=="min"?ObjectiveSense::Minimize:ObjectiveSense::Maximize;sec=Section::Name;continue; + } + if(sec==Section::Rows) { + if(w.size()!=2||w[0].size()!=1||!identifier(w[1]))fail("invalid MPS ROWS record"); + const char kind=static_cast(std::toupper(static_cast(w[0][0]))); + if(kind!='N'&&kind!='L'&&kind!='G'&&kind!='E')fail("unsupported MPS row type"); + if(rows.count(w[1])||w[1]==objective)fail("duplicate MPS row name"); + if(kind=='N'&&objective.empty()){objective=w[1];continue;} + rows.emplace(w[1],d.rows.size());d.rows.push_back({w[1],{},kind=='G'||kind=='E'?0:-inf,kind=='L'||kind=='E'?0:inf});senses.push_back(kind);continue; + } + if(sec==Section::Columns) { + if(w.size()==3&&w[1]=="'MARKER'") { + if(w[2]=="'INTORG'"&&!integer)integer=true;else if(w[2]=="'INTEND'"&&integer)integer=false;else fail("invalid integer marker");continue; + } + if(w.size()!=3&&w.size()!=5)fail("invalid MPS COLUMNS record"); + const bool existed=d.indices.count(w[0])!=0;const auto id=d.variable(w[0]);auto& v=d.vars[id]; + if(existed&&v.marker!=integer)fail("inconsistent column integrality markers"); + if(integer){v.marker=true;v.type=VariableType::Integer;} + for(std::size_t p=1;psecond].terms.push_back({id,value});} + }continue; + } + if(sec==Section::Rhs||sec==Section::Ranges) { + if(w.size()!=3&&w.size()!=5)fail("invalid RHS/RANGES record"); + auto& vector_name=sec==Section::Rhs?rhs_name:range_name; + if(vector_name.empty())vector_name=w[0];if(vector_name!=w[0])fail("multiple RHS/RANGES vectors are unsupported"); + for(std::size_t p=1;p::max()).second)fail("duplicate/invalid objective RHS");d.offset=-scalar(w[p+1]);continue;} + const auto found=rows.find(w[p]);if(found==rows.end())fail("RHS/RANGES references unknown row");const auto id=found->second; + if(sec==Section::Ranges){if(senses[id]=='N'||!ranges.emplace(id,number(w[p+1])).second)fail("duplicate/invalid row range");} + else {if(!rhs_seen.insert(id).second)fail("duplicate RHS row");const double value=scalar(w[p+1]); + if(senses[id]=='N')fail("RHS for nonobjective free row unsupported"); + if(senses[id]=='L'||senses[id]=='E')d.rows[id].ub=value;if(senses[id]=='G'||senses[id]=='E')d.rows[id].lb=value;} + }continue; + } + if(sec==Section::Bounds) { + if(w.size()!=3&&w.size()!=4)fail("invalid MPS BOUNDS record"); + if(bound_name.empty())bound_name=w[1];if(bound_name!=w[1])fail("multiple bound vectors unsupported"); + const auto found=d.indices.find(w[2]);if(found==d.indices.end())fail("BOUNDS references unknown column"); + const auto id=found->second;auto& v=d.vars[id];const auto code=lower(w[0]); + if(!bound_seen.insert({id,code}).second)fail("duplicate MPS bound record");v.any_bound=true; + const bool valued=code=="lo"||code=="up"||code=="fx"||code=="li"||code=="ui"||code=="sc"||code=="si"; + if((valued&&w.size()!=4)||(!valued&&w.size()!=3))fail("wrong MPS bound arity"); + const double value=valued?scalar(w[3]):0; + const bool writes_lower=code=="lo"||code=="li"||code=="fx"||code=="fr"||code=="mi"; + const bool writes_upper=code=="up"||code=="ui"||code=="fx"||code=="fr"||code=="pl"||code=="sc"||code=="si"; + if((writes_lower&&v.lower_set)||(writes_upper&&v.upper_set)|| + (code=="bv"&&(v.lower_set||v.upper_set)))fail("overlapping MPS bound definitions"); + if((code=="li"||code=="ui")&&(v.type==VariableType::Binary||v.type==VariableType::SemiContinuous||v.type==VariableType::SemiInteger)) + fail("conflicting MPS variable type definitions"); + if((code=="sc"&&v.type!=VariableType::Continuous)|| + (code=="si"&&v.type!=VariableType::Continuous&&v.type!=VariableType::Integer)|| + (code=="bv"&&(v.type==VariableType::SemiContinuous||v.type==VariableType::SemiInteger))) + fail("conflicting MPS variable type definitions (use SI for semi-integer columns)"); + if(code=="lo"||code=="li"){v.lb=value;v.lower_set=true;if(code=="li")v.type=VariableType::Integer;} + else if(code=="up"||code=="ui"){v.ub=value;v.upper_set=true;if(code=="ui")v.type=VariableType::Integer;} + else if(code=="fx"){v.lb=v.ub=value;v.lower_set=v.upper_set=true;} + else if(code=="fr"){v.lb=-inf;v.ub=inf;v.lower_set=v.upper_set=true;} + else if(code=="mi"){v.lb=-inf;v.lower_set=true;} + else if(code=="pl"){v.ub=inf;v.upper_set=true;} + else if(code=="bv"){v.type=VariableType::Binary;v.lb=0;v.ub=1;v.lower_set=v.upper_set=true;} + else if(code=="sc"||code=="si"){v.type=code=="si"?VariableType::SemiInteger:VariableType::SemiContinuous;v.ub=value;v.upper_set=true;if(!v.lower_set)v.lb=1;} + else fail("unsupported MPS bound type: "+w[0]);continue; + } + fail("unexpected MPS record or unsupported section"); + } + if(!ended||!columns||objective.empty())fail("MPS requires ROWS, COLUMNS and ENDATA"); + for(auto& v:d.vars)if(v.marker&&!v.any_bound)v.ub=1; + for(const auto& range:ranges) { + const auto id=range.first;const auto value=range.second;auto& row=d.rows[id]; + if(senses[id]=='L'||(senses[id]=='E'&&value<0))row.lb=narrow(static_cast(row.ub)-std::fabs(value)); + else row.ub=narrow(static_cast(row.lb)+std::fabs(value)); + } + return d.build(); +} + +std::string real(double value) { + if(std::isinf(value))return value<0?"-inf":"+inf"; + std::ostringstream out;out.imbue(std::locale::classic());out<::max_digits10)< map(original.variables.size()); + for(const auto& v:original.variables)if(v.active)map[v.variable.id]=m.add_variable(v.type,v.lower,v.upper,v.name); + const auto terms=[&](const std::vector& src){std::vector out;for(const auto& t:src)out.push_back({map[t.variable.id],t.coefficient});return out;}; + for(const auto& r:original.rows)if(r.active)m.add_row(terms(r.terms),r.lower,r.upper,r.name); + m.set_objective(terms(original.objective.terms),original.objective.sense,original.objective.offset);return m.snapshot(); +} +void equivalent(const ModelSnapshot& a,const ModelSnapshot& b) { + if(a.variables.size()!=b.variables.size()||a.rows.size()!=b.rows.size()||a.objective.offset!=b.objective.offset||a.objective.sense!=b.objective.sense) + fail("export round trip changed dimensions/objective"); + const auto terms=[](const auto& x,const auto& y){if(x.size()!=y.size())fail("export changed expression size");for(std::size_t i=0;i costs(m.variables.size(),0);for(const auto& t:m.objective.terms)costs[t.variable.id]=t.coefficient; + const auto expression_out=[&](const std::vector& terms){if(terms.empty())out<<" 0";for(const auto& t:terms)out<<(t.coefficient<0?" - ":" + ")<= "<>> by_column(m.variables.size()); + for(const auto& r:m.rows)for(const auto& t:r.terms)by_column[t.variable.id].push_back({r.constraint.id,t.coefficient}); + out<<"COLUMNS\n";bool integer=false;std::size_t marker=0; + for(const auto& v:m.variables) { + const bool integral=v.type==VariableType::Integer||v.type==VariableType::Binary||v.type==VariableType::SemiInteger; + if(integral!=integer){out<<" mark"<(r.upper)-r.lower))<<'\n';} + out<<"BOUNDS\n"; + for(const auto& v:m.variables) { + if(v.type==VariableType::Binary) { + if(v.lower!=0||v.upper!=1)fail("MPS binary export requires bounds [0,1]; use LP for tighter binary bounds"); + out<<" BV bnd x"< serial{0};std::random_device random; + for(int attempt=0;attempt<64;++attempt) { + path=destination.parent_path()/("."+destination.filename().string()+".tmp-"+std::to_string(random())+"-"+std::to_string(serial++)); +#ifdef _WIN32 + const int fd=_wopen(path.c_str(),_O_CREAT|_O_EXCL|_O_WRONLY|_O_BINARY,_S_IREAD|_S_IWRITE); + if(fd>=0){file=_fdopen(fd,"wb");if(!file)_close(fd);} +#else + const int fd=::open(path.c_str(),O_CREAT|O_EXCL|O_WRONLY,0600); + if(fd>=0){file=fdopen(fd,"wb");if(!file)::close(fd);} +#endif + if(file)return; + if(fd>=0){std::error_code ec;std::filesystem::remove(path,ec);fail("cannot open temporary output stream");} + if(errno!=EEXIST)fail("cannot create same-directory temporary output"); + }fail("cannot allocate unique temporary output"); + } + ~Temporary(){if(file)std::fclose(file);if(!path.empty()){std::error_code ec;std::filesystem::remove(path,ec);}} + void write(const std::string& bytes) { + if(std::fwrite(bytes.data(),1,bytes.size(),file)!=bytes.size()||std::fflush(file)!=0)fail("output write failed"); + FILE* closing=file;file=nullptr;if(std::fclose(closing)!=0)fail("output close failed"); + } + void replace(const std::filesystem::path& destination) { +#ifdef _WIN32 + if(!MoveFileExW(path.c_str(),destination.c_str(),MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH))fail("atomic destination replacement failed"); +#else + if(std::rename(path.c_str(),destination.c_str())!=0)fail("atomic destination replacement failed"); +#endif + path.clear(); + } +}; +Model parse_file(const std::filesystem::path& path,bool mps) { + std::ifstream input(path);if(!input)fail("cannot open input: "+path.string());input.imbue(std::locale::classic()); + Model model=mps?read_mps(input):read_lp(input);if(input.bad())fail("input read failed");return model; +} +} + +Model read_model(const std::string& filename) { + return parse_file(std::filesystem::path(filename),format(filename)==".mps"); +} +void write_model(const ModelSnapshot& model,const std::string& filename) { + const bool mps=format(filename)==".mps"; + if(std::any_of(model.globals.begin(),model.globals.end(),[](const auto& item){return item.active;})) + fail("native globals cannot be preserved by linear LP/MPS export"); + if(std::any_of(model.indicators.begin(),model.indicators.end(),[](const auto& item){return item.active;})) + fail("active original indicators require retained logical metadata and domain guards; linear LP/MPS export is unsupported for this model"); + const auto original=compact(model);const auto bytes=serialize(original,mps); + const auto destination=std::filesystem::absolute(std::filesystem::path(filename)); + Temporary output(destination);output.write(bytes); + auto restored=parse_file(output.path,mps);equivalent(original,restored.snapshot());output.replace(destination); +} +}} diff --git a/gecode/optimize/lp_basis.cpp b/gecode/optimize/lp_basis.cpp new file mode 100644 index 0000000000..5a46c5bee7 --- /dev/null +++ b/gecode/optimize/lp_basis.cpp @@ -0,0 +1,112 @@ +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { +namespace { +void tick(const SolveBudget* budget) { + // Admission does not mutate any backend. The solve wrapper translates this + // finite-work interruption into the shared budget's exact stop reason. + if (budget && budget->expired()) throw ModelError("Basis admission interrupted by solve budget"); +} +void status(LpBasisStatus value,double lower,double upper) { + switch(value) { + case LpBasisStatus::Basic: case LpBasisStatus::NonbasicUnspecified: return; + case LpBasisStatus::Lower: + if(std::isfinite(lower))return; + throw ModelError("Lower basis status requires a finite lower bound"); + case LpBasisStatus::Upper: + if(std::isfinite(upper))return; + throw ModelError("Upper basis status requires a finite upper bound"); + case LpBasisStatus::Zero: + if(!std::isfinite(lower)&&!std::isfinite(upper))return; + throw ModelError("Zero basis status requires a free row or column"); + } + throw ModelError("Unknown original LP basis status"); +} +bool same(Variable a,Variable b){return a.model_id==b.model_id&&a.id==b.id;} +bool same(Constraint a,Constraint b){return a.model_id==b.model_id&&a.id==b.id;} +bool terms(const std::vector& a,const std::vector& b,const SolveBudget& budget){ + if(a.size()!=b.size())return false; + for(std::size_t i=0;i make_lp_basis(const LpBasisData& data){ + return Detail::LpBasisAccess::create(data,LpBasisOrigin::Caller); +} +std::shared_ptr make_lp_basis(const LpObservations& observed){ + if(observed.basis().state!=LpObservationState::Available) + throw ModelError("Cannot construct a basis from an unavailable observation group"); + LpBasisData data;data.source=observed.source(); + data.rows.reserve(observed.rows().size());data.columns.reserve(observed.columns().size()); + for(const auto& row:observed.rows())data.rows.push_back(row.basis); + for(const auto& col:observed.columns())data.columns.push_back(col.basis); + return Detail::LpBasisAccess::create(std::move(data),LpBasisOrigin::Observations); +} +namespace Detail { +void LpBasisAccess::validate_statuses(const ModelSnapshot& model, + const std::vector>& rows, + const std::vector>& columns,const SolveBudget* budget){ + if(rows.size()!=model.rows.size()||columns.size()!=model.variables.size()) + throw ModelError("Basis dimensions must exactly match original row/column slots"); + std::size_t basics=0,active_rows=0; + for(std::size_t i=0;i LpBasisAccess::create(LpBasisData data,LpBasisOrigin origin){ + validate_structure(data.source);validate_statuses(data.source,data.rows,data.columns); + auto basis=std::shared_ptr(new LpBasis);basis->data_=std::move(data);basis->origin_=origin; + return basis; +} +void LpBasisAccess::compatible(const LpBasis& basis,const ModelSnapshot& next,const SolveBudget& budget){ + const auto& old=basis.source(); + auto mismatch=[](){throw ModelError("Basis requires identical original owner, revision and active source content");}; + if(old.model_id!=next.model_id||old.revision!=next.revision||old.variables.size()!=next.variables.size()||old.rows.size()!=next.rows.size())mismatch(); + validate_statuses(next,basis.rows(),basis.columns(),&budget); + for(std::size_t i=0;i + +namespace Gecode { namespace Optimize { +namespace Detail { struct LpBasisAccess; } + +/** Untrusted caller data. Factory copies, validates, and freezes all records. */ +struct LpBasisData { + ModelSnapshot source; + /** Exactly one entry per original slot; inactive slots must be absent. */ + std::vector> rows, columns; +}; +enum class LpBasisOrigin { Caller, Observations }; +class LpBasis { +public: + LpBasis(const LpBasis&) = delete; + LpBasis& operator=(const LpBasis&) = delete; + ModelId id() const noexcept { return data_.source.model_id; } + Revision revision() const noexcept { return data_.source.revision; } + const ModelSnapshot& source() const noexcept { return data_.source; } + const std::vector>& rows() const noexcept { return data_.rows; } + const std::vector>& columns() const noexcept { return data_.columns; } + LpBasisOrigin origin() const noexcept { return origin_; } +private: + LpBasis() = default; + friend struct Detail::LpBasisAccess; + LpBasisData data_; + LpBasisOrigin origin_ = LpBasisOrigin::Caller; +}; +/** Throw ModelError for malformed/unsupported source or status data. */ +std::shared_ptr make_lp_basis(const LpBasisData&); +/** Requires an Available complete original basis; copies historical source. */ +std::shared_ptr make_lp_basis(const LpObservations&); + +enum class LpBasisSubmissionState { + NotAttempted, Accepted, Repaired, Rejected, Interrupted +}; +struct LpBasisSubmission { + LpBasisSubmissionState state = LpBasisSubmissionState::NotAttempted; + bool backend_attempted = false; + /** Absent until timely accepted backend statuses were completely checked. */ + std::optional statuses_changed; + std::string message; +}; +struct LpBasisSolveOptions { + LpObservationOptions observations; + /** Required. Same owner/revision, original IDs/masks and active source content; + * inactive payloads are nonsemantic. Never a relaxation. */ + std::shared_ptr basis; + /** Simultaneous primal_start is rejected; no implicit precedence rule. */ + void validate() const; +}; +struct LpBasisSolveResult { + LpObservedResult observed; + /** Retains submitted source/status lifetime independently of caller/session. */ + std::shared_ptr requested_basis; + LpBasisSubmission submission; +}; +/** + * Numerical HiGHS ordinary continuous LP only; active constant rows unsupported. + * Accepted/Repaired describe setBasis before optimization, not final status. + * Repaired means statuses changed during submission. No faster-solve, checkpoint, + * independent nonsingularity, or certificate claim. A rejected backend submission + * returns without cold fallback, invalidating the session for a clean next load. + * Factorization/repair is cooperatively budgeted and may be uninterruptible. + */ +LpBasisSolveResult solve_lp_with_basis(const ModelSnapshot&, const LpBasisSolveOptions&); +LpBasisSolveResult solve_lp_with_basis(const Model&, const LpBasisSolveOptions&); +}} +#endif diff --git a/gecode/optimize/lp_basis_detail.hpp b/gecode/optimize/lp_basis_detail.hpp new file mode 100644 index 0000000000..96fb654aaf --- /dev/null +++ b/gecode/optimize/lp_basis_detail.hpp @@ -0,0 +1,15 @@ +/* Private validation seam; not installed. */ +#ifndef GECODE_OPTIMIZE_LP_BASIS_DETAIL_HPP +#define GECODE_OPTIMIZE_LP_BASIS_DETAIL_HPP +#include +namespace Gecode { namespace Optimize { namespace Detail { +struct LpBasisAccess { + static std::shared_ptr create(LpBasisData, LpBasisOrigin); + static void validate_statuses(const ModelSnapshot&, + const std::vector>& rows, + const std::vector>& columns, + const SolveBudget* budget = nullptr); + static void compatible(const LpBasis&, const ModelSnapshot&, const SolveBudget&); +}; +}}} +#endif diff --git a/gecode/optimize/lp_evidence.cpp b/gecode/optimize/lp_evidence.cpp new file mode 100644 index 0000000000..b8ca226390 --- /dev/null +++ b/gecode/optimize/lp_evidence.cpp @@ -0,0 +1,366 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { +#ifdef GECODE_OPTIMIZE_TEST_LP_EVIDENCE +namespace Detail { +SolveResult lp_evidence_test_solve(LpEvidencePhase,const ModelSnapshot&,const SolveOptions&); +void lp_evidence_test_checkpoint(const char*,std::size_t); +} +#endif +namespace { +using Wide=long double; +using Clock=std::chrono::steady_clock; +constexpr double inf=std::numeric_limits::infinity(); +struct Failure : std::runtime_error { + Termination termination;LpEvidenceReason reason; + Failure(Termination t,LpEvidenceReason r,const char* text):std::runtime_error(text),termination(t),reason(r){} +}; +[[noreturn]] void bad(const char* text){throw Failure(Termination::NumericalFailure,LpEvidenceReason::InvalidBackendData,text);} +[[noreturn]] void unsupported(const char* text){throw Failure(Termination::Unsupported,LpEvidenceReason::Unsupported,text);} +struct Budget { + SolveBudget shared;Clock::time_point started;double seconds; + Budget(const SolveOptions& options,Clock::time_point start):shared(options),started(start),seconds(options.time_limit_seconds){} + double remaining() const {return std::max(0.0,seconds-std::chrono::duration(Clock::now()-started).count());} + std::optional stop() const { + if(shared.cancelled())return Termination::Cancelled; + if(remaining()==0)return Termination::TimeLimit; + if(shared.node_limit_reached())return Termination::NodeLimit; + return {}; + } + void check() const {if(const auto reason=stop())throw Failure(*reason,LpEvidenceReason::Stopped,"Whole LP evidence allowance stopped");} +}; +struct Meter { + Budget& budget;const LpEvidenceLimits& limits;std::size_t work=0,retained=0; + void tick(std::size_t n=1){budget.check();if(n>limits.max_work-work) + throw Failure(Termination::IterationLimit,LpEvidenceReason::ResourceLimit,"LP evidence coordinator work limit reached");work+=n;} + void keep(std::size_t n){tick(0);if(n>limits.max_retained_slots-retained) + throw Failure(Termination::MemoryLimit,LpEvidenceReason::ResourceLimit,"LP evidence retained-slot limit reached");retained+=n;} +}; +void event(const char* point,std::size_t stage) { +#ifdef GECODE_OPTIMIZE_TEST_LP_EVIDENCE + Detail::lp_evidence_test_checkpoint(point,stage); +#else + (void)point;(void)stage; +#endif +} +struct Sum { + Wide sum=0,correction=0; + void add(Wide value){if(!std::isfinite(value))bad("Nonfinite original evidence arithmetic"); + const Wide next=sum+value;if(!std::isfinite(next))bad("Original evidence sum overflow"); + correction+=std::abs(sum)>=std::abs(value)?(sum-next)+value:(value-next)+sum; + if(!std::isfinite(correction))bad("Original evidence compensation overflow");sum=next;} + Wide value() const {const Wide v=sum+correction;if(!std::isfinite(v))bad("Original evidence sum overflow");return v;} +}; +double narrow(Wide value){if(!std::isfinite(value)||std::abs(value)>std::numeric_limits::max())bad("Evidence exceeds finite double representation"); + const double out=static_cast(value);if(value!=0&&out==0)bad("Evidence conversion would erase a nonzero value");return out;} +void range(double value,double cap,const char* text){if(std::isfinite(value)&&std::abs(value)>=cap)unsupported(text);} +std::size_t add(std::size_t a,std::size_t b){if(b>std::numeric_limits::max()-a) + throw Failure(Termination::MemoryLimit,LpEvidenceReason::ResourceLimit,"LP evidence size arithmetic overflow");return a+b;} +void cap(std::size_t n,std::size_t limit,const char* text){if(n>limit)throw Failure(Termination::MemoryLimit,LpEvidenceReason::ResourceLimit,text);} +void visit(const ModelSnapshot& model,Meter& meter,bool retain=false){ + const auto charge=[&](std::size_t n){meter.tick(n);if(retain)meter.keep(n);}; + charge(model.variables.size());charge(model.rows.size());charge(model.objective.terms.size()); + for(const auto& v:model.variables)charge(v.name.size()); + for(const auto& r:model.rows){charge(r.terms.size());charge(r.name.size());} + // Metadata is validated and owned, but active logical semantics are unsupported. + for(const auto& i:model.indicators){charge(1);charge(i.terms.size());charge(i.generated_rows.size());charge(i.domains.size());} + for(const auto& g:model.globals){charge(1);charge(g.name.size()); + if(g.payload.valueless_by_exception())throw ModelError("Global constraint has no payload"); + std::visit([&](const auto& data){using T=std::decay_t; + if constexpr(std::is_same_v||std::is_same_v||std::is_same_v)charge(data.variables.size()); + if constexpr(std::is_same_v)charge(add(data.elements.size(),2)); + if constexpr(std::is_same_v){charge(data.starts.size());charge(data.durations.size());charge(data.heights.size());} + if constexpr(std::is_same_v)charge(data.successors.size()); + if constexpr(std::is_same_v)for(const auto& tuple:data.tuples)charge(tuple.size()); + if constexpr(std::is_same_v){charge(data.transitions.size());charge(data.final_states.size());} + },g.payload);} +} +void admit(const ModelSnapshot& source,const LpEvidenceOptions& options,Meter& meter){ + visit(source,meter);validate_structure(source);meter.tick(0); + if(options.solve.backend==Backend::Native||options.solve.guarantee!=Guarantee::Numerical) + unsupported("LP evidence requires Auto/HiGHS and Numerical guarantee"); + if(!options.solve.primal_start.empty())unsupported("LP evidence does not map caller primal starts to auxiliary models"); + for(const auto& i:source.indicators){meter.tick();if(i.active)unsupported("LP evidence does not relax active indicators");} + for(const auto& g:source.globals){meter.tick();if(g.active)unsupported("LP evidence does not relax active globals");} + for(const auto& v:source.variables){meter.tick();if(!v.active)continue; + if(v.type!=VariableType::Continuous)unsupported("LP evidence requires original Continuous variables"); + range(v.lower,1e20,"Original variable bound exceeds auxiliary backend range");range(v.upper,1e20,"Original variable bound exceeds auxiliary backend range");} + for(const auto& r:source.rows){meter.tick();if(!r.active)continue; + range(r.lower,1e20,"Original row side exceeds auxiliary backend range");range(r.upper,1e20,"Original row side exceeds auxiliary backend range"); + for(const auto& t:r.terms){meter.tick();range(t.coefficient,1e15,"Original matrix coefficient exceeds auxiliary backend range"); + if(std::abs(t.coefficient)<=1e-12)unsupported("Nonzero original matrix coefficients <=1e-12 require rescaling");}} + range(source.objective.offset,1e20,"Original objective offset exceeds supported source range"); + for(const auto& t:source.objective.terms){meter.tick();range(t.coefficient,1e20,"Original objective coefficient exceeds auxiliary backend range");} +} +struct Sizes {std::size_t variables=0,rows=0,nonzeros=0;}; +Sizes sizes(const ModelSnapshot& source,LpEvidencePhase phase,Meter& meter){ + Sizes out; + for(const auto& v:source.variables){meter.tick();if(!v.active)continue; + if(phase==LpEvidencePhase::Farkas){out.rows=add(out.rows,1);out.variables=add(out.variables,std::isfinite(v.lower)+std::isfinite(v.upper)); + out.nonzeros=add(out.nonzeros,2*(std::isfinite(v.lower)+std::isfinite(v.upper)));} + else out.variables=add(out.variables,1);} + for(const auto& r:source.rows){meter.tick();if(!r.active)continue; + if(phase==LpEvidencePhase::Farkas){const auto n=std::isfinite(r.lower)+std::isfinite(r.upper);out.variables=add(out.variables,n); + for(int i=0;i(std::numeric_limits::max()); + if(out.variables>=index_max||out.rows>=index_max||out.nonzeros>=index_max)unsupported("LP evidence auxiliary dimensions exceed backend index range"); + return out; +} +LpEvidenceStage build(const ModelSnapshot& source,LpEvidencePhase phase,Meter& meter){ + const auto dimensions=sizes(source,phase,meter); + // Charge snapshots, copied result values/masks, mappings and construction scratch. + meter.keep(add(add(dimensions.nonzeros,dimensions.rows),add(dimensions.variables,dimensions.variables))); + meter.keep(add(dimensions.variables,dimensions.variables)); + meter.keep(dimensions.variables); // objective terms upper bound + Model model;if(model.id()==source.model_id){Model distinct;model=std::move(distinct);} + LpEvidenceStage stage;stage.phase=phase;stage.nonzeros=dimensions.nonzeros;stage.columns.reserve(dimensions.variables); + std::vector mapping(source.variables.size());meter.tick(mapping.size()); + if(phase!=LpEvidencePhase::Farkas){ + for(const auto& v:source.variables){meter.tick();if(!v.active)continue;double lower=v.lower,upper=v.upper; + if(phase==LpEvidencePhase::Recession){lower=std::isfinite(v.lower)?0:-1;upper=std::isfinite(v.upper)?0:1;} + mapping[v.variable.id]=model.add_continuous(lower,upper);stage.columns.push_back({LpEvidenceColumnKind::SourceVariable,static_cast(v.variable.id),{}});} + for(const auto& r:source.rows){meter.tick();if(!r.active)continue;std::vector terms;terms.reserve(r.terms.size()); + for(const auto& t:r.terms){meter.tick();terms.push_back({mapping[t.variable.id],t.coefficient});} + model.add_row(terms,phase==LpEvidencePhase::Recession?(std::isfinite(r.lower)?0:-inf):r.lower, + phase==LpEvidencePhase::Recession?(std::isfinite(r.upper)?0:inf):r.upper);} + if(phase==LpEvidencePhase::Recession){std::vector terms;terms.reserve(source.objective.terms.size()); + const double sign=source.objective.sense==ObjectiveSense::Minimize?1:-1; + for(const auto& t:source.objective.terms){meter.tick();terms.push_back({mapping[t.variable.id],sign*t.coefficient});}model.minimize(terms);} + } else { + std::vector> stationarity(source.variables.size());std::vector normalization,objective; + normalization.reserve(dimensions.variables);objective.reserve(dimensions.variables); + const auto multiplier=[&](LpEvidenceColumnKind kind,std::size_t slot,LpEvidenceSide side,double endpoint){ + meter.tick();const auto v=model.add_continuous(0,1);stage.columns.push_back({kind,slot,side});normalization.push_back({v,1}); + const double sign=side==LpEvidenceSide::Lower?1:-1;if(endpoint!=0)objective.push_back({v,sign*endpoint});return v;}; + for(const auto& r:source.rows){meter.tick();if(!r.active)continue; + for(const auto side:{LpEvidenceSide::Lower,LpEvidenceSide::Upper}){const double endpoint=side==LpEvidenceSide::Lower?r.lower:r.upper; + if(!std::isfinite(endpoint))continue;const auto v=multiplier(LpEvidenceColumnKind::RowSide,r.constraint.id,side,endpoint); + for(const auto& t:r.terms){meter.tick();stationarity[t.variable.id].push_back({v,(side==LpEvidenceSide::Lower?1:-1)*t.coefficient});}}} + for(const auto& v:source.variables){meter.tick();if(!v.active)continue; + for(const auto side:{LpEvidenceSide::Lower,LpEvidenceSide::Upper}){const double endpoint=side==LpEvidenceSide::Lower?v.lower:v.upper; + if(!std::isfinite(endpoint))continue;const auto multiplier_var=multiplier(LpEvidenceColumnKind::VariableSide,v.variable.id,side,endpoint); + stationarity[v.variable.id].push_back({multiplier_var,side==LpEvidenceSide::Lower?1.0:-1.0});}} + for(const auto& v:source.variables){meter.tick();if(v.active)model.add_row(stationarity[v.variable.id],0,0);} + model.add_row(normalization,-inf,1);model.maximize(objective); + } + meter.tick(dimensions.nonzeros);stage.auxiliary_model=std::make_shared(model.snapshot()); + meter.tick(0);validate_structure(*stage.auxiliary_model);meter.tick(0);return stage; +} +bool known(Termination t){switch(t){case Termination::Unknown:case Termination::Optimal:case Termination::Infeasible:case Termination::Unbounded: + case Termination::InfeasibleOrUnbounded:case Termination::TimeLimit:case Termination::NodeLimit:case Termination::MemoryLimit:case Termination::IterationLimit: + case Termination::SolutionLimit:case Termination::ObjectiveLimit:case Termination::Cancelled:case Termination::NumericalFailure:case Termination::Unsupported: + case Termination::InvalidModel:case Termination::BackendError:return true;}return false;} +bool complete(Termination t){return t==Termination::Optimal||t==Termination::Infeasible;} +void check_stage(LpEvidenceStage& stage,const SolveOptions& options,Meter& meter){ + const auto& model=*stage.auxiliary_model;auto& r=*stage.auxiliary_result; + if(!known(r.termination)||r.guarantee!=Guarantee::Numerical)bad("Invalid auxiliary status/guarantee"); + if(r.model_id!=model.model_id||r.revision!=model.revision)bad("Auxiliary result owner/revision mismatch"); + if(r.active_variables.size()!=model.variables.size())bad("Auxiliary result active-mask dimension mismatch"); + for(std::size_t i=0;i create(const ModelSnapshot& source,const LpEvidenceOptions& options,Meter& meter){ + auto out=std::shared_ptr(new LpEvidence);visit(source,meter,true);out->source_=source; + out->tolerances_=options.checks;out->primal_tolerance_=options.solve.feasibility_tolerance; + if(options.request!=LpEvidenceRequest::Farkas)out->primal_={LpEvidenceState::Unavailable,LpEvidenceReason::NoFeasibleBase,"No checked base point and direction"}; + if(options.request!=LpEvidenceRequest::PrimalRay)out->farkas_={LpEvidenceState::Unavailable,LpEvidenceReason::NoContradiction,"No checked contradiction"}; + return out; + } + static void prepare(LpEvidence& out,const LpEvidenceOptions& options,Meter& meter){ + const auto& source=out.source_; + meter.keep(add(add(source.variables.size(),source.variables.size()),source.rows.size())); + meter.keep(add(source.rows.size(),source.variables.size())); + // Preflight all potentially requested transforms before any backend work. + if(options.request!=LpEvidenceRequest::Farkas){sizes(source,LpEvidencePhase::FeasibleBase,meter);sizes(source,LpEvidencePhase::Recession,meter);} + if(options.request!=LpEvidenceRequest::PrimalRay)sizes(source,LpEvidencePhase::Farkas,meter); + out.stages_.reserve(3); + if(options.request!=LpEvidenceRequest::Farkas){out.stages_.push_back(build(source,LpEvidencePhase::FeasibleBase,meter));out.stages_.push_back(build(source,LpEvidencePhase::Recession,meter));} + if(options.request!=LpEvidenceRequest::PrimalRay)out.stages_.push_back(build(source,LpEvidencePhase::Farkas,meter)); + } + static bool stage(LpEvidence& out,std::size_t index,const LpEvidenceOptions& options,Meter& meter,LpEvidenceResult& result){ + auto& stage=out.stages_[index];event("before_solve",index);meter.tick(0); + if(result.attempted_calls>=options.limits.max_auxiliary_solves)throw Failure(Termination::IterationLimit,LpEvidenceReason::ResourceLimit,"LP evidence auxiliary-solve call limit reached"); + { + auto adjusted=options.solve;adjusted.backend=Backend::Highs;adjusted.guarantee=Guarantee::Numerical; + adjusted.time_limit_seconds=meter.budget.remaining();adjusted.cancellation=meter.budget.shared.cancellation(); + stage.attempted=true;++result.attempted_calls; +#ifdef GECODE_OPTIMIZE_TEST_LP_EVIDENCE + stage.auxiliary_result=lp_evidence_test_solve(stage.phase,*stage.auxiliary_model,adjusted); +#else + stage.auxiliary_result=solve(*stage.auxiliary_model,adjusted); +#endif + event("after_solve",index);meter.tick(0);check_stage(stage,options.solve,meter);event("after_check",index);meter.tick(0); + } + event("stage_cleanup",index);meter.tick(0);return stage.candidate_examined&&stage.auxiliary_check.valid; + } + static void base(LpEvidence& out,const LpEvidenceStage& stage,Meter& meter){ + auto& data=out.primal_data_;const auto& source=out.source_; + std::vector point(source.variables.size(),std::numeric_limits::quiet_NaN());meter.tick(point.size()); + for(std::size_t i=0;ivalues[i];} + visit(source,meter);auto check=validate(source,point,out.primal_tolerance_,1e-6);meter.tick(0); + if(!check.valid)throw Failure(Termination::NumericalFailure,LpEvidenceReason::FailedOriginalChecks,"Auxiliary base fails original source validation"); + data.base_point=std::move(point);data.base_check=std::move(check); + } + static void primal(LpEvidence& out,const LpEvidenceStage& stage,Meter& meter){ + auto& data=out.primal_data_;const auto& source=out.source_; + if(!data.base_check.valid){out.primal_={LpEvidenceState::Unavailable,LpEvidenceReason::NoFeasibleBase,"An improving direction alone does not establish a feasible source"};return;} + Wide scale=0;for(const auto value:stage.auxiliary_result->values){meter.tick();if(!std::isfinite(value))bad("Nonfinite direction candidate");scale=std::max(scale,std::abs(static_cast(value)));} + if(scale==0){data.direction_scale=0;out.primal_={LpEvidenceState::Unavailable,LpEvidenceReason::NoImprovingDirection,"Auxiliary returned the zero direction"};return;} + std::vector direction(source.variables.size(),std::numeric_limits::quiet_NaN());meter.tick(direction.size()); + for(std::size_t i=0;i(stage.auxiliary_result->values[i])/scale);} + Wide variable_error=0,row_error=0;std::vector activity(source.rows.size(),std::numeric_limits::quiet_NaN()); + for(const auto& v:source.variables){meter.tick();if(!v.active)continue;const Wide value=direction[v.variable.id]; + if(std::isfinite(v.lower))variable_error=std::max(variable_error,-value);if(std::isfinite(v.upper))variable_error=std::max(variable_error,value);} + for(const auto& r:source.rows){meter.tick();if(!r.active)continue;Sum sum;for(const auto& t:r.terms){meter.tick();sum.add(static_cast(t.coefficient)*direction[t.variable.id]);} + const Wide value=sum.value();activity[r.constraint.id]=narrow(value);if(std::isfinite(r.lower))row_error=std::max(row_error,-value);if(std::isfinite(r.upper))row_error=std::max(row_error,value);} + Sum slope;const Wide sign=source.objective.sense==ObjectiveSense::Minimize?1:-1; + for(const auto& t:source.objective.terms){meter.tick();slope.add(sign*static_cast(t.coefficient)*direction[t.variable.id]);} + data.direction_scale=narrow(scale);data.normalized_objective_slope=narrow(slope.value()); + data.max_variable_recession_violation=narrow(variable_error);data.max_row_recession_violation=narrow(row_error); + if(variable_error>out.tolerances_.recession||row_error>out.tolerances_.recession) + throw Failure(Termination::NumericalFailure,LpEvidenceReason::FailedOriginalChecks,"Normalized direction fails original recession checks"); + if(slope.value()>=-out.tolerances_.minimum_improvement){out.primal_={LpEvidenceState::Unavailable,LpEvidenceReason::NoImprovingDirection,"No strict normalized objective improvement"};return;} + data.direction=std::move(direction);data.row_direction=std::move(activity);out.primal_={LpEvidenceState::Available,LpEvidenceReason::None,"Checked numerical base point and improving recession direction; not an exact proof"}; + } + static void farkas(LpEvidence& out,const LpEvidenceStage& stage,Meter& meter){ + auto& data=out.farkas_data_;const auto& source=out.source_;std::vector signed_rows(source.rows.size());meter.tick(signed_rows.size()); + for(std::size_t i=0;ivalues[i]);} + Wide scale=0;for(const auto& row:signed_rows){meter.tick();scale=std::max(scale,std::abs(row.value()));} + if(scale==0){data.multiplier_scale=0;out.farkas_={LpEvidenceState::Unavailable,LpEvidenceReason::NoContradiction,"No nonzero signed original row multiplier"};return;} + std::vector rows(source.rows.size()),columns(source.variables.size());std::vector transpose(source.variables.size()); + for(const auto& r:source.rows){meter.tick();auto& entry=rows[r.constraint.id];entry.active=r.active;if(!r.active)continue; + entry.multiplier=narrow(signed_rows[r.constraint.id].value()/scale); + for(const auto& t:r.terms){meter.tick();transpose[t.variable.id].add(static_cast(t.coefficient)*entry.multiplier);}} + Wide max_stationarity=0;for(const auto& v:source.variables){meter.tick();auto& entry=columns[v.variable.id];entry.active=v.active;if(!v.active)continue; + entry.multiplier=narrow(-transpose[v.variable.id].value());Sum residual;residual.add(transpose[v.variable.id].sum);residual.add(transpose[v.variable.id].correction);residual.add(entry.multiplier); + max_stationarity=std::max(max_stationarity,std::abs(residual.value()));} + Sum margin; + const auto contribution=[&](LpFarkasEntry& entry,double lower,double upper){meter.tick();if(!entry.active||entry.multiplier==0)return; + entry.side=entry.multiplier>0?LpEvidenceSide::Lower:LpEvidenceSide::Upper;const double endpoint=entry.multiplier>0?lower:upper; + if(!std::isfinite(endpoint))throw Failure(Termination::NumericalFailure,LpEvidenceReason::FailedOriginalChecks,"Nonzero Farkas multiplier requires an infinite original side"); + const Wide term=static_cast(entry.multiplier)*endpoint;entry.contribution=narrow(term);margin.add(term);}; + data.multiplier_scale=narrow(scale);data.max_stationarity=narrow(max_stationarity); + for(const auto& r:source.rows)contribution(rows[r.constraint.id],r.lower,r.upper); + for(const auto& v:source.variables)contribution(columns[v.variable.id],v.lower,v.upper); + data.contradiction_margin=narrow(margin.value());data.rows=std::move(rows);data.columns=std::move(columns); + if(max_stationarity>out.tolerances_.stationarity)throw Failure(Termination::NumericalFailure,LpEvidenceReason::FailedOriginalChecks,"Published Farkas stationarity exceeds original tolerance"); + if(margin.value()<=out.tolerances_.minimum_contradiction){out.farkas_={LpEvidenceState::Unavailable,LpEvidenceReason::NoContradiction,"No strict positive original contradiction margin"};return;} + out.farkas_={LpEvidenceState::Available,LpEvidenceReason::None,"Checked numerical original-side contradiction; not an exact certificate"}; + if(out.primal_data_.base_check.valid)throw Failure(Termination::NumericalFailure,LpEvidenceReason::InconsistentEvidence,"Accepted original feasible point and positive Farkas margin are numerically inconsistent"); + } + static void fail(LpEvidence& out,LpEvidenceReason reason,const char* message,bool rejected) noexcept { + for(auto* group:{&out.primal_,&out.farkas_})if(group->state!=LpEvidenceState::NotRequested){ + group->state=rejected?LpEvidenceState::Rejected:LpEvidenceState::Unavailable;group->reason=reason;group->message.clear(); + try{group->message=message;}catch(...){} + } + } +}; +} +namespace { +template +LpEvidenceResult run(Snapshot snapshot,ModelId owner,Revision revision,const LpEvidenceOptions& options){ + const auto started=Clock::now();LpEvidenceResult result;result.model_id=owner;result.revision=revision; + std::optional budget;std::optional meter;std::shared_ptr artifact; + const auto fail=[&](Termination termination,LpEvidenceReason reason,const char* message){ + const bool rejected=reason==LpEvidenceReason::InvalidModel||reason==LpEvidenceReason::InvalidBackendData||reason==LpEvidenceReason::FailedOriginalChecks||reason==LpEvidenceReason::InconsistentEvidence||reason==LpEvidenceReason::Unsupported; + result.completion=rejected?LpEvidenceCompletion::Rejected:LpEvidenceCompletion::Interrupted;result.stop_reason=termination; + if(artifact)Detail::LpEvidenceAccess::fail(*artifact,reason,message,rejected); + try{result.message=message;}catch(...){result.message.clear();} + }; + try{ + options.validate();budget.emplace(options.solve,started);meter.emplace(Meter{*budget,options.limits});meter->tick(0); + { + const auto& source=snapshot();event("source_copy",0);meter->tick(0);admit(source,options,*meter); + artifact=Detail::LpEvidenceAccess::create(source,options,*meter);result.evidence=artifact; + Detail::LpEvidenceAccess::prepare(*artifact,options,*meter);event("admission",0);meter->tick(0); +#ifndef GECODE_OPTIMIZE_TEST_LP_EVIDENCE + if(!capabilities(Backend::Highs).available)unsupported("Numerical HiGHS backend is unavailable for LP evidence recovery"); +#endif + result.completion=LpEvidenceCompletion::Complete; + bool base=false; + const auto process=[&](std::size_t index){ + const bool candidate=Detail::LpEvidenceAccess::stage(*artifact,index,options,*meter,result); + const auto& stage=artifact->stages()[index]; + if(candidate){if(stage.phase==LpEvidencePhase::FeasibleBase){Detail::LpEvidenceAccess::base(*artifact,stage,*meter);base=true;} + if(stage.phase==LpEvidencePhase::Recession)Detail::LpEvidenceAccess::primal(*artifact,stage,*meter); + if(stage.phase==LpEvidencePhase::Farkas)Detail::LpEvidenceAccess::farkas(*artifact,stage,*meter);} + event("evidence_check",index);meter->tick(0); + if(!complete(stage.auxiliary_result->termination)){result.completion=LpEvidenceCompletion::Interrupted;result.stop_reason=stage.auxiliary_result->termination;result.message="Auxiliary stage stopped before definitive completion";return false;} + return true; + }; + if(options.request==LpEvidenceRequest::Farkas)process(0); + else if(process(0)){ + if(base){if(process(1)&&options.request==LpEvidenceRequest::Both)process(2);} + else if(options.request==LpEvidenceRequest::Automatic||options.request==LpEvidenceRequest::Both)process(2); + } + event("publication",artifact->stages().size());meter->tick(0); + } + event("source_cleanup",artifact?artifact->stages().size():0);meter->tick(0); + }catch(const Failure& e){fail(e.termination,e.reason,e.what());} + catch(const ModelError& e){fail(Termination::InvalidModel,LpEvidenceReason::InvalidModel,e.what());} + catch(const std::bad_alloc&){fail(Termination::MemoryLimit,LpEvidenceReason::AllocationFailure,"LP evidence allocation failed");} + catch(const std::exception& e){fail(Termination::BackendError,LpEvidenceReason::InvalidBackendData,e.what());} + if(budget)if(const auto stopped=budget->stop())fail(*stopped,LpEvidenceReason::Stopped,"Whole LP evidence budget stopped during final cleanup"); + if(meter)result.work=meter->work; + result.elapsed_seconds=std::chrono::duration(Clock::now()-started).count();return result; +} +std::size_t variable_slot(const LpEvidence& out,Variable variable){ + if(variable.model_id!=out.id()||variable.id>=out.source().variables.size()||!out.source().variables[variable.id].active)throw ModelError("LP evidence variable is foreign, missing or deleted");return variable.id;} +std::size_t row_slot(const LpEvidence& out,Constraint row){ + if(row.model_id!=out.id()||row.id>=out.source().rows.size()||!out.source().rows[row.id].active)throw ModelError("LP evidence row is foreign, missing or deleted");return row.id;} +} +void LpEvidenceTolerances::validate() const {for(double value:{recession,stationarity,minimum_improvement,minimum_contradiction}) + if(!std::isfinite(value)||value<0)throw ModelError("LP evidence tolerances must be finite and nonnegative");} +void LpEvidenceOptions::validate() const {solve.validate();checks.validate();switch(request){case LpEvidenceRequest::Automatic:case LpEvidenceRequest::PrimalRay:case LpEvidenceRequest::Farkas:case LpEvidenceRequest::Both:return;}throw ModelError("Unknown LP evidence request");} +const ModelSnapshot& LpEvidence::source() const noexcept{return source_;} +ModelId LpEvidence::id() const noexcept{return source_.model_id;} +Revision LpEvidence::revision() const noexcept{return source_.revision;} +const LpEvidenceTolerances& LpEvidence::tolerances() const noexcept{return tolerances_;} +double LpEvidence::primal_tolerance() const noexcept{return primal_tolerance_;} +const LpEvidenceGroup& LpEvidence::primal_ray() const noexcept{return primal_;} +const LpEvidenceGroup& LpEvidence::farkas() const noexcept{return farkas_;} +const LpPrimalEvidence& LpEvidence::primal_data() const noexcept{return primal_data_;} +const LpFarkasEvidence& LpEvidence::farkas_data() const noexcept{return farkas_data_;} +const std::vector& LpEvidence::stages() const noexcept{return stages_;} +double LpEvidence::base_value(Variable variable) const {const auto slot=variable_slot(*this,variable);if(!primal_data_.base_check.valid||slot>=primal_data_.base_point.size())throw ModelError("No checked original LP evidence base point");return primal_data_.base_point[slot];} +double LpEvidence::direction_value(Variable variable) const {const auto slot=variable_slot(*this,variable);if(primal_.state!=LpEvidenceState::Available||slot>=primal_data_.direction.size())throw ModelError("No available LP primal-ray evidence");return primal_data_.direction[slot];} +const LpFarkasEntry& LpEvidence::row_multiplier(Constraint row) const {const auto slot=row_slot(*this,row);if(farkas_.state!=LpEvidenceState::Available||slot>=farkas_data_.rows.size())throw ModelError("No available LP Farkas evidence");return farkas_data_.rows[slot];} +const LpFarkasEntry& LpEvidence::column_multiplier(Variable variable) const {const auto slot=variable_slot(*this,variable);if(farkas_.state!=LpEvidenceState::Available||slot>=farkas_data_.columns.size())throw ModelError("No available LP Farkas evidence");return farkas_data_.columns[slot];} +LpEvidenceResult analyze_lp_evidence(const ModelSnapshot& model,const LpEvidenceOptions& options){return run([&]()->const ModelSnapshot&{return model;},model.model_id,model.revision,options);} +LpEvidenceResult analyze_lp_evidence(const Model& model,const LpEvidenceOptions& options){return run([&]{return model.snapshot();},model.id(),model.revision(),options);} +}} diff --git a/gecode/optimize/lp_evidence.hpp b/gecode/optimize/lp_evidence.hpp new file mode 100644 index 0000000000..532a460ad8 --- /dev/null +++ b/gecode/optimize/lp_evidence.hpp @@ -0,0 +1,145 @@ +/* Explicit additional-work numerical LP evidence; never an exact certificate. */ +#ifndef GECODE_OPTIMIZE_LP_EVIDENCE_HPP +#define GECODE_OPTIMIZE_LP_EVIDENCE_HPP +#include +#include + +namespace Gecode { namespace Optimize { +enum class LpEvidenceRequest { Automatic, PrimalRay, Farkas, Both }; +struct LpEvidenceTolerances { + double recession = 1e-7; + double stationarity = 1e-7; + double minimum_improvement = 1e-7; + double minimum_contradiction = 1e-7; + void validate() const; +}; +struct LpEvidenceLimits { + std::size_t max_auxiliary_variables = 1000000; + std::size_t max_auxiliary_rows = 1000000; + std::size_t max_auxiliary_nonzeros = 10000000; + /** Aggregate source/private snapshots, values, maps and evidence vector slots. */ + std::size_t max_retained_slots = 50000000; + /** Deterministic coordinator element visits, excluding solver internals. */ + std::size_t max_work = 100000000; + /** Public solve invocations, including local empty/constant decisions. */ + std::size_t max_auxiliary_solves = 3; +}; +struct LpEvidenceOptions { + SolveOptions solve; + LpEvidenceRequest request = LpEvidenceRequest::Automatic; + LpEvidenceTolerances checks; + LpEvidenceLimits limits; + void validate() const; +}; +enum class LpEvidenceState { NotRequested, Available, Unavailable, Rejected }; +enum class LpEvidenceReason { + None, NotRequested, Unsupported, NoFeasibleBase, NoImprovingDirection, + NoContradiction, Stopped, InvalidBackendData, FailedOriginalChecks, + InconsistentEvidence, InvalidModel, ResourceLimit, AllocationFailure +}; +struct LpEvidenceGroup { + LpEvidenceState state = LpEvidenceState::NotRequested; + LpEvidenceReason reason = LpEvidenceReason::NotRequested; + std::string message; +}; +enum class LpEvidencePhase { FeasibleBase, Recession, Farkas }; +enum class LpEvidenceSide { Lower, Upper }; +enum class LpEvidenceColumnKind { SourceVariable, RowSide, VariableSide }; +struct LpEvidenceColumn { + LpEvidenceColumnKind kind = LpEvidenceColumnKind::SourceVariable; + std::size_t original_slot = 0; + std::optional side; +}; +struct LpEvidenceStage { + LpEvidencePhase phase = LpEvidencePhase::FeasibleBase; + /** Immutable private coordinates; never an original-source SolveResult. */ + std::shared_ptr auxiliary_model; + std::vector columns; + std::size_t nonzeros = 0; + /** The public auxiliary solve primitive was invoked; no vendor-run telemetry. */ + bool attempted = false; + std::optional auxiliary_result; + bool candidate_examined = false; + ValidationReport auxiliary_check; +}; +struct LpPrimalEvidence { + /** Full original-slot vectors; inactive slots are NaN. Empty until checked. */ + std::vector base_point, direction, row_direction; + ValidationReport base_check; + std::optional direction_scale, normalized_objective_slope; + std::optional max_variable_recession_violation, max_row_recession_violation; +}; +struct LpFarkasEntry { + bool active = false; + double multiplier = 0.0; + /** Absent for exactly zero multiplier; contribution is then exactly zero. */ + std::optional side; + double contribution = 0.0; +}; +struct LpFarkasEvidence { + /** Signed y for rows and independently derived z=-A^T*y for columns. + * Positive selects the lower side, negative selects the upper; accepted + * evidence has positive total finite-side contribution. Numerical only. */ + std::vector rows, columns; + std::optional multiplier_scale, contradiction_margin, max_stationarity; +}; +enum class LpEvidenceCompletion { Complete, Interrupted, Rejected }; +namespace Detail { struct LpEvidenceAccess; } +class LpEvidence { +public: + LpEvidence(const LpEvidence&) = delete; + LpEvidence& operator=(const LpEvidence&) = delete; + const ModelSnapshot& source() const noexcept; + ModelId id() const noexcept; + Revision revision() const noexcept; + const LpEvidenceTolerances& tolerances() const noexcept; + double primal_tolerance() const noexcept; + const LpEvidenceGroup& primal_ray() const noexcept; + const LpEvidenceGroup& farkas() const noexcept; + /** Diagnostics can remain on rejection; Available alone denotes acceptance. */ + const LpPrimalEvidence& primal_data() const noexcept; + const LpFarkasEvidence& farkas_data() const noexcept; + const std::vector& stages() const noexcept; + /** Check original owner/index/active status; no solve is triggered. */ + double base_value(Variable) const; + double direction_value(Variable) const; + const LpFarkasEntry& row_multiplier(Constraint) const; + const LpFarkasEntry& column_multiplier(Variable) const; +private: + friend struct Detail::LpEvidenceAccess; + LpEvidence() = default; + ModelSnapshot source_; + LpEvidenceTolerances tolerances_; + double primal_tolerance_ = 0; + LpEvidenceGroup primal_, farkas_; + LpPrimalEvidence primal_data_; + LpFarkasEvidence farkas_data_; + std::vector stages_; +}; +struct LpEvidenceResult { + ModelId model_id = 0; + Revision revision = 0; + LpEvidenceCompletion completion = LpEvidenceCompletion::Rejected; + std::optional stop_reason; + std::string message; + std::shared_ptr evidence; + /** Public auxiliary solve invocations, same unit as max_auxiliary_solves. */ + std::size_t attempted_calls = 0; + std::size_t work = 0; + double elapsed_seconds = 0.0; +}; +/** + * Fresh private auxiliary solves; no input model, session or historical result + * is changed. Numerical continuous LPs only, Auto/HiGHS. Unsupported source is + * never relaxed. Explicit Farkas needs one call; Automatic at most two; Both at + * most three. All share one time/cancel allowance and deterministic work caps. + * Complete describes requested analysis, not source-model optimality/status. + * A checked source base point is required with a primal direction. A feasible + * point plus accepted contradiction rejects both as inconsistent evidence. + * Cleanup/factorization is cooperative; a final expired budget clears evidence + * availability, retaining diagnostics and private-stage provenance only. + */ +LpEvidenceResult analyze_lp_evidence(const ModelSnapshot&, const LpEvidenceOptions& = {}); +LpEvidenceResult analyze_lp_evidence(const Model&, const LpEvidenceOptions& = {}); +}} +#endif diff --git a/gecode/optimize/lp_observations.cpp b/gecode/optimize/lp_observations.cpp new file mode 100644 index 0000000000..be0d81b8b4 --- /dev/null +++ b/gecode/optimize/lp_observations.cpp @@ -0,0 +1,477 @@ +#include +#include + +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { +namespace { +using Wide = long double; +struct Interrupted {}; +struct BadObservation : std::runtime_error { + using std::runtime_error::runtime_error; +}; +void tick(const SolveBudget& budget) { + if (budget.expired()) throw Interrupted{}; +} +void finite(double value, const char* message) { + if (!std::isfinite(value)) throw BadObservation(message); +} +double narrow(Wide value) { + if (!std::isfinite(value) || + std::abs(value) > static_cast(std::numeric_limits::max())) + throw BadObservation("Original observation arithmetic exceeds finite double range"); + const double result = static_cast(value); + if (!std::isfinite(result)) + throw BadObservation("Nonfinite original observation arithmetic"); + return result; +} +struct Sum { + Wide sum = 0, correction = 0; + void add(Wide value) { + if (!std::isfinite(value)) throw BadObservation("Nonfinite observation product"); + const Wide next = sum + value; + if (!std::isfinite(next)) throw BadObservation("Observation sum overflow"); + correction += std::abs(sum) >= std::abs(value) + ? (sum-next)+value : (value-next)+sum; + if (!std::isfinite(correction)) throw BadObservation("Observation compensation overflow"); + sum = next; + } + Wide value() const { + const Wide result = sum+correction; + if (!std::isfinite(result)) throw BadObservation("Observation sum overflow"); + return result; + } +}; +Wide difference(Wide value,double bound,const Sum* accumulated=nullptr) { + Sum result; + if (accumulated) { + result.add(accumulated->sum);result.add(accumulated->correction); + } else result.add(value); + result.add(-static_cast(bound)); + return result.value(); +} +LpObservationGroup group(LpObservationState state, LpObservationReason reason, + const std::string& message) { + return {state,reason,message}; +} +LpObservationGroup available() { + return group(LpObservationState::Available,LpObservationReason::None,""); +} +void not_requested(LpObservationGroup& out) { + out = group(LpObservationState::NotRequested,LpObservationReason::NotRequested,""); +} +void unavailable_group(LpObservationGroup& out, LpObservationReason reason, + const std::string& message) { + if (out.state == LpObservationState::NotRequested) return; + out = group(LpObservationState::Unavailable,reason,message); +} +void rejected_group(LpObservationGroup& out, LpObservationReason reason, + const std::string& message) { + if (out.state == LpObservationState::NotRequested) return; + out = group(LpObservationState::Rejected,reason,message); +} +bool supported_basis(LpBasisStatus value) { + switch (value) { + case LpBasisStatus::Lower: case LpBasisStatus::Basic: + case LpBasisStatus::Upper: case LpBasisStatus::Zero: + case LpBasisStatus::NonbasicUnspecified: return true; + } + return false; +} +void check_basis_position(LpBasisStatus status, Wide value, + double lower, double upper, double tolerance, + const Sum* accumulated=nullptr) { + if (!supported_basis(status)) throw BadObservation("Unknown backend basis status"); + switch (status) { + case LpBasisStatus::Lower: + if (!std::isfinite(lower) || std::abs(difference(value,lower,accumulated)) > tolerance) + throw BadObservation("Lower basis status disagrees with original activity/bound"); + break; + case LpBasisStatus::Upper: + if (!std::isfinite(upper) || std::abs(difference(value,upper,accumulated)) > tolerance) + throw BadObservation("Upper basis status disagrees with original activity/bound"); + break; + case LpBasisStatus::Zero: + if (std::isfinite(lower) || std::isfinite(upper) || std::abs(value) > tolerance) + throw BadObservation("Zero basis status requires a free nonbasic entity at zero"); + break; + case LpBasisStatus::NonbasicUnspecified: + if (!(std::isfinite(lower) && std::abs(difference(value,lower,accumulated)) <= tolerance) && + !(std::isfinite(upper) && std::abs(difference(value,upper,accumulated)) <= tolerance) && + !(!std::isfinite(lower) && !std::isfinite(upper) && std::abs(value) <= tolerance)) + throw BadObservation("Unspecified nonbasic entity is not at an original nonbasic position"); + break; + case LpBasisStatus::Basic: break; + } +} +Wide sign_violation(Wide multiplier, Wide value, double lower, double upper, + double primal_tolerance,const Sum* accumulated) { + const bool at_lower = std::isfinite(lower) && + difference(value,lower,accumulated) <= primal_tolerance; + const bool at_upper = std::isfinite(upper) && + -difference(value,upper,accumulated) <= primal_tolerance; + if (at_lower && at_upper) return 0; + if (at_lower) return std::max(-multiplier,Wide(0)); + if (at_upper) return std::max(multiplier,Wide(0)); + return std::abs(multiplier); +} +void check_result_layout(const ModelSnapshot& model, const SolveResult& result) { + if (result.model_id != model.model_id || result.revision != model.revision) + throw BadObservation("Result identity/revision differs from observation source"); + if (result.active_variables.size() != model.variables.size()) + throw BadObservation("Result active mask has the wrong original dimension"); + for (std::size_t i=0;i= raw.column_slots.size() || raw.column_slots[column++] != i) + throw BadObservation("Backend column map differs from original live slots"); + } + if (column != raw.column_slots.size()) throw BadObservation("Extra backend column map entries"); + for (std::size_t i=0;i= raw.row_slots.size() || raw.row_slots[row++] != i) + throw BadObservation("Backend row map differs from retained original rows"); + } + if (row != raw.row_slots.size()) throw BadObservation("Extra backend row map entries"); +} + +LpKktReport check_duals(const ModelSnapshot& model, const SolveResult& result, + const std::vector& activities, + const Detail::LpBackendObservations& raw, + const LpObservationMetadata& metadata, + const SolveBudget& budget) { + if (raw.column_duals.size() != raw.column_slots.size() || + raw.row_duals.size() != raw.row_slots.size()) + throw BadObservation("Backend dual vector dimension mismatch"); + const auto& tolerance = metadata.checks; + const Wide sense = model.objective.sense == ObjectiveSense::Minimize ? 1 : -1; + std::vector transpose(model.variables.size()); + std::vector costs(model.variables.size(),0); + std::vector row_duals(model.rows.size(),0); + for (const auto& term : model.objective.terms) costs[term.variable.id] = term.coefficient; + for (std::size_t i=0;i 0 ? lower : upper; + if (!std::isfinite(bound)) { finite_dual=false; return; } + dual_terms.add(multiplier*static_cast(bound)); + gap.add(-multiplier*static_cast(bound)); + const Wide complementarity = multiplier*difference(value,bound,accumulated); + if (!std::isfinite(complementarity)) + throw BadObservation("Complementarity product overflow"); + max_complementarity = std::max(max_complementarity,std::abs(complementarity)); + }; + std::size_t work=0; + for (std::size_t i=0;i(costs[i])*value); + } + LpKktReport out; + out.primal_valid=true; + out.max_dual_sign_violation=narrow(max_sign); + out.max_stationarity=narrow(max_stationarity); + out.dual_signs_valid=max_sign <= tolerance.dual_feasibility; + out.stationarity_valid=max_stationarity <= tolerance.stationarity; + if (finite_dual) { + out.max_complementarity=narrow(max_complementarity); + out.complementarity_valid=max_complementarity <= tolerance.complementarity; + Sum objective; + objective.add(model.objective.offset); + objective.add(sense*dual_terms.sum); + objective.add(sense*dual_terms.correction); + out.dual_objective_estimate=narrow(objective.value()); + const Wide normalized_gap=gap.value(); + out.normalized_gap=narrow(normalized_gap); + out.gap_valid=std::abs(normalized_gap) <= tolerance.objective_gap; + } + out.accepted=out.dual_signs_valid && out.stationarity_valid && + out.complementarity_valid && out.gap_valid; + if (!finite_dual) out.message="A nonzero multiplier requires an infinite endpoint"; + else if (!out.accepted) out.message="Original numerical LP KKT checks failed"; + tick(budget); + return out; +} +} + +void LpCheckTolerances::validate() const { + for (double value : {dual_feasibility,stationarity,complementarity,objective_gap}) + if (!std::isfinite(value) || value < 0) + throw ModelError("LP check tolerances must be finite and nonnegative"); +} +void LpObservationOptions::validate() const { solve.validate(); checks.validate(); } +const LpRowObservation& LpObservations::row(Constraint row) const { + if (!id() || row.model_id != id() || row.id >= rows_.size() || !rows_[row.id].active) + throw ModelError("Observation row is foreign, absent or deleted"); + return rows_[row.id]; +} +const LpColumnObservation& LpObservations::column(Variable column) const { + if (!id() || column.model_id != id() || column.id >= columns_.size() || !columns_[column.id].active) + throw ModelError("Observation variable is foreign, absent or deleted"); + return columns_[column.id]; +} +LpObservationCapabilities lp_observation_capabilities() { + const auto backend=capabilities(Backend::Highs); + LpObservationCapabilities out; + out.available=out.duals=out.basis_export=backend.available; + out.backend=backend.name;out.backend_version=backend.version; + out.limitations={"Ordinary Continuous linear models only; no active indicators/globals", + "Numerical observations only; duals require a timely optimal primal/dual point", + "Basis unavailable after adapter constant-row elision or without a vendor basis", + "No basis submission, rays, ranging or sensitivity analysis"}; + return out; +} + +namespace Detail { +std::shared_ptr LpObservationAccess::create( + ModelSnapshot model, const LpObservationOptions& options) { + options.validate(); + validate_structure(model); + auto out=std::shared_ptr(new LpObservations); + out->source_=std::move(model); + out->metadata_.checks=options.checks; + out->metadata_.primal_check_tolerance=options.solve.feasibility_tolerance; + out->rows_.resize(out->source_.rows.size()); + out->columns_.resize(out->source_.variables.size()); + for (std::size_t i=0;irows_.size();++i) out->rows_[i].active=out->source_.rows[i].active; + for (std::size_t i=0;icolumns_.size();++i) out->columns_[i].active=out->source_.variables[i].active; + if (!options.duals) not_requested(out->dual_point_); + if (!options.basis) not_requested(out->basis_); + return out; +} +std::string LpObservationAccess::unsupported(const ModelSnapshot& model, + const SolveOptions& options) { + if (options.backend == Backend::Native) return "LP observations require the HiGHS backend"; + if (options.guarantee != Guarantee::Numerical) return "LP observations support Numerical guarantees only"; + for (const auto& v : model.variables) + if (v.active && v.type != VariableType::Continuous) + return "LP observations require every active variable to be Continuous; no relaxation is performed"; + for (const auto& i : model.indicators) + if (i.active) return "LP observations do not support active original indicators"; + for (const auto& g : model.globals) + if (g.active) return "LP observations do not support active native globals"; + return {}; +} +void LpObservationAccess::unavailable(LpObservations& out,LpObservationReason reason, + const std::string& message) { + for (auto& row : out.rows_) { const bool active=row.active; row={};row.active=active; } + for (auto& col : out.columns_) { const bool active=col.active;col={};col.active=active; } + out.checks_={}; + unavailable_group(out.primal_rows_,reason,message); + unavailable_group(out.dual_point_,reason,message); + unavailable_group(out.basis_,reason,message); +} + +void LpObservationAccess::final_budget(LpObservedResult& out, + std::shared_ptr& observations,const SolveBudget& budget) noexcept { + if (!budget.expired()) return; + out.result.termination=budget.stop_reason().value_or(Termination::Unknown); + try { + out.result.message="Budget expired before LP observations completed"; + if (observations) + unavailable(*observations,LpObservationReason::Interrupted,out.result.message); + } catch (...) { + observations.reset();out.observations.reset();out.result.message.clear(); + } +} + +void LpObservationAccess::finish(LpObservations& out,const SolveResult& result, + const LpBackendObservations& raw, + const SolveBudget& budget) { + out.metadata_.backend=result.backend; + out.metadata_.backend_version=result.backend_version; + auto clear_unpublished = [&]() { + if (out.primal_rows_.state != LpObservationState::Available) + for (auto& row : out.rows_) {row.activity.reset();row.lower_slack.reset();row.upper_slack.reset();} + if (out.dual_point_.state != LpObservationState::Available) { + for (auto& row : out.rows_) {row.dual.reset();row.dual_source=LpDualSource::None;} + for (auto& col : out.columns_) col.reduced_cost.reset(); + } + if (out.basis_.state != LpObservationState::Available) { + for (auto& row : out.rows_) row.basis.reset(); + for (auto& col : out.columns_) col.basis.reset(); + } + }; + auto interrupt_pending = [&]() { + for (auto* item : {&out.primal_rows_,&out.dual_point_,&out.basis_}) + if (item->state == LpObservationState::Unavailable) + unavailable_group(*item,LpObservationReason::Interrupted,"Budget expired before observation group completed"); + clear_unpublished(); + }; + try { + tick(budget); + check_result_layout(out.source_,result); + if (!result.has_solution()) { + unavailable(out,LpObservationReason::NoPrimalPoint,"No validated original primal point"); + return; + } + const auto primal=validate(out.source_,result.values,out.metadata_.primal_check_tolerance); + if (!primal.valid || !primal.objective || !result.objective || + std::abs(static_cast(*primal.objective)-*result.objective) > out.metadata_.checks.objective_gap) + throw BadObservation("Original primal/objective check rejected observation point"); + std::vector activities(out.rows_.size()); + std::size_t work=0; + for (std::size_t i=0;i(term.coefficient)*result.values[term.variable.id]); + } + activities[i]=activity; + out.rows_[i].activity=narrow(activity.value()); + if (std::isfinite(row.lower)) out.rows_[i].lower_slack=narrow(difference(activity.value(),row.lower,&activity)); + if (std::isfinite(row.upper)) out.rows_[i].upper_slack=narrow(-difference(activity.value(),row.upper,&activity)); + } + tick(budget); + out.primal_rows_=available();out.checks_.primal_valid=true; + if (!raw.attempted) { + unavailable_group(out.dual_point_,LpObservationReason::NoBackendSolve,"No backend solve produced duals"); + unavailable_group(out.basis_,LpObservationReason::NoBackendSolve,"No backend solve produced a basis"); + return; + } + if (!raw.timely) throw Interrupted{}; + if (raw.failure != LpCaptureFailure::None) { + const bool allocation=raw.failure == LpCaptureFailure::Allocation; + rejected_group(out.dual_point_,allocation ? LpObservationReason::AllocationFailure : LpObservationReason::InvalidBackendData, + "Backend observation capture failed"); + rejected_group(out.basis_,allocation ? LpObservationReason::AllocationFailure : LpObservationReason::InvalidBackendData, + "Backend observation capture failed"); + return; + } + if (!raw.complete) throw BadObservation("Incomplete backend observation capture"); + check_mapping(out.source_,raw,budget); + for (auto tolerance : {raw.primal_tolerance,raw.dual_tolerance}) + if (tolerance && (!std::isfinite(*tolerance) || *tolerance < 0)) + throw BadObservation("Invalid effective backend tolerance"); + out.metadata_.backend_primal_tolerance=raw.primal_tolerance; + out.metadata_.backend_dual_tolerance=raw.dual_tolerance; + if (result.termination != Termination::Optimal) { + unavailable_group(out.dual_point_,LpObservationReason::NotOptimal,"Dual observations currently require final Optimal status"); + unavailable_group(out.basis_,LpObservationReason::NotOptimal,"Basis observations currently require final Optimal status"); + return; + } + if (!raw.info_valid || !raw.value_valid || !raw.primal_feasible) { + unavailable_group(out.dual_point_,LpObservationReason::NoPrimalPoint,"Backend primal/info validity flags are unavailable"); + unavailable_group(out.basis_,LpObservationReason::NoPrimalPoint,"Backend primal/info validity flags are unavailable"); + return; + } + if (out.dual_point_.state != LpObservationState::NotRequested) { + if (!raw.dual_valid || !raw.dual_feasible) { + unavailable_group(out.dual_point_,LpObservationReason::NoDualPoint,"Backend has no valid feasible dual point"); + } else { + try { + out.checks_=check_duals(out.source_,result,activities,raw,out.metadata_,budget); + if (!out.checks_.accepted) { + rejected_group(out.dual_point_,LpObservationReason::FailedChecks,out.checks_.message); + } else { + for (std::size_t i=0;i +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { +namespace Detail { struct LpObservationAccess; } + +enum class LpObservationState { NotRequested, Available, Unavailable, Rejected }; +enum class LpObservationReason { + None, NotRequested, Unsupported, NoBackendSolve, NoPrimalPoint, NotOptimal, + NoDualPoint, NoBasis, ElidedConstantRows, Interrupted, InvalidBackendData, + FailedChecks, AllocationFailure, InvalidModel +}; +enum class LpBasisStatus { Lower, Basic, Upper, Zero, NonbasicUnspecified }; +enum class LpDualSource { None, Backend, DerivedConstantRow }; + +struct LpObservationGroup { + LpObservationState state = LpObservationState::Unavailable; + LpObservationReason reason = LpObservationReason::NoBackendSolve; + std::string message; +}; + +/** Absolute tolerances in original units; each is finite and nonnegative. */ +struct LpCheckTolerances { + double dual_feasibility = 1e-7; + double stationarity = 1e-7; + double complementarity = 1e-6; + double objective_gap = 1e-6; + void validate() const; +}; +struct LpObservationOptions { + SolveOptions solve; + bool duals = true; + bool basis = true; + LpCheckTolerances checks; + void validate() const; +}; + +/** Optional metrics are absent unless their complete computation succeeded. */ +struct LpKktReport { + bool primal_valid = false; + bool dual_signs_valid = false; + bool stationarity_valid = false; + bool complementarity_valid = false; + bool gap_valid = false; + bool accepted = false; + std::optional max_dual_sign_violation; + std::optional max_stationarity; + std::optional max_complementarity; + /** Original objective sense, numerical estimate; never an exact bound. */ + std::optional dual_objective_estimate; + /** Normalized primal-minus-dual, with the common offset cancelled first. */ + std::optional normalized_gap; + std::string message; +}; + +struct LpRowObservation { + bool active = false; + std::optional activity; + /** Absent for an infinite side or unavailable primal observation. */ + std::optional lower_slack, upper_slack; + std::optional dual; + LpDualSource dual_source = LpDualSource::None; + std::optional basis; +}; +struct LpColumnObservation { + bool active = false; + std::optional reduced_cost; + std::optional basis; +}; +struct LpObservationMetadata { + std::string backend, backend_version; + LpCheckTolerances checks; + double primal_check_tolerance = 1e-7; + std::optional backend_primal_tolerance, backend_dual_tolerance; +}; + +/** + * Immutable original-coordinate data; owns the exact historical source model. + * Groups distinguish unavailable data from zero and from empty available arrays. + * Row/column lookups reject foreign handles and original tombstones. Accessors + * perform no solver work and remain usable after model/session destruction. + * Dual values use A^T*pi + reduced_cost = c in the original objective sense. + * Basis status is numerical backend data, not a proof of feasibility/optimality. + */ +class LpObservations { +public: + LpObservations(const LpObservations&) = delete; + LpObservations& operator=(const LpObservations&) = delete; + ModelId id() const noexcept { return source_.model_id; } + Revision revision() const noexcept { return source_.revision; } + const ModelSnapshot& source() const noexcept { return source_; } + const LpObservationMetadata& metadata() const noexcept { return metadata_; } + const LpObservationGroup& primal_rows() const noexcept { return primal_rows_; } + const LpObservationGroup& dual_point() const noexcept { return dual_point_; } + const LpObservationGroup& basis() const noexcept { return basis_; } + const LpKktReport& checks() const noexcept { return checks_; } + const std::vector& rows() const noexcept { return rows_; } + const std::vector& columns() const noexcept { return columns_; } + const LpRowObservation& row(Constraint constraint) const; + const LpColumnObservation& column(Variable variable) const; + +private: + LpObservations() = default; + friend struct Detail::LpObservationAccess; + ModelSnapshot source_; + LpObservationMetadata metadata_; + LpObservationGroup primal_rows_, dual_point_, basis_; + LpKktReport checks_; + std::vector rows_; + std::vector columns_; +}; + +struct LpObservedResult { + SolveResult result; + /** May be null when invalid input or allocation failure prevents capture. */ + std::shared_ptr observations; +}; +struct LpObservationCapabilities { + bool available = false; + bool duals = false; + bool basis_export = false; + std::string backend, backend_version; + std::vector limitations; +}; +LpObservationCapabilities lp_observation_capabilities(); + +/** + * HiGHS Numerical only, on original Continuous linear models without active + * indicators or globals. No automatic relaxation or fallback. The whole-call + * cooperative budget includes snapshot copying, conversion and checking. + * Duals require a timely optimal primal/dual point; basis absence + * does not invalidate a validated primal solution. No ray/ranging work occurs. + */ +LpObservedResult solve_lp_observed(const ModelSnapshot&, + const LpObservationOptions& = {}); +LpObservedResult solve_lp_observed(const Model&, + const LpObservationOptions& = {}); + +}} +#endif diff --git a/gecode/optimize/lp_observations_detail.hpp b/gecode/optimize/lp_observations_detail.hpp new file mode 100644 index 0000000000..39f3109d93 --- /dev/null +++ b/gecode/optimize/lp_observations_detail.hpp @@ -0,0 +1,39 @@ +/* Private collection/checking seam. Not installed or part of the public API. */ +#ifndef GECODE_OPTIMIZE_LP_OBSERVATIONS_DETAIL_HPP +#define GECODE_OPTIMIZE_LP_OBSERVATIONS_DETAIL_HPP +#include + +namespace Gecode { namespace Optimize { namespace Detail { + +enum class LpCaptureFailure { None, Allocation, InvalidData }; +struct LpBackendObservations { + bool requested_duals = true, requested_basis = true; + bool attempted = false, timely = false, complete = false; + bool info_valid = false, value_valid = false, primal_feasible = false; + bool dual_valid = false, dual_feasible = false; + bool basis_valid = false, info_basis_valid = false; + LpCaptureFailure failure = LpCaptureFailure::None; + ModelId model_id = 0; + Revision revision = 0; + std::vector column_slots, row_slots; + std::vector column_duals, row_duals; + std::vector column_basis, row_basis; + std::optional primal_tolerance, dual_tolerance; +}; + +struct LpObservationAccess { + static std::shared_ptr create(ModelSnapshot model, + const LpObservationOptions&); + static std::string unsupported(const ModelSnapshot&, const SolveOptions&); + static void unavailable(LpObservations&, LpObservationReason, + const std::string& message); + /** Pure original-model check of owned raw data; never calls a solver. */ + static void finish(LpObservations&, const SolveResult&, + const LpBackendObservations&, const SolveBudget&); + /** Final gate, called after raw capture destruction; also independently testable. */ + static void final_budget(LpObservedResult&, std::shared_ptr&, + const SolveBudget&) noexcept; +}; + +}}} +#endif diff --git a/gecode/optimize/lp_sensitivity.cpp b/gecode/optimize/lp_sensitivity.cpp new file mode 100644 index 0000000000..c571569732 --- /dev/null +++ b/gecode/optimize/lp_sensitivity.cpp @@ -0,0 +1,420 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { +namespace { +using Wide=long double; +using Clock=std::chrono::steady_clock; +constexpr std::size_t missing=std::numeric_limits::max(); +struct Failure : std::runtime_error { + LpSensitivityReason reason;std::optional stop; + Failure(LpSensitivityReason r,const char* text,std::optional s={}) + :std::runtime_error(text),reason(r),stop(s){} +}; +[[noreturn]] void fail(LpSensitivityReason r,const char* text) {throw Failure(r,text);} +struct Sum { + Wide sum=0,correction=0; + void add(Wide x) { + if(!std::isfinite(x))fail(LpSensitivityReason::FailedLinearSolveChecks,"Nonfinite sensitivity arithmetic"); + Wide n=sum+x;correction+=std::abs(sum)>=std::abs(x)?(sum-n)+x:(x-n)+sum;sum=n; + if(!std::isfinite(sum)||!std::isfinite(correction)) + fail(LpSensitivityReason::FailedLinearSolveChecks,"Sensitivity accumulation overflow"); + } + Wide value() const {return sum+correction;} +}; +double narrow(Wide x,LpSensitivityReason reason=LpSensitivityReason::FailedLinearSolveChecks) { + if(!std::isfinite(x)||std::abs(x)>std::numeric_limits::max())fail(reason,"Finite sensitivity value exceeds double representation"); + const double v=static_cast(x); + if(x!=0&&v==0)fail(reason,"Sensitivity conversion would erase a nonzero value"); + return v; +} +std::size_t plus(std::size_t a,std::size_t b) { + if(b>missing-a)fail(LpSensitivityReason::ResourceLimit,"Sensitivity size overflow");return a+b; +} +SolveOptions budget_options(const LpSensitivityOptions& o) { + SolveOptions s;s.time_limit_seconds=o.time_limit_seconds;s.cancellation=o.cancellation;return s; +} +bool known(Termination t) { + switch(t) { + case Termination::Unknown:case Termination::Optimal:case Termination::Infeasible: + case Termination::Unbounded:case Termination::InfeasibleOrUnbounded:case Termination::TimeLimit: + case Termination::NodeLimit:case Termination::MemoryLimit:case Termination::IterationLimit: + case Termination::SolutionLimit:case Termination::ObjectiveLimit:case Termination::Cancelled: + case Termination::NumericalFailure:case Termination::Unsupported:case Termination::InvalidModel: + case Termination::BackendError:return true; + }return false; +} +struct Inequality {Wide a,b;LpSensitivityLimiter limiter;double tolerance;}; +} +namespace Detail { +#ifdef GECODE_OPTIMIZE_TEST_LP_SENSITIVITY +std::unique_ptr lp_sensitivity_test_factor(const ModelSnapshot&,const LpBasis&,double,const SolveBudget&); +void lp_sensitivity_test_checkpoint(const char*,std::size_t); +#endif +struct LpSensitivityAccess { + const LpSensitivityOptions& opts;Clock::time_point started;SolveBudget budget; + LpSensitivityWork work; + std::shared_ptr out; + std::unique_ptr factor; + std::vector>> matrix; + std::vector entities; + std::vector col_map,row_map,basic,position; + std::vector status; + std::vector lower,upper; + std::vector point,cost,dual,reduced; + Wide sense=1; + LpSensitivityAccess(const LpSensitivityOptions& options,Clock::time_point start) + :opts(options),started(start),budget(budget_options(options)){} + const ModelSnapshot& source() const {return out->original_.observations->source();} + void check() const { + if(budget.cancelled())throw Failure(LpSensitivityReason::Stopped,"LP sensitivity cancelled",Termination::Cancelled); + if(std::chrono::duration(Clock::now()-started).count()>=opts.time_limit_seconds) + throw Failure(LpSensitivityReason::Stopped,"LP sensitivity time limit reached",Termination::TimeLimit); + } + void tick(std::size_t n=1) { + check();if(n>opts.limits.max_work-work.coordinator_visits) + throw Failure(LpSensitivityReason::ResourceLimit,"LP sensitivity coordinator work limit reached",Termination::IterationLimit); + work.coordinator_visits+=n; + } + void keep(std::size_t n) { + tick(0);if(n>opts.limits.max_retained_slots-work.retained_slots) + throw Failure(LpSensitivityReason::ResourceLimit,"LP sensitivity retained-slot limit reached",Termination::MemoryLimit); + work.retained_slots+=n; + } + void cap(std::size_t n,std::size_t maximum,const char* text) { + tick(0);if(n>maximum)throw Failure(LpSensitivityReason::ResourceLimit,text,Termination::MemoryLimit); + if(n>static_cast(std::numeric_limits::max())) + fail(LpSensitivityReason::ResourceLimit,"Sensitivity count exceeds ptrdiff_t"); + } + void event(const char* text,std::size_t index=0) { +#ifdef GECODE_OPTIMIZE_TEST_LP_SENSITIVITY + lp_sensitivity_test_checkpoint(text,index); +#else + (void)text;(void)index; +#endif + check(); + } + std::size_t entity_slot(const LpSensitivityEntity& entity) const { + if(const auto* v=std::get_if(&entity)) { + if(v->model_id!=source().model_id||v->id>=col_map.size()||col_map[v->id]==missing) + fail(LpSensitivityReason::ChangedBasis,"Foreign/deleted factor column");return col_map[v->id]; + } + const auto& r=std::get(entity); + if(r.model_id!=source().model_id||r.id>=row_map.size()||row_map[r.id]==missing) + fail(LpSensitivityReason::ChangedBasis,"Foreign/deleted factor row");return row_map[r.id]; + } + void prepare(const LpObservedResult& original) { + opts.validate();event("before_copy"); + if(!original.observations)fail(LpSensitivityReason::InvalidSource,"Missing owning LP observations"); + const auto& m=original.observations->source(); + cap(opts.parameters.size(),opts.limits.max_requests,"Too many sensitivity requests"); + std::size_t nr=0,nc=0,nz=0,slots=plus(plus(m.variables.size(),m.rows.size()),plus(m.indicators.size(),m.globals.size())); + slots=plus(slots,m.objective.terms.size());tick(slots); + for(const auto& v:m.variables){tick(v.name.size());slots=plus(slots,v.name.size());nc+=v.active;} + for(const auto& r:m.rows){tick(plus(r.name.size(),r.terms.size()));slots=plus(slots,plus(r.name.size(),r.terms.size()));nr+=r.active;if(r.active)nz=plus(nz,r.terms.size());} + for(const auto& g:m.globals){tick();if(g.active)fail(LpSensitivityReason::Unsupported,"Sensitivity does not support original globals");} + for(const auto& i:m.indicators){tick();if(i.active)fail(LpSensitivityReason::Unsupported,"Sensitivity does not support original indicators");} + // Inactive semantic payloads are still copied by the owning basis factory. + for(const auto& i:m.indicators){const auto n=plus(plus(i.terms.size(),i.generated_rows.size()),i.domains.size());tick(n);slots=plus(slots,n);} + for(const auto& g:m.globals){tick(g.name.size());slots=plus(slots,g.name.size()); + std::visit([&](const auto& d){using T=std::decay_t;std::size_t n=0; + if constexpr(std::is_same_v||std::is_same_v||std::is_same_v)n=d.variables.size(); + if constexpr(std::is_same_v)n=plus(d.elements.size(),2); + if constexpr(std::is_same_v)n=plus(plus(d.starts.size(),d.durations.size()),d.heights.size()); + if constexpr(std::is_same_v)n=d.successors.size(); + if constexpr(std::is_same_v){n=plus(n,d.tuples.size());for(const auto& tuple:d.tuples){tick();n=plus(n,tuple.size());}} + if constexpr(std::is_same_v)n=plus(n,plus(d.transitions.size(),d.final_states.size())); + tick(n);slots=plus(slots,n); + },g.payload);} + cap(nr,opts.limits.max_rows,"Too many sensitivity basis rows");cap(nc,opts.limits.max_columns,"Too many sensitivity columns"); + cap(nz,opts.limits.max_nonzeros,"Too many sensitivity nonzeros"); + if(nr&&nr>opts.limits.max_factor_entries/nr)fail(LpSensitivityReason::ResourceLimit,"Sensitivity factor-fill admission exceeded"); + keep(plus(slots,slots));keep(plus(original.result.values.size(),original.result.active_variables.size())); + cap(plus(nr,nc),missing/32,"Sensitivity workspace size overflow");keep(32*plus(nr,nc)); + keep(plus(nz,nz));keep(plus(m.variables.size(),m.rows.size()));keep(opts.parameters.size()); + validate_structure(m);tick(0); + if(opts.backend!=Backend::Auto&&opts.backend!=Backend::Highs) + fail(LpSensitivityReason::Unsupported,"Sensitivity requires the HiGHS numerical factorization backend"); + for(const auto& v:m.variables)if(v.active&&v.type!=VariableType::Continuous) + fail(LpSensitivityReason::Unsupported,"Sensitivity requires original Continuous variables"); + for(const auto& r:m.rows)if(r.active&&r.terms.empty())fail(LpSensitivityReason::NoBasis,"Sensitivity cannot map elided constant rows"); + const auto& r=original.result; + if(!known(r.termination)||r.guarantee!=Guarantee::Numerical||r.model_id!=m.model_id||r.revision!=m.revision) + fail(LpSensitivityReason::InvalidSource,"Invalid LP result status/guarantee/identity"); + if(r.termination!=Termination::Optimal)fail(LpSensitivityReason::NotOptimal,"Sensitivity requires an optimal LP result"); + const auto& o=*original.observations; + if(o.basis().state!=LpObservationState::Available)fail(LpSensitivityReason::NoBasis,"Sensitivity requires an available original basis"); + if(o.primal_rows().state!=LpObservationState::Available||o.dual_point().state!=LpObservationState::Available||!o.checks().accepted) + fail(LpSensitivityReason::FailedReferenceChecks,"Sensitivity requires accepted O1 primal/dual/KKT observations"); + if(!nr||!nc)fail(LpSensitivityReason::NoBasis,"Zero-row/column sensitivity is not implemented"); + if(!r.solution_validated||r.values.size()!=m.variables.size()||r.active_variables.size()!=m.variables.size()||!r.objective||!std::isfinite(*r.objective)) + fail(LpSensitivityReason::InvalidSource,"Missing valid original LP assignment/objective"); + if(!std::isfinite(r.elapsed_seconds)||r.elapsed_seconds<0)fail(LpSensitivityReason::InvalidSource,"Invalid original LP elapsed time"); + for(const auto* scalar:{&r.best_bound,&r.absolute_gap,&r.relative_gap,&r.native_backend_gap}) + if(*scalar&&!std::isfinite(**scalar))fail(LpSensitivityReason::InvalidSource,"Nonfinite original LP scalar"); + if((r.absolute_gap&&*r.absolute_gap<0)||(r.relative_gap&&*r.relative_gap<0)||(r.native_backend_gap&&*r.native_backend_gap<0))fail(LpSensitivityReason::InvalidSource,"Negative original LP gap"); + for(std::size_t i=0;i(*r.objective)-*r.best_bound); + const Wide scalar_rounding=4*std::numeric_limits::epsilon()* + std::max({Wide(1),std::abs(static_cast(*r.objective)),std::abs(static_cast(*r.best_bound))}); + if(gap< -std::max(static_cast(opts.checks.kkt.objective_gap),scalar_rounding)) + fail(LpSensitivityReason::InvalidSource,"Original LP bound is on the wrong side"); + const Wide absolute=std::abs(static_cast(*r.objective)-*r.best_bound); + if(r.absolute_gap&&std::abs(absolute-*r.absolute_gap)>opts.checks.kkt.objective_gap) + fail(LpSensitivityReason::InvalidSource,"Original LP absolute gap is inconsistent"); + if(r.relative_gap&&std::abs(absolute/std::max(Wide(1),std::abs(static_cast(*r.objective)))-*r.relative_gap)>opts.checks.kkt.objective_gap) + fail(LpSensitivityReason::InvalidSource,"Original LP relative gap is inconsistent"); + } + std::set> requested; + for(const auto& p:opts.parameters){tick(); + if(const auto* v=std::get_if(&p)){ + if(v->variable.model_id!=m.model_id||v->variable.id>=m.variables.size()||!m.variables[v->variable.id].active) + fail(LpSensitivityReason::InvalidSource,"Sensitivity variable is foreign/deleted"); + if(!requested.emplace(0,v->variable.id).second)fail(LpSensitivityReason::InvalidSource,"Duplicate sensitivity request"); + }else if(const auto* q=std::get_if(&p)){ + if(q->row.model_id!=m.model_id||q->row.id>=m.rows.size()||!m.rows[q->row.id].active) + fail(LpSensitivityReason::InvalidSource,"Sensitivity row is foreign/deleted"); + const auto& row=m.rows[q->row.id];if(!std::isfinite(row.lower)||row.lower!=row.upper) + fail(LpSensitivityReason::Unsupported,"Only equality common RHS sensitivity is supported"); + if(!requested.emplace(1,q->row.id).second)fail(LpSensitivityReason::InvalidSource,"Duplicate sensitivity request"); + }else fail(LpSensitivityReason::InvalidSource,"Sensitivity request has no payload");} + out=std::shared_ptr(new LpSensitivity);out->original_=original;out->tolerances_=opts.checks; + for(const auto& v:m.variables)out->columns_.push_back(v.active); + for(const auto& row:m.rows)out->rows_.push_back(row.active); + for(const auto& p:opts.parameters)out->entries_.push_back({p,{LpSensitivityState::Unavailable,LpSensitivityReason::NotRequested,"Not yet analyzed"},{}}); + out->basis_=make_lp_basis(o);event("after_copy"); + } + void setup() { + event("before_factor");work.factor_setup_attempted=true; +#ifdef GECODE_OPTIMIZE_TEST_LP_SENSITIVITY + factor=lp_sensitivity_test_factor(source(),*out->basis_,opts.checks.primal_feasibility,budget); +#else + factor=make_lp_sensitivity_factor(source(),*out->basis_,opts.checks.primal_feasibility,budget); +#endif + event("after_factor");if(!factor)fail(LpSensitivityReason::BackendFailure,"Missing private factorization"); + const auto& m=source();col_map.assign(m.variables.size(),missing);row_map.assign(m.rows.size(),missing); + const std::size_t n=factor->columns.size(),r=factor->rows.size(),total=plus(n,r); + std::size_t count=0;for(const auto& v:m.variables)count+=v.active;if(count!=n)fail(LpSensitivityReason::ChangedBasis,"Factor column count mismatch"); + count=0;for(const auto& row:m.rows)count+=row.active;if(count!=r)fail(LpSensitivityReason::ChangedBasis,"Factor row count mismatch"); + for(auto slot:factor->columns){tick();if(slot>=col_map.size()||!m.variables[slot].active||col_map[slot]!=missing) + fail(LpSensitivityReason::ChangedBasis,"Invalid factor column mapping");col_map[slot]=entities.size();entities.push_back(m.variables[slot].variable); + lower.push_back(m.variables[slot].lower);upper.push_back(m.variables[slot].upper);status.push_back(*out->basis_->columns()[slot]);} + for(auto slot:factor->rows){tick();if(slot>=row_map.size()||!m.rows[slot].active||row_map[slot]!=missing) + fail(LpSensitivityReason::ChangedBasis,"Invalid factor row mapping");row_map[slot]=entities.size();entities.push_back(m.rows[slot].constraint); + lower.push_back(-m.rows[slot].upper);upper.push_back(-m.rows[slot].lower);auto s=*out->basis_->rows()[slot]; + status.push_back(s==LpBasisStatus::Lower?LpBasisStatus::Upper:s==LpBasisStatus::Upper?LpBasisStatus::Lower:s);} + matrix.resize(total);for(std::size_t i=0;irows[i]].terms){tick();matrix[col_map[t.variable.id]].push_back({i,t.coefficient});}matrix[n+i].push_back({i,1});} + position.assign(total,missing); + if(factor->order.size()!=r)fail(LpSensitivityReason::ChangedBasis,"Wrong factor basis size"); + for(const auto& entity:factor->order){tick();const auto k=entity_slot(entity); + if(position[k]!=missing||status[k]!=LpBasisStatus::Basic)fail(LpSensitivityReason::ChangedBasis,"Duplicate/nonbasic factor entity"); + position[k]=basic.size();basic.push_back(k);} + for(std::size_t k=0;korder_=factor->order;out->backend_version_=factor->version; + cost.assign(total,0);for(const auto& t:m.objective.terms)cost[col_map[t.variable.id]]=sense*t.coefficient; + point.assign(total,0);for(std::size_t k=0;k& v) { + Sum s;s.add(anchor);for(auto [row,a]:matrix[column]){tick();s.add(-a*v[row]);}return s.value(); + } + std::vector solve_system(const std::vector& rhs,bool transpose) { + event("before_system",work.basis_solves); + if(work.basis_solves>=opts.limits.max_basis_solves)throw Failure(LpSensitivityReason::ResourceLimit,"Basis linear-system call limit reached",Termination::IterationLimit); + std::vector input;for(Wide v:rhs){tick();input.push_back(narrow(v));} + ++work.basis_solves;auto raw=factor->solve(input,transpose);event("after_system",work.basis_solves); + if(raw.size()!=basic.size())fail(LpSensitivityReason::FailedLinearSolveChecks,"Wrong basis solution count"); + std::vector result;for(double v:raw){tick();if(!std::isfinite(v))fail(LpSensitivityReason::FailedLinearSolveChecks,"Nonfinite basis solution");result.push_back(v);} + std::vector residual(basic.size()),magnitudes(basic.size()); + if(transpose){for(std::size_t p=0;popts.checks.system_absolute+opts.checks.system_relative*scale) + fail(LpSensitivityReason::FailedLinearSolveChecks,"Original basis-system residual failed");} + auto& checks=out->checks_;checks.max_system_residual=std::max(checks.max_system_residual.value_or(0),narrow(max_absolute)); + checks.max_scaled_system_residual=std::max(checks.max_scaled_system_residual.value_or(0),narrow(max_scaled));return result; + } + void reference() { + std::vector sums(basic.size()); + for(std::size_t k=0;k rhs;for(const auto& s:sums)rhs.push_back(s.value()); + const auto primal=solve_system(rhs,false);for(std::size_t p=0;p values(source().variables.size(),std::numeric_limits::quiet_NaN()); + Wide difference=0;for(std::size_t j=0;jcolumns.size();++j){tick();values[factor->columns[j]]=narrow(point[j]);difference=std::max(difference,std::abs(point[j]-out->original_.result.values[factor->columns[j]]));} + auto& checks=out->checks_;checks.max_point_difference=narrow(difference);checks.basis_point_matches=difference<=opts.checks.primal_feasibility; + checks.primal=validate(source(),values,opts.checks.primal_feasibility,0); + auto& kkt=checks.kkt;kkt.primal_valid=checks.primal.valid; + if(!checks.primal.valid||!checks.basis_point_matches)fail(LpSensitivityReason::FailedReferenceChecks,"Selected basis point disagrees with original feasible LP point"); + reduced.resize(point.size());Wide max_stationarity=0,max_sign=0,max_comp=0;Sum normalized_gap,dual_terms; + for(std::size_t k=0;k0?lower[k]:upper[k];if(!std::isfinite(side))fail(LpSensitivityReason::FailedReferenceChecks,"Basis dual requires an infinite bound"); + dual_terms.add(value*side);normalized_gap.add(-value*side);max_comp=std::max(max_comp,std::abs(value*(point[k]-side)));} + } + // Public primal activities are independently recomputed by validate; also + // check all retained logical coordinates against A*x, not only B residuals. + std::vector activity(basic.size()); + for(std::size_t k=0;kopts.checks.primal_feasibility) + fail(LpSensitivityReason::FailedReferenceChecks,"Logical basis coordinate disagrees with original row activity"); + kkt.max_stationarity=narrow(max_stationarity);kkt.max_dual_sign_violation=narrow(std::max(Wide(0),max_sign));kkt.max_complementarity=narrow(max_comp); + kkt.normalized_gap=narrow(normalized_gap.value());Sum objective;objective.add(source().objective.offset);objective.add(sense*dual_terms.sum);objective.add(sense*dual_terms.correction);kkt.dual_objective_estimate=narrow(objective.value()); + kkt.stationarity_valid=max_stationarity<=opts.checks.kkt.stationarity;kkt.dual_signs_valid=max_sign<=opts.checks.kkt.dual_feasibility; + kkt.complementarity_valid=max_comp<=opts.checks.kkt.complementarity;kkt.gap_valid=std::abs(normalized_gap.value())<=opts.checks.kkt.objective_gap; + kkt.accepted=kkt.primal_valid&&kkt.stationarity_valid&&kkt.dual_signs_valid&&kkt.complementarity_valid&&kkt.gap_valid; + if(!kkt.accepted)fail(LpSensitivityReason::FailedReferenceChecks,"Selected basis failed original numerical KKT checks");event("after_reference"); + } + LpSensitivityLimiter limiter(std::size_t k,LpSensitivitySide side,bool dual_condition) { + if(k>=factor->columns.size()&&(side==LpSensitivitySide::Lower||side==LpSensitivitySide::Upper)) + side=side==LpSensitivitySide::Lower?LpSensitivitySide::Upper:LpSensitivitySide::Lower; + return {entities[k],side,dual_condition}; + } + LpParameterInterval interval(const LpSensitivityParameter& parameter) { + const std::size_t total=point.size();std::vector inequalities; + inequalities.reserve(2*total);LpParameterInterval output; + if(const auto* p=std::get_if(¶meter)) { + const auto j=col_map[p->variable.id];output.anchor=narrow(sense*cost[j]);output.objective_slope=narrow(point[j]); + std::vector v(basic.size(),0);if(position[j]!=missing){std::vector rhs(basic.size(),0);rhs[position[j]]=sense;v=solve_system(rhs,true);} + for(std::size_t k=0;k(parameter);const auto selected=row_map[q.row.id]; + output.anchor=source().rows[q.row.id].lower;std::vector direction(total,0); + if(position[selected]==missing){std::vector rhs(basic.size(),0);rhs[selected-factor->columns.size()]=1; + const auto v=solve_system(rhs,false);for(std::size_t k=0;kcolumns.size();++j){tick();slope.add(sense*cost[j]*direction[j]);}output.objective_slope=narrow(slope.value()); + if(position[selected]==missing&&std::abs(slope.value()-sense*dual[selected-factor->columns.size()])>opts.checks.kkt.stationarity) + fail(LpSensitivityReason::FailedIntervalChecks,"Equality objective slope disagrees with original row dual"); + for(std::size_t k=0;k::infinity(),hi=std::numeric_limits::infinity(); + for(const auto& q:inequalities){tick();if(!std::isfinite(q.a)||!std::isfinite(q.b))fail(LpSensitivityReason::FailedIntervalChecks,"Nonfinite interval inequality"); + if(q.b==0){if(q.a<0)fail(LpSensitivityReason::FailedIntervalChecks,"Numerical basis has no fixed-basis interval");continue;} + const Wide endpoint=-q.a/q.b;if(!std::isfinite(endpoint))fail(LpSensitivityReason::FailedIntervalChecks,"Finite interval ratio overflowed"); + if(q.b>0&&endpoint>lo){lo=endpoint;output.lower_limiter=q.limiter;} + if(q.b<0&&endpoint0||hi<0||lo>hi)fail(LpSensitivityReason::FailedIntervalChecks,"Numerical interval excludes its source anchor"); + auto end=[&](Wide delta,bool lower_end) { + if(!std::isfinite(delta))return LpRangeEnd{lower_end?LpRangeEndKind::NegativeInfinity:LpRangeEndKind::PositiveInfinity,{}}; + Sum value;value.add(output.anchor);value.add(delta);return LpRangeEnd{LpRangeEndKind::Finite,narrow(value.value(),LpSensitivityReason::FailedIntervalChecks)}; + }; + output.lower=end(lo,true);output.upper=end(hi,false); + // Recheck the published absolute values, including rounding during narrowing. + const Wide published_lo=output.lower.value?static_cast(*output.lower.value)-output.anchor:lo; + const Wide published_hi=output.upper.value?static_cast(*output.upper.value)-output.anchor:hi; + Wide max_violation=0;bool lower_limited=!std::isfinite(lo),upper_limited=!std::isfinite(hi); + for(const auto& q:inequalities){tick(); + if(std::isfinite(published_lo)){Sum v;v.add(q.a);v.add(q.b*published_lo);const Wide x=v.value();max_violation=std::max(max_violation,-x); + if(x< -q.tolerance)fail(LpSensitivityReason::FailedIntervalChecks,"Published lower endpoint fails original affine checks"); + if(q.b>0&&std::abs(x)<=q.tolerance)lower_limited=true; + }else if(q.b>0)fail(LpSensitivityReason::FailedIntervalChecks,"Invalid negative-infinity interval direction"); + if(std::isfinite(published_hi)){Sum v;v.add(q.a);v.add(q.b*published_hi);const Wide x=v.value();max_violation=std::max(max_violation,-x); + if(x< -q.tolerance)fail(LpSensitivityReason::FailedIntervalChecks,"Published upper endpoint fails original affine checks"); + if(q.b<0&&std::abs(x)<=q.tolerance)upper_limited=true; + }else if(q.b<0)fail(LpSensitivityReason::FailedIntervalChecks,"Invalid positive-infinity interval direction");} + if(!lower_limited||!upper_limited)fail(LpSensitivityReason::FailedIntervalChecks,"Finite endpoint has no checked limiting condition"); + output.checks={true,inequalities.size(),narrow(std::max(Wide(0),max_violation)),!std::isfinite(lo),!std::isfinite(hi),"Original affine interval checks accepted numerically"};return output; + } + static void clear_owned(const std::shared_ptr& data,LpSensitivityReason reason,const char* text) noexcept { + if(!data)return; + for(auto& entry:data->entries_){entry.group.state=LpSensitivityState::Unavailable;entry.group.reason=reason;entry.interval.reset();} + try {for(auto& entry:data->entries_)entry.group.message=text;}catch(...){for(auto& entry:data->entries_)entry.group.message.clear();} + } + void clear(LpSensitivityReason reason,const char* text) noexcept {clear_owned(out,reason,text);} + void release() {factor.reset();matrix.clear();entities.clear();col_map.clear();row_map.clear();basic.clear();position.clear();status.clear();lower.clear();upper.clear();point.clear();cost.clear();dual.clear();reduced.clear();event("after_cleanup");} + void run(const LpObservedResult& original,LpSensitivityResult& result) { + try { + prepare(original);result.sensitivity=out;setup();reference();std::size_t accepted=0; + for(std::size_t i=0;ientries_.size();++i){event("before_interval",i);auto& entry=out->entries_[i]; + try {entry.interval=interval(entry.parameter);entry.group={LpSensitivityState::Available,LpSensitivityReason::None,"Fixed-basis numerical interval available"};++accepted;} + catch(const Failure& e){if(e.stop||e.reason==LpSensitivityReason::ResourceLimit||e.reason==LpSensitivityReason::Stopped)throw; + entry.interval.reset();entry.group={LpSensitivityState::Rejected,e.reason,e.what()};} + event("after_interval",i);} + result.completion=accepted==out->entries_.size()?LpSensitivityCompletion::Complete:accepted?LpSensitivityCompletion::Partial:LpSensitivityCompletion::Rejected; + result.reason=result.completion==LpSensitivityCompletion::Complete?LpSensitivityReason::None:LpSensitivityReason::FailedIntervalChecks; + result.message=accepted==out->entries_.size()?"Requested fixed-basis numerical intervals accepted":"Some requested intervals failed numerical checks"; + check(); + } catch(const Failure& e) { + factor.reset();clear(e.reason,e.what());result.completion=e.stop?LpSensitivityCompletion::Interrupted:LpSensitivityCompletion::Rejected; + result.reason=e.reason;result.stop_reason=e.stop;result.message=e.what(); + } catch(const LpSensitivityBackendError& e) { + factor.reset();clear(e.reason,e.what());result.reason=e.reason;result.message=e.what();result.completion=LpSensitivityCompletion::Rejected; + } catch(const ModelError& e) { + factor.reset();clear(LpSensitivityReason::InvalidSource,e.what());result.reason=LpSensitivityReason::InvalidSource;result.message=e.what();result.completion=LpSensitivityCompletion::Rejected; + } + result.sensitivity=out;result.work=work; + } +}; +} +void LpSensitivityTolerances::validate() const { + kkt.validate();for(double v:{primal_feasibility,system_absolute,system_relative}) + if(!std::isfinite(v)||v<0)throw ModelError("LP sensitivity tolerances must be finite and nonnegative"); +} +void LpSensitivityOptions::validate() const { + checks.validate();if(std::isnan(time_limit_seconds)||time_limit_seconds<0)throw ModelError("Invalid sensitivity time limit"); + if(parameters.empty())throw ModelError("Sensitivity requires a nonempty explicit parameter list"); + if(backend!=Backend::Auto&&backend!=Backend::Highs&&backend!=Backend::Native)throw ModelError("Unknown sensitivity backend"); +} +const LpSensitivityEntry* LpSensitivity::objective(Variable v) const { + if(v.model_id!=id()||v.id>=columns_.size()||!columns_[v.id])throw ModelError("Sensitivity variable is foreign, absent or deleted"); + for(const auto& e:entries_)if(const auto* p=std::get_if(&e.parameter))if(p->variable==v)return &e;return nullptr; +} +const LpSensitivityEntry* LpSensitivity::equality_rhs(Constraint r) const { + if(r.model_id!=id()||r.id>=rows_.size()||!rows_[r.id])throw ModelError("Sensitivity row is foreign, absent or deleted"); + for(const auto& e:entries_)if(const auto* p=std::get_if(&e.parameter))if(p->row.model_id==r.model_id&&p->row.id==r.id)return &e;return nullptr; +} +LpSensitivityResult analyze_lp_sensitivity(const LpObservedResult& original,const LpSensitivityOptions& options) { + const auto start=Clock::now();LpSensitivityResult result;result.model_id=original.result.model_id;result.revision=original.result.revision; + std::unique_ptr access;bool options_valid=false; + try {options.validate();options_valid=true;access=std::make_unique(options,start);access->run(original,result);} + catch(const std::bad_alloc&){if(access)access->clear(LpSensitivityReason::AllocationFailure,"LP sensitivity allocation failed"); + result.completion=LpSensitivityCompletion::Interrupted;result.reason=LpSensitivityReason::AllocationFailure;result.stop_reason=Termination::MemoryLimit;result.message.clear();} + catch(const ModelError& e){if(access)access->clear(LpSensitivityReason::InvalidSource,e.what()); + result.completion=LpSensitivityCompletion::Rejected;result.reason=LpSensitivityReason::InvalidSource;try{result.message=e.what();}catch(...){result.message.clear();}} + catch(const std::exception& e){if(access)access->clear(LpSensitivityReason::BackendFailure,e.what()); + result.completion=LpSensitivityCompletion::Rejected;result.reason=LpSensitivityReason::BackendFailure;try{result.message=e.what();}catch(...){result.message.clear();}} + std::shared_ptr owned; + if(access){owned=access->out;result.sensitivity=owned;result.work=access->work; + try{access->release();access->check();}catch(const Failure& e){access->clear(e.reason,e.what());result.completion=LpSensitivityCompletion::Interrupted;result.reason=e.reason;result.stop_reason=e.stop;try{result.message=e.what();}catch(...){result.message.clear();}} + catch(...){access->clear(LpSensitivityReason::BackendFailure,"LP sensitivity cleanup failed");result.completion=LpSensitivityCompletion::Rejected;result.reason=LpSensitivityReason::BackendFailure;result.message.clear();} + access.reset(); + } + // Includes destruction of retained vector capacity, backend state and budget. + std::optional final_stop; + if(options_valid&&options.cancellation&&options.cancellation->cancelled())final_stop=Termination::Cancelled; + else if(options_valid&&std::chrono::duration(Clock::now()-start).count()>=options.time_limit_seconds)final_stop=Termination::TimeLimit; + if(final_stop){Detail::LpSensitivityAccess::clear_owned(owned,LpSensitivityReason::Stopped,"Whole LP sensitivity allowance stopped during cleanup"); + result.completion=LpSensitivityCompletion::Interrupted;result.reason=LpSensitivityReason::Stopped;result.stop_reason=final_stop; + try{result.message="Whole LP sensitivity allowance stopped during cleanup";}catch(...){result.message.clear();} + } + result.elapsed_seconds=std::chrono::duration(Clock::now()-start).count();return result; +} +}} diff --git a/gecode/optimize/lp_sensitivity.hpp b/gecode/optimize/lp_sensitivity.hpp new file mode 100644 index 0000000000..6657ddf982 --- /dev/null +++ b/gecode/optimize/lp_sensitivity.hpp @@ -0,0 +1,163 @@ +/* Owning numerical sensitivity of one specified optimal continuous LP basis. */ +#ifndef GECODE_OPTIMIZE_LP_SENSITIVITY_HPP +#define GECODE_OPTIMIZE_LP_SENSITIVITY_HPP +#include +#include +#include + +namespace Gecode { namespace Optimize { +namespace Detail { struct LpSensitivityAccess; } + +struct LpObjectiveParameter { Variable variable; }; +struct LpEqualityRhsParameter { Constraint row; }; +using LpSensitivityParameter=std::variant; +using LpSensitivityEntity=std::variant; + +enum class LpSensitivityState { NotRequested,Available,Unavailable,Rejected }; +enum class LpSensitivityReason { + None,NotRequested,Unsupported,NotOptimal,NoBasis,InvalidSource,InvalidBasis, + ChangedBasis,FailedReferenceChecks,FailedLinearSolveChecks,FailedIntervalChecks, + ResourceLimit,Stopped,AllocationFailure,BackendFailure +}; +struct LpSensitivityGroup { + LpSensitivityState state=LpSensitivityState::Unavailable; + LpSensitivityReason reason=LpSensitivityReason::NotRequested; + std::string message; +}; +struct LpSensitivityTolerances { + double primal_feasibility=1e-7; + LpCheckTolerances kkt; + /** |B*v-rhs| <= absolute + relative*(|B|*|v|+|rhs|), per equation. */ + double system_absolute=1e-9,system_relative=1e-9; + void validate() const; +}; +struct LpSensitivityLimits { + std::size_t max_rows=4096,max_columns=100000,max_nonzeros=1000000; + std::size_t max_requests=4096,max_basis_solves=8194; + /** Checked m*m admission, not an exact backend allocation/RSS bound. */ + std::size_t max_factor_entries=16777216; + /** Logical retained slots and coordinator visits, not bytes or CPU work. */ + std::size_t max_retained_slots=20000000,max_work=100000000; +}; +struct LpSensitivityOptions { + Backend backend=Backend::Auto; + double time_limit_seconds=std::numeric_limits::infinity(); + std::shared_ptr cancellation; + /** Nonempty, unique requests; complete preflight precedes factorization. */ + std::vector parameters; + LpSensitivityTolerances checks; + LpSensitivityLimits limits; + void validate() const; +}; + +enum class LpRangeEndKind { Finite,NegativeInfinity,PositiveInfinity }; +struct LpRangeEnd { + LpRangeEndKind kind=LpRangeEndKind::Finite; + /** In an available interval, present exactly for Finite. Infinity is a + * direction, not a parameter value. Default/unavailable data does not + * describe an interval endpoint. */ + std::optional value; +}; +enum class LpSensitivitySide { Lower,Upper,Fixed,Free }; +struct LpSensitivityLimiter { + LpSensitivityEntity entity; + LpSensitivitySide side=LpSensitivitySide::Lower; + bool dual_condition=false; +}; +struct LpIntervalCheckReport { + bool accepted=false; + std::size_t inequalities=0; + std::optional max_endpoint_violation; + bool lower_direction_checked=false,upper_direction_checked=false; + std::string message; +}; +struct LpParameterInterval { + double anchor=0; + LpRangeEnd lower,upper; + /** Original objective change = slope*(parameter-anchor), no offset. */ + std::optional objective_slope; + std::optional lower_limiter,upper_limiter; + LpIntervalCheckReport checks; +}; +struct LpSensitivityEntry { + LpSensitivityParameter parameter; + LpSensitivityGroup group; + /** Present only for Available; a singleton is distinct from unavailable. */ + std::optional interval; +}; +struct LpSensitivityReferenceChecks { + ValidationReport primal; + LpKktReport kkt; + bool basis_point_matches=false; + std::optional max_point_difference; + std::optional max_system_residual,max_scaled_system_residual; +}; +struct LpSensitivityWork { + bool factor_setup_attempted=false; + /** Attempted private factor-system calls, including calls failing before the + * backend accessor is entered; not optimization iterations or run telemetry. */ + std::size_t basis_solves=0,coordinator_visits=0,retained_slots=0; +}; + +/** Immutable historical source/basis/ranges. Accessors perform no backend work. + * Original-slot masks include tombstones. Lookups reject foreign/tombstone IDs; + * nullptr means the valid historical entity was not requested. A model/session + * edit or destruction never changes this object. All intervals are qualified + * Numerical, for one parameter and this selected basis/status assignment only. + */ +class LpSensitivity { +public: + LpSensitivity(const LpSensitivity&)=delete; + LpSensitivity& operator=(const LpSensitivity&)=delete; + ModelId id() const noexcept {return original_.result.model_id;} + Revision revision() const noexcept {return original_.result.revision;} + const LpObservedResult& original() const noexcept {return original_;} + const std::shared_ptr& basis() const noexcept {return basis_;} + const std::vector& factor_order() const noexcept {return order_;} + const std::vector& entries() const noexcept {return entries_;} + const LpSensitivityEntry* objective(Variable) const; + const LpSensitivityEntry* equality_rhs(Constraint) const; + const std::vector& active_columns() const noexcept {return columns_;} + const std::vector& active_rows() const noexcept {return rows_;} + const LpSensitivityReferenceChecks& checks() const noexcept {return checks_;} + const LpSensitivityTolerances& tolerances() const noexcept {return tolerances_;} + const std::string& backend_version() const noexcept {return backend_version_;} +private: + LpSensitivity()=default; + friend struct Detail::LpSensitivityAccess; + LpObservedResult original_; + std::shared_ptr basis_; + std::vector order_; + std::vector entries_; + std::vector columns_,rows_; + LpSensitivityReferenceChecks checks_; + LpSensitivityTolerances tolerances_; + std::string backend_version_; +}; +enum class LpSensitivityCompletion { Complete,Partial,Interrupted,Rejected }; +struct LpSensitivityResult { + ModelId model_id=0; + Revision revision=0; + LpSensitivityCompletion completion=LpSensitivityCompletion::Rejected; + LpSensitivityReason reason=LpSensitivityReason::InvalidSource; + std::optional stop_reason; + std::string message; + std::shared_ptr sensitivity; + LpSensitivityWork work; + double elapsed_seconds=0; +}; + +/** Additional private factorization/linear algebra, zero optimization runs. + * HiGHS/Auto only; a timely optimal solve_lp_observed() result on a Continuous + * linear source with checked duals and complete basis. Empty row/column systems, + * active indicators/globals, elided constant rows, automatic relaxation, + * replacement bases and stale-revision transplants are unsupported. Only + * objective coefficients and equality common RHS are supported parameter kinds. + * Factorization/triangular solves are cooperatively budgeted. A final whole-call + * limit, cancellation, resource or cleanup failure clears every availability; + * original solve provenance and completed diagnostics remain historical data. + */ +LpSensitivityResult analyze_lp_sensitivity(const LpObservedResult&, + const LpSensitivityOptions&); +}} +#endif diff --git a/gecode/optimize/lp_sensitivity_backend.cpp b/gecode/optimize/lp_sensitivity_backend.cpp new file mode 100644 index 0000000000..63981ccc4b --- /dev/null +++ b/gecode/optimize/lp_sensitivity_backend.cpp @@ -0,0 +1,98 @@ +#include +#include +#include +#ifdef GECODE_OPTIMIZE_WITH_HIGHS +#include +#endif +namespace Gecode { namespace Optimize { namespace Detail { +namespace { +[[noreturn]] void fail(LpSensitivityReason reason,const char* text) { + throw LpSensitivityBackendError(reason,text); +} +#ifdef GECODE_OPTIMIZE_WITH_HIGHS +void check(const SolveBudget& budget) { + if(budget.expired())fail(LpSensitivityReason::Stopped,"LP factorization budget stopped"); +} +void ok(HighsStatus status,const char* text) { + if(status!=HighsStatus::kOk)fail(LpSensitivityReason::BackendFailure,text); +} +HighsBasisStatus status(LpBasisStatus input) { + switch(input) { + case LpBasisStatus::Lower:return HighsBasisStatus::kLower; + case LpBasisStatus::Upper:return HighsBasisStatus::kUpper; + case LpBasisStatus::Basic:return HighsBasisStatus::kBasic; + case LpBasisStatus::Zero:return HighsBasisStatus::kZero; + case LpBasisStatus::NonbasicUnspecified:break; + } + fail(LpSensitivityReason::InvalidBasis,"Sensitivity requires explicit basis statuses"); +} +struct Factor final : LpSensitivityFactor { + Highs backend; + std::vector solve(const std::vector& rhs,bool transpose) override { + if(rhs.size()!=rows.size()||rhs.empty()) + fail(LpSensitivityReason::InvalidBasis,"Invalid basis-system RHS size"); + for(double x:rhs)if(!std::isfinite(x)) + fail(LpSensitivityReason::FailedLinearSolveChecks,"Nonfinite basis-system RHS"); + std::vector values(rows.size(),0); + ok(transpose?backend.getBasisTransposeSolve(rhs.data(),values.data()): + backend.getBasisSolve(rhs.data(),values.data()),"HiGHS basis-system solve failed"); + for(double x:values)if(!std::isfinite(x)) + fail(LpSensitivityReason::FailedLinearSolveChecks,"Nonfinite basis-system solution"); + return values; + } +}; +#endif +} +std::unique_ptr make_lp_sensitivity_factor( + const ModelSnapshot& source,const LpBasis& basis,double tolerance,const SolveBudget& budget) { +#ifndef GECODE_OPTIMIZE_WITH_HIGHS + (void)source;(void)basis;(void)tolerance;(void)budget; + fail(LpSensitivityReason::Unsupported,"This build has no HiGHS LP factorization backend"); +#else + check(budget); + LpBasisAccess::compatible(basis,source,budget); + auto compiled=compile_lp_sensitivity(source,tolerance); + check(budget); + if(compiled.rows.empty())fail(LpSensitivityReason::NoBasis,"Zero-row sensitivity is not implemented"); + auto out=std::make_unique(); + out->columns=std::move(compiled.columns);out->rows=std::move(compiled.rows); + out->version=out->backend.version(); + ok(out->backend.setOptionValue("output_flag",false),"LP factorization log setup failed"); + ok(out->backend.setOptionValue("threads",1),"LP factorization thread setup failed"); + ok(out->backend.setOptionValue("presolve","off"),"LP factorization presolve setup failed"); + ok(out->backend.setOptionValue("time_limit",budget.remaining_seconds()),"LP factorization time setup failed"); + ok(out->backend.passModel(compiled.lp),"LP factorization model load failed or changed input"); + check(budget); + HighsBasis requested;requested.alien=false;requested.valid=true;requested.useful=true; + for(auto slot:out->columns)requested.col_status.push_back(status(*basis.columns()[slot])); + for(auto slot:out->rows)requested.row_status.push_back(status(*basis.rows()[slot])); + ok(out->backend.setBasis(requested,"Gecode original LP sensitivity basis"),"LP sensitivity basis rejected"); + check(budget); + std::vector order(out->rows.size()); + if(out->backend.getBasicVariables(order.data())!=HighsStatus::kOk||!out->backend.hasInvert()) + fail(LpSensitivityReason::InvalidBasis,"Selected LP basis is singular or cannot be factored without repair"); + check(budget); + const auto& actual=out->backend.getBasis(); + if(!actual.valid||actual.col_status!=requested.col_status||actual.row_status!=requested.row_status) + fail(LpSensitivityReason::ChangedBasis,"Backend changed selected sensitivity basis"); + std::vector seen_columns(out->columns.size()),seen_rows(out->rows.size()); + for(HighsInt index:order) { + if(index>=0) { + const auto k=static_cast(index); + if(k>=out->columns.size()||seen_columns[k]||requested.col_status[k]!=HighsBasisStatus::kBasic) + fail(LpSensitivityReason::ChangedBasis,"Invalid structural factor basis mapping"); + seen_columns[k]=true;out->order.push_back(source.variables[out->columns[k]].variable); + } else { + const auto k=static_cast(-(index+1)); + if(k>=out->rows.size()||seen_rows[k]||requested.row_status[k]!=HighsBasisStatus::kBasic) + fail(LpSensitivityReason::ChangedBasis,"Invalid logical factor basis mapping"); + seen_rows[k]=true;out->order.push_back(source.rows[out->rows[k]].constraint); + } + } + // The private model remains unsolved: no optimization run or status is invented. + if(out->backend.getModelStatus()!=HighsModelStatus::kNotset||out->backend.getInfo().valid) + fail(LpSensitivityReason::BackendFailure,"Unexpected optimization state during factor-only analysis"); + check(budget);return out; +#endif +} +}}} diff --git a/gecode/optimize/lp_sensitivity_backend.hpp b/gecode/optimize/lp_sensitivity_backend.hpp new file mode 100644 index 0000000000..2e63fd0c6b --- /dev/null +++ b/gecode/optimize/lp_sensitivity_backend.hpp @@ -0,0 +1,22 @@ +/* Private factorization interface: never installed. No optimization operation. */ +#ifndef GECODE_OPTIMIZE_LP_SENSITIVITY_BACKEND_HPP +#define GECODE_OPTIMIZE_LP_SENSITIVITY_BACKEND_HPP +#include +namespace Gecode { namespace Optimize { namespace Detail { +struct LpSensitivityFactor { + std::vector columns,rows; + std::vector order; + std::string version; + virtual ~LpSensitivityFactor()=default; + virtual std::vector solve(const std::vector&,bool transpose)=0; +}; +class LpSensitivityBackendError : public std::runtime_error { +public: + LpSensitivityReason reason; + LpSensitivityBackendError(LpSensitivityReason r,const char* message) + :std::runtime_error(message),reason(r){} +}; +std::unique_ptr make_lp_sensitivity_factor( + const ModelSnapshot&,const LpBasis&,double primal_tolerance,const SolveBudget&); +}}} +#endif diff --git a/gecode/optimize/lp_sensitivity_highs_detail.hpp b/gecode/optimize/lp_sensitivity_highs_detail.hpp new file mode 100644 index 0000000000..0ba5aedf08 --- /dev/null +++ b/gecode/optimize/lp_sensitivity_highs_detail.hpp @@ -0,0 +1,13 @@ +/* Private existing-adapter bridge. Deliberately excluded from installation. */ +#ifndef GECODE_OPTIMIZE_LP_SENSITIVITY_HIGHS_DETAIL_HPP +#define GECODE_OPTIMIZE_LP_SENSITIVITY_HIGHS_DETAIL_HPP +#include +#include +namespace Gecode { namespace Optimize { namespace Detail { +struct LpSensitivityCompiled { + HighsLp lp; + std::vector columns,rows; +}; +LpSensitivityCompiled compile_lp_sensitivity(const ModelSnapshot&,double primal_tolerance); +}}} +#endif diff --git a/gecode/optimize/model.cpp b/gecode/optimize/model.cpp new file mode 100644 index 0000000000..f4f928f2be --- /dev/null +++ b/gecode/optimize/model.cpp @@ -0,0 +1,494 @@ +/* Sparse numerical model implementation. */ +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { +namespace { + +ModelId allocate_model_id() { + static std::atomic next{1}; + ModelId candidate = next.load(std::memory_order_relaxed); + for (;;) { + if (candidate == std::numeric_limits::max()) + throw ModelError("Model identity space exhausted"); + if (next.compare_exchange_weak(candidate, candidate + 1, + std::memory_order_relaxed)) + return candidate; + } +} + +void check_bounds(double lower, double upper) { + if (std::isnan(lower) || std::isnan(upper) || + lower == std::numeric_limits::infinity() || + upper == -std::numeric_limits::infinity() || lower > upper) + throw ModelError("Invalid lower/upper bounds"); +} + +void check_variable_bounds(VariableType type, double lower, double upper) { + check_bounds(lower, upper); + switch (type) { + case VariableType::Continuous: + case VariableType::Integer: + break; + case VariableType::Binary: + if (lower < 0.0 || upper > 1.0) + throw ModelError("Binary bounds must be within [0,1]"); + break; + case VariableType::SemiContinuous: + case VariableType::SemiInteger: + if (!std::isfinite(lower) || lower <= 0.0) + throw ModelError("Semi-variable nonzero interval must have a positive finite lower bound"); + break; + default: + throw ModelError("Unknown variable type"); + } +} + +void check_finite(double value, const char* message) { + if (!std::isfinite(value)) + throw ModelError(message); +} + +void check_sense(ObjectiveSense sense) { + if (sense != ObjectiveSense::Minimize && sense != ObjectiveSense::Maximize) + throw ModelError("Unknown objective sense"); +} + +bool references(const std::vector& terms, Variable variable) { + return std::any_of(terms.begin(), terms.end(), [variable](const Term& term) { + return term.variable == variable; + }); +} + +std::vector replaced(const std::vector& terms, Variable variable, + double coefficient) { + std::vector result; + result.reserve(terms.size() + 1); + for (const Term& term : terms) + if (term.variable != variable) + result.push_back(term); + if (coefficient != 0.0) + result.push_back({variable, coefficient}); + return result; +} + +} // namespace + +Model::Model() : model_id_(allocate_model_id()) {} + +Model::Model(Model&& other) noexcept + : model_id_(std::exchange(other.model_id_, 0)), + revision_(std::exchange(other.revision_, 0)), + variables_(std::move(other.variables_)), rows_(std::move(other.rows_)), + objective_(std::move(other.objective_)), indicators_(std::move(other.indicators_)), + globals_(std::move(other.globals_)) {} + +Model& Model::operator=(Model&& other) noexcept { + if (this != &other) { + model_id_ = std::exchange(other.model_id_, 0); + revision_ = std::exchange(other.revision_, 0); + variables_ = std::move(other.variables_); + rows_ = std::move(other.rows_); + objective_ = std::move(other.objective_); + indicators_ = std::move(other.indicators_); + globals_ = std::move(other.globals_); + } + return *this; +} + +void Model::require_live() const { + if (model_id_ == 0) + throw ModelError("Cannot use a moved-from model"); +} + +void Model::require_revision_capacity() const { + require_live(); + if (revision_ == std::numeric_limits::max()) + throw ModelError("Model revision space exhausted"); +} + +const VariableData& Model::variable(Variable handle) const { + require_live(); + if (handle.model_id != model_id_ || handle.id >= variables_.size() || + !variables_[static_cast(handle.id)].active) + throw ModelError("Variable handle is foreign, invalid, or deleted"); + return variables_[static_cast(handle.id)]; +} + +const RowData& Model::row(Constraint handle) const { + require_live(); + if (handle.model_id != model_id_ || handle.id >= rows_.size() || + !rows_[static_cast(handle.id)].active) + throw ModelError("Constraint handle is foreign, invalid, or deleted"); + return rows_[static_cast(handle.id)]; +} + +std::vector Model::normalize(const std::vector& terms, + const std::vector* additions) const { + require_live(); + for (const Term& term : terms) { + if (term.variable.id < variables_.size() || !additions) { + (void) variable(term.variable); + } else if (term.variable.model_id != model_id_ || + term.variable.id - variables_.size() >= additions->size() || + !(*additions)[static_cast(term.variable.id - variables_.size())].active) { + throw ModelError("Variable handle is foreign, invalid, or deleted"); + } + check_finite(term.coefficient, "Linear coefficients must be finite"); + } + std::vector ordered = terms; + // Sorting duplicate coefficients as well as IDs gives a fixed summation order + // independent of insertion order. This is numerical, not exact arithmetic. + std::sort(ordered.begin(), ordered.end(), [](const Term& a, const Term& b) { + return a.variable.id != b.variable.id ? a.variable.id < b.variable.id : + a.coefficient < b.coefficient; + }); + std::vector result; + result.reserve(ordered.size()); + for (std::size_t i = 0; i < ordered.size();) { + const Variable handle = ordered[i].variable; + long double sum = 0.0L, correction = 0.0L; + do { + const long double value = static_cast(ordered[i].coefficient); + const long double next = sum + value; + if (!std::isfinite(next)) + throw ModelError("Linear coefficient coalescing overflow"); + // Neumaier compensation also helps targets where long double is double. + correction += std::abs(sum) >= std::abs(value) ? + (sum - next) + value : (value - next) + sum; + sum = next; + if (!std::isfinite(correction)) + throw ModelError("Linear coefficient coalescing overflow"); + ++i; + } while (i < ordered.size() && ordered[i].variable == handle); + sum += correction; + if (!std::isfinite(sum)) + throw ModelError("Linear coefficient coalescing overflow"); + if (sum < -static_cast(std::numeric_limits::max()) || + sum > static_cast(std::numeric_limits::max())) + throw ModelError("Coalesced coefficient exceeds the numerical range"); + const double coefficient = static_cast(sum); + if (coefficient != 0.0) + result.push_back({handle, coefficient}); + } + return result; +} + +ModelSnapshot Model::snapshot() const { + require_live(); + return {model_id_, revision_, variables_, rows_, objective_, indicators_, globals_}; +} + +Variable Model::add_variable(VariableType type, double lower, double upper, + std::string name) { + require_revision_capacity(); + check_variable_bounds(type, lower, upper); + if (variables_.size() >= std::numeric_limits::max()) + throw ModelError("Variable slot space exhausted"); + Variable handle{model_id_, static_cast(variables_.size())}; + variables_.push_back({handle, type, lower, upper, std::move(name), true, std::nullopt}); + ++revision_; + return handle; +} + +Variable Model::add_continuous(double lower, double upper, std::string name) { + return add_variable(VariableType::Continuous, lower, upper, std::move(name)); +} + +Variable Model::add_integer(double lower, double upper, std::string name) { + return add_variable(VariableType::Integer, lower, upper, std::move(name)); +} + +Variable Model::add_binary(std::string name) { + return add_variable(VariableType::Binary, 0.0, 1.0, std::move(name)); +} + +Constraint Model::add_row(const std::vector& terms, double lower, + double upper, std::string name) { + require_revision_capacity(); + check_bounds(lower, upper); + auto normalized = normalize(terms); + if (rows_.size() >= std::numeric_limits::max()) + throw ModelError("Constraint slot space exhausted"); + Constraint handle{model_id_, static_cast(rows_.size())}; + rows_.push_back({handle, std::move(normalized), lower, upper, std::move(name), true, std::nullopt}); + ++revision_; + return handle; +} + +namespace { +template +void check_bulk_capacity(const std::vector& target, std::size_t additions) { + if (additions > target.max_size() - target.size() || + additions > std::numeric_limits::max() - target.size()) + throw ModelError("Bulk addition exceeds the model slot capacity"); +} +template +void append_prepared(std::vector& target, std::vector& prepared) { + static_assert(std::is_nothrow_move_constructible::value, + "Atomic bulk commit requires noexcept entity moves"); + // A failed reserve preserves both the old values and their addresses. All + // subsequent moves are nonthrowing and fit within the reserved allocation. + const auto required = target.size() + prepared.size(); + if (required > target.capacity()) { + const auto growth = std::min(target.capacity(), target.max_size() - target.capacity()); + target.reserve(std::max(required, target.capacity() + growth)); + } + for (auto& item : prepared) target.push_back(std::move(item)); +} +} + +std::vector Model::add_variables(const std::vector& input) { + require_live(); + if (input.empty()) return {}; + require_revision_capacity(); + check_bulk_capacity(variables_, input.size()); + std::vector prepared; + std::vector handles; + prepared.reserve(input.size()); handles.reserve(input.size()); + for (std::size_t i = 0; i < input.size(); ++i) { + const auto& item = input[i]; + check_variable_bounds(item.type, item.lower, item.upper); + const Variable handle{model_id_, static_cast(variables_.size() + i)}; + prepared.push_back({handle, item.type, item.lower, item.upper, item.name, true, std::nullopt}); + handles.push_back(handle); + } + append_prepared(variables_, prepared); + ++revision_; + return handles; +} + +std::vector Model::add_rows(const std::vector& input) { + require_live(); + if (input.empty()) return {}; + require_revision_capacity(); + check_bulk_capacity(rows_, input.size()); + std::vector prepared; + std::vector handles; + prepared.reserve(input.size()); handles.reserve(input.size()); + for (std::size_t i = 0; i < input.size(); ++i) { + const auto& item = input[i]; + check_bounds(item.lower, item.upper); + const Constraint handle{model_id_, static_cast(rows_.size() + i)}; + prepared.push_back({handle, normalize(item.terms), item.lower, item.upper, item.name, true, std::nullopt}); + handles.push_back(handle); + } + append_prepared(rows_, prepared); + ++revision_; + return handles; +} + +std::vector Model::add_rows_sparse(const SparseRowBatch& input) { + require_live(); + const auto count = input.lower.size(), nonzeros = input.coefficient.size(); + if (input.row_start.empty() || input.row_start.size() - 1 != count || + input.upper.size() != count || input.column.size() != nonzeros || + (!input.names.empty() && input.names.size() != count) || + input.row_start.front() != 0 || input.row_start.back() != nonzeros) + throw ModelError("Invalid sparse row-batch dimensions or endpoints"); + for (std::size_t i = 0; i < count; ++i) + if (input.row_start[i] > input.row_start[i + 1] || input.row_start[i + 1] > nonzeros) + throw ModelError("Sparse row offsets must be monotone and within nonzeros"); + // Reject stale/foreign/duplicate column handles even when a column is unused. + std::vector seen; + seen.reserve(input.columns.size()); + for (const auto handle : input.columns) { (void) variable(handle); seen.push_back(handle.id); } + std::sort(seen.begin(), seen.end()); + if (std::adjacent_find(seen.begin(), seen.end()) != seen.end()) + throw ModelError("Sparse row-batch column mapping contains duplicate variables"); + for (const auto column : input.column) + if (column >= input.columns.size()) throw ModelError("Sparse row-batch column index is out of range"); + if (!count) return {}; + require_revision_capacity(); + check_bulk_capacity(rows_, count); + std::vector prepared; + std::vector handles; + prepared.reserve(count); handles.reserve(count); + for (std::size_t i = 0; i < count; ++i) { + check_bounds(input.lower[i], input.upper[i]); + std::vector terms; + terms.reserve(input.row_start[i + 1] - input.row_start[i]); + for (auto k = input.row_start[i]; k < input.row_start[i + 1]; ++k) + terms.push_back({input.columns[input.column[k]], input.coefficient[k]}); + const Constraint handle{model_id_, static_cast(rows_.size() + i)}; + prepared.push_back({handle, normalize(terms), input.lower[i], input.upper[i], + input.names.empty() ? std::string{} : input.names[i], true, std::nullopt}); + handles.push_back(handle); + } + append_prepared(rows_, prepared); + ++revision_; + return handles; +} + +void Model::set_objective(const std::vector& terms, ObjectiveSense sense, + double offset) { + require_revision_capacity(); + check_sense(sense); + check_finite(offset, "Objective offset must be finite"); + auto normalized = normalize(terms); + objective_.terms.swap(normalized); + objective_.offset = offset; + objective_.sense = sense; + ++revision_; +} + +void Model::minimize(const std::vector& terms, double offset) { + set_objective(terms, ObjectiveSense::Minimize, offset); +} + +void Model::maximize(const std::vector& terms, double offset) { + set_objective(terms, ObjectiveSense::Maximize, offset); +} + +void Model::set_bounds(Variable handle, double lower, double upper) { + const VariableType type = variable(handle).type; + require_revision_capacity(); + check_variable_bounds(type, lower, upper); + for (const auto& indicator : indicators_) { + if (!indicator.active) continue; + if (indicator.inactive_gate && *indicator.inactive_gate == handle && + (lower != 0.0 || upper != 1.0)) + throw ModelError("Cannot change an active indicator gate's bounds; remove_indicator first"); + for (const auto& domain : indicator.domains) { + if (domain.variable != handle) continue; + const double effective_lower = type == VariableType::SemiContinuous || + type == VariableType::SemiInteger ? 0.0 : lower; + if (effective_lower < domain.lower || upper > domain.upper) + throw ModelError("Bound widening invalidates an indicator formulation; remove and rebuild it first"); + } + } + auto& data = variables_[static_cast(handle.id)]; + data.lower = lower; + data.upper = upper; + ++revision_; +} + +void Model::set_bounds(Constraint handle, double lower, double upper) { + (void) row(handle); + require_unprotected(handle); + require_revision_capacity(); + check_bounds(lower, upper); + auto& data = rows_[static_cast(handle.id)]; + data.lower = lower; + data.upper = upper; + ++revision_; +} + +void Model::set_coefficient(Constraint constraint, Variable handle, + double coefficient) { + (void) variable(handle); + const auto& data = row(constraint); + require_unprotected(constraint); + require_revision_capacity(); + check_finite(coefficient, "Linear coefficients must be finite"); + auto normalized = normalize(replaced(data.terms, handle, coefficient)); + rows_[static_cast(constraint.id)].terms.swap(normalized); + ++revision_; +} + +void Model::set_objective_coefficient(Variable handle, double coefficient) { + (void) variable(handle); + require_revision_capacity(); + check_finite(coefficient, "Objective coefficients must be finite"); + auto normalized = normalize(replaced(objective_.terms, handle, coefficient)); + objective_.terms.swap(normalized); + ++revision_; +} + +void Model::set_objective_offset(double offset) { + require_revision_capacity(); + check_finite(offset, "Objective offset must be finite"); + objective_.offset = offset; + ++revision_; +} + +void Model::set_name(Variable handle, std::string name) { + (void) variable(handle); + require_revision_capacity(); + variables_[static_cast(handle.id)].name.swap(name); + ++revision_; +} + +void Model::set_name(Constraint handle, std::string name) { + (void) row(handle); + require_revision_capacity(); + rows_[static_cast(handle.id)].name.swap(name); + ++revision_; +} + +void Model::remove(Variable handle) { + (void) variable(handle); + require_revision_capacity(); + for (const auto& record : globals_) if (record.active) + for (const auto variable : Detail::global_variables(record.payload)) + if (variable == handle) throw ModelError("Cannot remove a variable referenced by an active global constraint"); + for (const auto& indicator : indicators_) { + if (!indicator.active) continue; + if (indicator.activator == handle || + (indicator.inactive_gate && *indicator.inactive_gate == handle) || + references(indicator.terms, handle)) + throw ModelError("Cannot remove an active indicator variable; remove_indicator first"); + } + if (references(objective_.terms, handle)) + throw ModelError("Cannot remove a variable referenced by the objective"); + for (const RowData& data : rows_) + if (data.active && references(data.terms, handle)) + throw ModelError("Cannot remove a variable referenced by an active constraint"); + variables_[static_cast(handle.id)].active = false; + ++revision_; +} + +void Model::remove(Constraint handle) { + (void) row(handle); + require_unprotected(handle); + require_revision_capacity(); + auto& data = rows_[static_cast(handle.id)]; + data.active = false; + data.terms.clear(); + ++revision_; +} + +void Model::require_unprotected(Constraint handle) const { + for (const auto& indicator : indicators_) { + if (!indicator.active) continue; + for (const auto& generated : indicator.generated_rows) + if (generated.model_id == handle.model_id && generated.id == handle.id) + throw ModelError("Cannot edit an indicator's generated row; remove_indicator first"); + } +} + +GlobalConstraint Model::add_global(GlobalPayload payload, std::string name) { + require_revision_capacity(); + Detail::validate_global_payload(payload, model_id_, variables_, true); + if (globals_.size() >= std::numeric_limits::max()) + throw ModelError("Global constraint slot space exhausted"); + GlobalConstraint handle{model_id_, static_cast(globals_.size())}; + globals_.push_back({handle, std::move(payload), std::move(name), true}); + ++revision_; + return handle; +} +const GlobalData& Model::global(GlobalConstraint handle) const { + require_live(); + if (handle.model_id != model_id_ || handle.id >= globals_.size() || !globals_[handle.id].active) + throw ModelError("Global constraint handle is foreign, invalid, or deleted"); + return globals_[handle.id]; +} +void Model::remove(GlobalConstraint handle) { + (void) global(handle); require_revision_capacity(); + globals_[handle.id].active = false; + ++revision_; +} +void Model::set_name(GlobalConstraint handle, std::string name) { + (void) global(handle); require_revision_capacity(); + globals_[handle.id].name.swap(name); + ++revision_; +} + +}} diff --git a/gecode/optimize/model.hpp b/gecode/optimize/model.hpp new file mode 100644 index 0000000000..c1b5f8d087 --- /dev/null +++ b/gecode/optimize/model.hpp @@ -0,0 +1,244 @@ +/* Sparse numerical model for the additive optimization API. */ +#ifndef GECODE_OPTIMIZE_MODEL_HPP +#define GECODE_OPTIMIZE_MODEL_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { + +struct Term { + Variable variable; + double coefficient = 0.0; +}; + +struct VariableSpec { + VariableType type = VariableType::Continuous; + double lower = 0.0; + double upper = std::numeric_limits::infinity(); + std::string name; +}; +struct RowSpec { + std::vector terms; + double lower = -std::numeric_limits::infinity(); + double upper = std::numeric_limits::infinity(); + std::string name; +}; +/** + * CSR row input with an explicit unique live-variable column mapping. + * Row starts have rows+1 entries, begin at zero and end at coefficients.size(). + * Column indices may be unsorted/repeated within a row; normal coalescing applies. + * Names are empty or have exactly one entry per row. No dense matrix is created. + */ +struct SparseRowBatch { + std::vector columns; + std::vector row_start{0}, column; + std::vector coefficient, lower, upper; + std::vector names; +}; + +struct Indicator { + ModelId model_id = 0; + std::uint64_t id = 0; +}; + +struct VariableData { + Variable variable; + VariableType type = VariableType::Continuous; + double lower = 0.0; + double upper = std::numeric_limits::infinity(); + std::string name; + bool active = true; + std::optional indicator_origin; +}; + +struct RowData { + Constraint constraint; + std::vector terms; + double lower = -std::numeric_limits::infinity(); + double upper = std::numeric_limits::infinity(); + std::string name; + bool active = true; + std::optional indicator_origin; +}; + +struct ObjectiveData { + std::vector terms; + double offset = 0.0; + ObjectiveSense sense = ObjectiveSense::Minimize; +}; + +/** A domain used to justify a bounded indicator's inactive relaxation. */ +struct IndicatorDomain { + Variable variable; + double lower = 0.0; + double upper = 0.0; +}; + +/** Original logical meaning and the complete, guarded linear lowering. */ +struct IndicatorData { + Indicator indicator; + Variable activator; + bool active_value = true; + std::vector terms; + double lower = -std::numeric_limits::infinity(); + double upper = std::numeric_limits::infinity(); + std::optional inactive_gate; + std::optional lower_m; + std::optional upper_m; + std::vector generated_rows; + std::vector domains; + bool active = true; +}; + +struct GlobalConstraint { ModelId model_id = 0; std::uint64_t id = 0; }; +struct AllDifferentData { std::vector variables; }; +struct ElementData { + Variable index; + std::vector elements; + Variable result; + std::int64_t index_base = 0; +}; +struct TableData { + std::vector variables; + std::vector> tuples; +}; +struct CumulativeData { + std::vector starts; + std::vector durations; + std::vector heights; + std::int64_t capacity = 0; +}; +struct CircuitData { + std::vector successors; + std::int64_t index_base = 0; +}; +struct RegularTransition { + std::uint64_t from = 0; + std::int64_t symbol = 0; + std::uint64_t to = 0; +}; +struct RegularData { + std::vector variables; + std::uint64_t state_count = 1; + std::uint64_t initial_state = 0; + std::vector transitions; + std::vector final_states; +}; +using GlobalPayload = std::variant; +struct GlobalData { + GlobalConstraint global; + GlobalPayload payload; + std::string name; + bool active = true; +}; + +/** An owning historical snapshot. Public fields are untrusted at API boundaries. */ +struct ModelSnapshot { + ModelId model_id = 0; + Revision revision = 0; + std::vector variables; + std::vector rows; + ObjectiveData objective; + std::vector indicators; + std::vector globals; +}; + +namespace Detail { class ConstraintBatch; } + +/** + * An owning sparse model with a linear objective and retained logical and + * global constraints. + * + * Handles identify a model and a never-reused slot. Successful mutations advance + * the revision, including assignments of unchanged values. Failed mutations leave + * the model unchanged. Snapshots own their data and remain usable after edits. + * + * Copying is disabled. Moving preserves the identity and all existing handles; + * a moved-from model can only be destroyed, assigned another model, or queried + * for its zero id/revision. Mutations and snapshot() reject a moved-from object. + * Mutation is not thread-safe; independent models can be created concurrently. + */ +class Model { +public: + Model(); + Model(const Model&) = delete; + Model& operator=(const Model&) = delete; + Model(Model&& other) noexcept; + Model& operator=(Model&& other) noexcept; + + ModelId id() const noexcept { return model_id_; } + Revision revision() const noexcept { return revision_; } + ModelSnapshot snapshot() const; + + Variable add_variable(VariableType type, double lower, double upper, + std::string name = {}); + Variable add_continuous(double lower = 0.0, + double upper = std::numeric_limits::infinity(), + std::string name = {}); + Variable add_integer(double lower = 0.0, + double upper = std::numeric_limits::infinity(), + std::string name = {}); + Variable add_binary(std::string name = {}); + Constraint add_row(const std::vector& terms, double lower, double upper, + std::string name = {}); + + /** Atomic bulk additions; one revision per nonempty batch, no reused slots. + * Validation/allocation failure leaves entities, revision and existing views + * unchanged. Empty valid batches are no-ops. Rows refer to existing variables. + */ + std::vector add_variables(const std::vector& variables); + std::vector add_rows(const std::vector& rows); + std::vector add_rows_sparse(const SparseRowBatch& rows); + + void set_objective(const std::vector& terms, ObjectiveSense sense, + double offset = 0.0); + void minimize(const std::vector& terms, double offset = 0.0); + void maximize(const std::vector& terms, double offset = 0.0); + + void set_bounds(Variable variable, double lower, double upper); + void set_bounds(Constraint constraint, double lower, double upper); + void set_coefficient(Constraint constraint, Variable variable, + double coefficient); + void set_objective_coefficient(Variable variable, double coefficient); + void set_objective_offset(double offset); + void set_name(Variable variable, std::string name); + void set_name(Constraint constraint, std::string name); + + /** Reject removal while an active row or the objective references variable. */ + void remove(Variable variable); + /** Remove a row, retaining its slot as a tombstone. */ + void remove(Constraint constraint); + + const VariableData& variable(Variable variable) const; + const RowData& row(Constraint constraint) const; + GlobalConstraint add_global(GlobalPayload payload, std::string name = {}); + const GlobalData& global(GlobalConstraint constraint) const; + void remove(GlobalConstraint constraint); + void set_name(GlobalConstraint constraint, std::string name); + +private: + friend class Detail::ConstraintBatch; + ModelId model_id_; + Revision revision_ = 0; + std::vector variables_; + std::vector rows_; + ObjectiveData objective_; + std::vector indicators_; + std::vector globals_; + + void require_live() const; + void require_revision_capacity() const; + void require_unprotected(Constraint constraint) const; + std::vector normalize(const std::vector& terms, + const std::vector* additions = nullptr) const; +}; + +}} +#endif diff --git a/gecode/optimize/native.cpp b/gecode/optimize/native.cpp new file mode 100644 index 0000000000..10b1ad7054 --- /dev/null +++ b/gecode/optimize/native.cpp @@ -0,0 +1,2520 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef GECODE_OPTIMIZE_WITH_NATIVE +#include +#include +#endif + +#if defined(GECODE_OPTIMIZE_WITH_NATIVE) && defined(GECODE_OPTIMIZE_WITH_HIGHS) && \ + defined(__SIZEOF_INT128__) && (defined(__GNUC__) || defined(__clang__)) +#define GECODE_OPTIMIZE_NATIVE_LP_ENABLED 1 +#include +#include +#endif + +namespace Gecode { namespace Optimize { +#ifdef GECODE_NATIVE_SEARCH_TEST_HOOKS +// Coordinator-only fault injection; no production callback or ABI surface. +void native_search_test_event(const char* event); +#endif +#ifdef GECODE_NATIVE_ROOT_CUT_TEST_HOOKS +void native_root_cut_test_event(const char* event, NativeRootCoverCompletion& completion); +#endif +#ifdef GECODE_NATIVE_START_TEST_HOOKS +void native_start_test_event(const char* event); +#endif +#ifdef GECODE_NATIVE_BRANCHING_TEST_HOOKS +void native_branching_test_event(const char* event, std::size_t slot, double& down, double& up); +#endif +#ifdef GECODE_NATIVE_NEIGHBORHOOD_TEST_HOOKS +void native_neighborhood_test_event(const char*, std::size_t, int, int, std::vector*); +#endif +namespace { +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED +namespace LP = Gecode::Experimental::LpRelaxation; +#endif +class Unsupported : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; +struct Interrupted { Termination reason; }; +#ifdef GECODE_OPTIMIZE_WITH_NATIVE +void neighborhood_event(const char* event, std::size_t slot=0, int lower=0, int upper=0, + std::vector* values=nullptr) { +#ifdef GECODE_NATIVE_NEIGHBORHOOD_TEST_HOOKS + native_neighborhood_test_event(event,slot,lower,upper,values); +#else + (void)event;(void)slot;(void)lower;(void)upper;(void)values; +#endif +} +void checkpoint(const SolveBudget& budget) { + if (const auto reason = budget.stop_reason()) throw Interrupted{*reason}; +} + +using NeighborhoodCompletion = NativeNeighborhoodCompletion; +struct NeighborhoodStopped { NeighborhoodCompletion reason; }; +class NeighborhoodBudget { + const NativeNeighborhoodSettings& settings; + NativeNeighborhoodStatistics& statistics; + const SolveBudget& shared; + const double started; +public: + NeighborhoodBudget(const NativeNeighborhoodSettings& settings0, + NativeNeighborhoodStatistics& statistics0,const SolveBudget& shared0) + :settings(settings0),statistics(statistics0),shared(shared0),started(shared0.elapsed_seconds()) {} + void check(const char* event="neighborhood_checkpoint") const { + neighborhood_event(event); + if(shared.cancelled()) throw Interrupted{Termination::Cancelled}; + if(shared.time_limit_reached()) throw Interrupted{Termination::TimeLimit}; + if(shared.elapsed_seconds()-started>=settings.time_limit_seconds) + throw NeighborhoodStopped{NeighborhoodCompletion::LocalTimeLimit}; + } + void work(std::size_t count=1) { + check(); + if(count>settings.max_coordinator_work-statistics.coordinator_work) + throw NeighborhoodStopped{NeighborhoodCompletion::WorkLimit}; + statistics.coordinator_work+=count; + } + void source(std::size_t count=1) { + check(); + if(count>settings.max_source_entries-statistics.source_entries) + throw NeighborhoodStopped{NeighborhoodCompletion::SourceLimit}; + work(count); statistics.source_entries+=count; + } + void finish() noexcept {statistics.elapsed_seconds=shared.elapsed_seconds()-started;} + double start_seconds() const noexcept {return started;} +}; + +void construction_checkpoint(const SolveBudget& budget,NeighborhoodBudget* local, + const char* event="neighborhood_construction") { + if(local) local->check(event); else checkpoint(budget); +} + +constexpr std::int64_t exact_double_integer = INT64_C(9007199254740992); + +int integer(double value, const char* kind) { + if (!std::isfinite(value) || value != std::trunc(value) || + value < Int::Limits::min || value > Int::Limits::max) + throw Unsupported(std::string(kind) + " must be integral and within native Gecode Int::Limits"); + return static_cast(value); +} +std::optional finite_bound(double value) { + return std::isfinite(value) ? std::optional(integer(value, "Row bound")) : std::nullopt; +} +struct NativeVariable { + std::size_t slot; + int lower; + int upper; + bool semi; +}; +struct NativeTerm { int column; int coefficient; }; +struct NativeRow { + std::vector terms; + std::optional lower; + std::optional upper; +}; +struct NativeIndicator { + int activator; + bool active_value; + std::optional gate; + NativeRow row; +}; +struct NativeRegular { + int initial = 0; + bool accepts_empty = false; + std::vector transitions; // Includes explicit native sentinel. + std::vector finals; // Deduplicated, with final -1. +}; +NativeRegular prepare_regular(const RegularData& data,const SolveBudget& budget) { + std::map states;states.emplace(data.initial_state,0); + std::set finals; + std::set symbols; + for(const auto& edge:data.transitions){ + checkpoint(budget); + if(edge.symbolInt::Limits::max) + throw Unsupported("Regular alphabet symbol exceeds native Gecode limits"); + symbols.insert(static_cast(edge.symbol)); + states.emplace(edge.from,0);states.emplace(edge.to,0); + } + for(auto state:data.final_states){checkpoint(budget);states.emplace(state,0);finals.insert(state);} + const bool short_symbols=symbols.empty()||(*symbols.begin()>=std::numeric_limits::min()&&*symbols.rbegin()<=std::numeric_limits::max()); + if(const char* error=Detail::native_regular_size_error(data.variables.size(),states.size(),data.transitions.size(),symbols.size(),short_symbols)) + throw Unsupported(error); + // Original state IDs need not be small or dense. Unreferenced declared states + // have no transitions or accepting role and never require native storage. + int next=0;for(auto& entry:states){checkpoint(budget);entry.second=next++;} + NativeRegular result;result.initial=states.at(data.initial_state); + result.accepts_empty=finals.count(data.initial_state)!=0; + result.transitions.reserve(data.transitions.size()+1);result.finals.reserve(finals.size()+1); + for(const auto& edge:data.transitions){checkpoint(budget);result.transitions.emplace_back(states.at(edge.from),static_cast(edge.symbol),states.at(edge.to));} + result.transitions.emplace_back(-1,0,0); + for(auto state:finals){checkpoint(budget);result.finals.push_back(states.at(state));} + result.finals.push_back(-1);checkpoint(budget);return result; +} +struct Compiled { + std::vector variables; + std::vector rows; + std::vector indicators; + std::vector objective; + std::vector columns; + std::vector globals; + std::map regulars; + int cost_lower = 0; + int cost_upper = 0; + std::int64_t offset = 0; + bool minimize = true; + bool binary_domains = true; +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + std::shared_ptr relaxation; + // Keep exact original-source cut attribution for every live native clone. + std::shared_ptr root_cover_evidence; + LP::IntegerOptions relaxation_options; +#endif +}; + +Compiled compile(const ModelSnapshot& model, const SolveBudget& budget) { + Compiled result; + std::vector column(model.variables.size(), -1); + for (const auto& variable : model.variables) { + neighborhood_event("compile_variable",static_cast(variable.variable.id)); + checkpoint(budget); + if (!variable.active) continue; + result.binary_domains = result.binary_domains && + variable.type == VariableType::Binary && variable.lower == 0 && variable.upper == 1; + if (variable.type != VariableType::Integer && variable.type != VariableType::Binary && + variable.type != VariableType::SemiInteger) + throw Unsupported("Native bridge supports Integer, Binary and SemiInteger variables only"); + if (result.variables.size() >= static_cast(Int::Limits::max)) + throw Unsupported("Too many variables for a native IntVarArray"); + column[variable.variable.id] = static_cast(result.variables.size()); + result.variables.push_back({static_cast(variable.variable.id), + integer(variable.lower, "Variable lower bound"), + integer(variable.upper, "Variable upper bound"), + variable.type == VariableType::SemiInteger}); + } + const auto terms = [&](const std::vector& original, std::int64_t limit, + int* minimum = nullptr, int* maximum = nullptr) { + std::vector converted; + std::int64_t magnitude = 0, lower = 0, upper = 0; + for (const auto& term : original) { + checkpoint(budget); + const int coefficient = integer(term.coefficient, "Linear coefficient"); + const int index = column[term.variable.id]; + const auto& variable = result.variables[static_cast(index)]; + const std::int64_t a = static_cast(coefficient) * (variable.semi ? 0 : variable.lower); + const std::int64_t b = static_cast(coefficient) * variable.upper; + const auto absolute = std::max(std::abs(a), std::abs(b)); + if (absolute > limit - magnitude) + throw Unsupported("Native linear activity exceeds conservative exact propagation limits; tighten bounds or rescale with exact integers"); + magnitude += absolute; + lower += std::min(a, b); upper += std::max(a, b); + converted.push_back({index, coefficient}); + } + if (minimum) *minimum = static_cast(lower); + if (maximum) *maximum = static_cast(upper); + return converted; + }; + const auto row = [&](const std::vector& original, double lower, double upper) { + return NativeRow{terms(original, Int::Limits::max), finite_bound(lower), finite_bound(upper)}; + }; + for (const auto& original : model.rows) { + checkpoint(budget); + if (!original.active || original.indicator_origin) continue; + result.rows.push_back(row(original.terms, original.lower, original.upper)); + } + for (const auto& original : model.indicators) { + checkpoint(budget); + if (!original.active) continue; + NativeIndicator indicator{column[original.activator.id], original.active_value, {}, + row(original.terms, original.lower, original.upper)}; + if (original.inactive_gate) indicator.gate = column[original.inactive_gate->id]; + result.indicators.push_back(std::move(indicator)); + } + // The cost equality includes a cost variable in addition to its expression. + // Reserving half the native range bounds that complete equality's activity. + result.objective = terms(model.objective.terms, Int::Limits::max / 2, + &result.cost_lower, &result.cost_upper); + if (model.objective.offset != std::trunc(model.objective.offset) || + std::abs(model.objective.offset) > static_cast(exact_double_integer - + std::max(std::abs(result.cost_lower), std::abs(result.cost_upper)))) + throw Unsupported("Native objective offset and all attainable objective values must be exact double integers (magnitude <= 2^53)"); + result.offset = static_cast(model.objective.offset); + result.minimize = model.objective.sense == ObjectiveSense::Minimize; + const auto native_constant=[](std::int64_t value) { + if(valueInt::Limits::max) + throw Unsupported("Global integer constant exceeds native Gecode limits"); + }; + for(const auto& global:model.globals) if(global.active) { + checkpoint(budget); + if(Detail::global_variables(global.payload).size()>static_cast(Int::Limits::max)) + throw Unsupported("Too many global arguments for native arrays"); + std::visit([&](const auto& data) { + using T=std::decay_t; + if constexpr(std::is_same_v || std::is_same_v) { + const auto count=[&]() {if constexpr(std::is_same_v) return data.elements.size();else return data.successors.size();}(); + native_constant(data.index_base); + if(count) native_constant(data.index_base+static_cast(count-1)); + } else if constexpr(std::is_same_v) { + if(data.tuples.size()>static_cast(Int::Limits::max)) throw Unsupported("Too many native table tuples"); + for(const auto& tuple:data.tuples) {checkpoint(budget);for(const auto value:tuple) native_constant(value);} + } else if constexpr(std::is_same_v) { + native_constant(data.capacity); + constexpr auto limit=std::numeric_limits::max(); + std::int64_t widths=0,energy=0,count=0,time_magnitude=0; + std::int64_t min_height=Int::Limits::max; + bool overloaded_task=false; + for(std::size_t i=0;i(variable.upper)+data.durations[i]; + native_constant(end); + time_magnitude=std::max({time_magnitude,std::abs(start),std::abs(end)}); + min_height=std::min(min_height,data.heights[i]); + overloaded_task=overloaded_task || data.heights[i]>data.capacity; + const std::int64_t width=static_cast(variable.upper)-start+1; + const auto task_energy=data.durations[i]*data.heights[i]; // factors <= native int max + if(width>limit-widths || task_energy>limit-energy) + throw Unsupported("Native cumulative arithmetic exceeds exact int64 accumulation"); + widths+=width;energy+=task_energy;++count; + } + // Zero/singleton task sets and an individually overloaded mandatory + // task are resolved directly when posting; they need no resource tree. + if(count<=1 || overloaded_task) return; + if(data.capacity && (widths>limit/data.capacity || + data.capacity*widths>limit/count)) + throw Unsupported("Native cumulative propagation energy range exceeds int64"); + // cumulative/edge-finding.hpp indexes an int update[count*unique_heights] + // array; count^2 is a conservative bound and also protects task-tree + // node/event counts and next-power-of-two arithmetic. + if(count>Int::Limits::max/count) + throw Unsupported("Native cumulative task-count product exceeds integer array limits"); + // In either time direction all task endpoints have magnitude <= B. + // Omega/Lambda subtree energies are <= E, and their envelopes are + // bounded by C*B+E. Edge finding subtracts (C-height)*lct, so its + // numerator has magnitude <= 2*C*B+E. It then divides by height and + // casts to int. Require 2*C*B+E <= native_max*min_height, protecting + // that cast as well as every int64 envelope/difference intermediate. + // This also implies 2*B+sum(duration)<=native_max, bounding unary + // tree arithmetic when all task pairs must run disjointly. + // Products here fit int64: every factor is within native int limits. + const std::int64_t time_product=data.capacity*time_magnitude; + const std::int64_t difference_limit=static_cast(Int::Limits::max)*min_height; + if(energy>difference_limit || time_product>(difference_limit-energy)/2) + throw Unsupported("Native cumulative envelope/difference exceeds exact propagation limits; reduce the time range, durations or resource scale"); + } else if constexpr(std::is_same_v) { + result.regulars.emplace(&global.payload,prepare_regular(data,budget)); + } + },global.payload); + result.globals.push_back(&global.payload); + } + result.columns=std::move(column); + return result; +} + +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED +LP::BoundedIntegerModel relaxation_model(const Compiled& compiled, + const SolveBudget& budget) { + LP::BoundedIntegerModel result; + result.linear.c.resize(compiled.variables.size(), 0); + for (const auto& variable : compiled.variables) { + checkpoint(budget); + result.lower.push_back(variable.semi ? 0 : variable.lower); + result.upper.push_back(variable.upper); + } + for (const auto& term : compiled.objective) { + checkpoint(budget); + result.linear.c[term.column] = compiled.minimize ? term.coefficient : -term.coefficient; + } + const auto side = [&](const NativeRow& row, int bound, int sign) { + checkpoint(budget); + for (const auto& term : row.terms) { + checkpoint(budget); + result.linear.column.push_back(static_cast(term.column)); + result.linear.a.push_back(static_cast(sign) * term.coefficient); + } + result.linear.b.push_back(static_cast(sign) * bound); + result.linear.row_start.push_back(result.linear.a.size()); + }; + for (const auto& row : compiled.rows) { + if (row.lower) side(row, *row.lower, 1); + if (row.upper) side(row, *row.upper, -1); + } + try { LP::validate_integer_model(result); } + catch (const std::invalid_argument& error) { + throw Unsupported(std::string("Native LP relaxation: ") + error.what()); + } + return result; +} + +struct RelaxationState { + std::shared_ptr backend; + std::shared_ptr root_cover_evidence; + LP::Stats handoff; + NativeRootCoverStatistics root; +}; + +void root_cut_event(const char* event, NativeRootCoverCompletion& completion) { +#ifdef GECODE_NATIVE_ROOT_CUT_TEST_HOOKS + native_root_cut_test_event(event, completion); +#else + (void)event; (void)completion; +#endif +} + +NativeRootCoverCompletion root_completion(LP::Cuts::RootLoopCompletion value) { + using From=LP::Cuts::RootLoopCompletion;using To=NativeRootCoverCompletion; + switch(value) { + case From::NoNewCuts:return To::NoNewCuts; + case From::RoundLimit:return To::RoundLimit; + case From::WorkLimit:return To::WorkLimit; + case From::StorageLimit:return To::StorageLimit; + case From::SeparationLimit:return To::SeparationLimit; + case From::Cancelled:return To::Cancelled; + case From::TimeLimit:return To::TimeLimit; + case From::NoPrimalSuggestion:return To::NoPrimalSuggestion; + case From::InvalidSuggestion:return To::InvalidSuggestion; + case From::CallbackError:return To::CallbackError; + case From::BackendError:return To::BackendError; + case From::AllocationFailure:return To::AllocationFailure; + } + throw std::runtime_error("Unknown root cover loop completion"); +} + +void prepare_relaxation(Compiled& compiled,const NativeLpSettings& settings, + const SolveBudget& budget,RelaxationState& state) { + auto original=relaxation_model(compiled,budget); + if (settings.root_cover_cuts) { + const auto& limits=*settings.root_cover_cuts; + LP::Cuts::RootLoopOptions options; + options.max_rounds=limits.max_rounds;options.max_work=limits.max_work; + options.pool.max_cuts=limits.max_cuts;options.pool.max_nonzeros=limits.max_cut_nonzeros; + options.max_columns=limits.max_model_columns;options.max_rows=limits.max_model_rows; + options.max_nonzeros=limits.max_model_nonzeros; + options.separation.max_rows=limits.max_separation_rows; + options.separation.max_terms_per_row=limits.max_terms_per_row; + options.denominator=limits.denominator; + options.stop_requested=[&budget](){return budget.expired();}; + using Clock=std::chrono::steady_clock; + const auto now=Clock::now();const double remaining=budget.remaining_seconds(); + // Very large finite/infinite solve budgets need no representable absolute + // deadline: the shared predicate still checks their original budget. + const auto available=std::chrono::duration(Clock::time_point::max()-now).count(); + if (std::isfinite(remaining) && static_cast(remaining)(std::chrono::duration(remaining)); + root_cut_event("before_loop",state.root.completion);checkpoint(budget); + auto loop=LP::Cuts::root_cover_loop(LP::Cuts::SourceModel(std::move(original)),options); + const auto& stats=loop.stats; + state.root.completion=root_completion(loop.completion); + if (state.root.completion==NativeRootCoverCompletion::Cancelled && + !budget.cancelled() && budget.time_limit_reached()) + state.root.completion=NativeRootCoverCompletion::TimeLimit; + state.root.rounds=stats.rounds;state.root.augmentations=stats.augmentations; + state.root.cuts=loop.cuts.size();state.root.work=stats.work; + for (const auto& cut:loop.cuts) state.root.nonzeros+=cut.inequality().column.size(); + state.root.projected_coordinates=stats.projected_coordinates; + state.root.unsupported_rows=stats.unsupported_rows;state.root.oversized_rows=stats.oversized_rows; + state.root.separated_cuts=stats.separated_cuts;state.root.duplicate_cuts=stats.duplicate_cuts; + state.root.arithmetic_rejections=stats.arithmetic_rejections; + state.root.lp_calls=stats.lp_calls;state.root.lp_seconds=stats.lp_seconds; + state.root.valid_bounds=stats.valid_bounds;state.root.rejected_bounds=stats.rejected_bounds; + state.root.numerical_infeasibility_reports=stats.floating_infeasible_reports; + state.backend=loop.backend; + if (state.backend) state.handoff=state.backend->statistics(); + state.root_cover_evidence=std::make_shared(std::move(loop)); + root_cut_event("after_loop",state.root.completion); + checkpoint(budget); // Recover the precise outer time/cancel/node reason. + switch(state.root.completion) { + case NativeRootCoverCompletion::Cancelled:throw Interrupted{Termination::Cancelled}; + case NativeRootCoverCompletion::TimeLimit:throw Interrupted{Termination::TimeLimit}; + case NativeRootCoverCompletion::AllocationFailure:throw std::bad_alloc(); + case NativeRootCoverCompletion::CallbackError: + case NativeRootCoverCompletion::BackendError: + throw std::runtime_error("Native root cover loop failed before native search"); + case NativeRootCoverCompletion::NoNewCuts: + case NativeRootCoverCompletion::RoundLimit: + case NativeRootCoverCompletion::WorkLimit: + case NativeRootCoverCompletion::StorageLimit: + case NativeRootCoverCompletion::SeparationLimit: + case NativeRootCoverCompletion::NoPrimalSuggestion: + case NativeRootCoverCompletion::InvalidSuggestion:break; + default:throw std::runtime_error("Native root cover loop returned no completion"); + } + // Root cut limits bound this optional preprocessing only. If no backend was + // constructed, ordinary checked native LP solving can still proceed. + if (!state.backend) { + state.backend=std::make_shared(state.root_cover_evidence->model.model()); + state.handoff=state.backend->statistics(); + } + } else { + state.backend=std::make_shared(std::move(original)); + } + compiled.relaxation=state.backend; + compiled.root_cover_evidence=state.root_cover_evidence; + compiled.relaxation_options.frequency=settings.frequency==NativeLpFrequency::Root + ? LP::Frequency::Root:LP::Frequency::EveryNode; + compiled.relaxation_options.bound_tightening=settings.bound_tightening; + compiled.relaxation_options.bound_change_interval=settings.bound_change_interval; + root_cut_event("prepared",state.root.completion);checkpoint(budget); +} + +NativeLpStatistics relaxation_statistics(const RelaxationState& state) { + NativeLpStatistics result;result.root_cover=state.root; + const auto after=state.backend?state.backend->statistics():LP::Stats{}; + const auto& before=state.handoff; + result.lp_calls=state.root.lp_calls+(after.lp_calls-before.lp_calls); + result.lp_seconds=state.root.lp_seconds+(after.lp_ms-before.lp_ms)/1000.0; + result.valid_bounds=state.root.valid_bounds+(after.valid_bounds-before.valid_bounds); + result.rejected_bounds=state.root.rejected_bounds+(after.rejected-before.rejected); + result.numerical_infeasibility_reports=state.root.numerical_infeasibility_reports+ + (after.infeasible_status-before.infeasible_status); + result.certificate_evaluations=after.certificate_evaluations-before.certificate_evaluations; + result.conditional_checks=after.conditional_checks-before.conditional_checks; + result.variable_fixings=after.variable_fixings-before.variable_fixings; + result.variable_bound_tightenings=after.variable_bound_tightenings-before.variable_bound_tightenings; + return result; +} +#endif + +struct KnapsackBound { + int objective; + std::shared_ptr> witness; +}; + +struct KnapsackData { + std::size_t capacity; + std::vector weights, costs; +}; + +// Shared exact admission for both automatic routing and DP construction. +std::optional prepare_knapsack(const Compiled& model, + const SolveBudget& budget) { + const auto n = model.variables.size(); + if (!model.binary_domains || !n || model.rows.size() != 1 || + !model.indicators.empty() || !model.globals.empty()) return {}; + const auto& row = model.rows.front(); + if (row.terms.size() != n) return {}; + const bool positive = row.terms.front().coefficient > 0; + if (positive ? (!row.upper || (row.lower && *row.lower > 0)) + : (!row.lower || (row.upper && *row.upper < 0))) return {}; + const std::int64_t capacity = positive ? *row.upper : -std::int64_t(*row.lower); + constexpr std::size_t max_transitions = 32000000; + constexpr std::size_t max_payload_bytes = 8 * 1024 * 1024; + if (capacity < 0 || capacity > 65536) return {}; + const auto width = static_cast(capacity) + 1; + // Admit work and storage separately. Division protects every later product; + // packed decisions need one bit per recurrence, not an int64 value per cell. + if (n > max_transitions / width) return {}; + const auto transitions = n * width; + const auto decision_words = transitions / 64 + (transitions % 64 != 0); + std::size_t available = max_payload_bytes; + const auto account = [&](std::size_t count, std::size_t bytes) { + if (count > available / bytes) return false; + available -= count * bytes; + return true; + }; + if (!account(width, sizeof(std::int64_t)) || + !account(width, sizeof(std::int64_t)) || + !account(decision_words, sizeof(std::uint64_t)) || + !account(n, sizeof(std::int64_t)) || // weights + !account(n, sizeof(std::int64_t)) || // normalized costs + !account(n, sizeof(unsigned char))) return {}; // reconstructed witness + checkpoint(budget); + std::vector weights(n, 0), costs(n, 0); + for (const auto& term : row.terms) { + checkpoint(budget); + const auto weight = positive ? std::int64_t(term.coefficient) + : -std::int64_t(term.coefficient); + if (weight <= 0 || term.column < 0 || static_cast(term.column) >= n || + weights[static_cast(term.column)] != 0) return {}; + weights[static_cast(term.column)] = weight; + } + bool improves_zero = false; + for (const auto& term : model.objective) { + checkpoint(budget); + costs[static_cast(term.column)] = model.minimize + ? std::int64_t(term.coefficient) : -std::int64_t(term.coefficient); + improves_zero = improves_zero || costs[static_cast(term.column)] < 0; + } + // Otherwise all-zero is already optimal and the ordinary minimum-first + // brancher and original objective box provide the same preference and bound. + if (!improves_zero) return {}; + return KnapsackData{static_cast(capacity),std::move(weights),std::move(costs)}; +} + +// Exact root strengthening for one bounded binary capacity row. A missing +// result means the original native search should run without this optional work. +std::optional knapsack_bound(const Compiled& model, + const SolveBudget& budget) { + const auto started = std::chrono::steady_clock::now(); + const auto within_budget = [&] { + checkpoint(budget); + // This optional preprocessing may stop without producing any evidence. + // Original search keeps the remainder of the unchanged shared solve budget. + return std::chrono::steady_clock::now() - started < std::chrono::milliseconds(250); + }; + const auto admitted = prepare_knapsack(model,budget); + if (!admitted || !within_budget()) return {}; + const auto n = model.variables.size(), capacity = admitted->capacity, width = capacity+1; + const auto& weights = admitted->weights; + const auto& costs = admitted->costs; + // previous[c] minimizes normalized cost using the preceding items and at most + // c capacity. Empty selection is feasible, so the base row is zero everywhere. + // Each entry is a feasible subset cost. The compiler's absolute objective + // activity bound protects every partial sum, including signed costs. + std::vector previous(width, 0), current(width, 0); + const auto transitions = n * width; // checked by shared admission + std::vector take(transitions / 64 + (transitions % 64 != 0), 0); + if (!within_budget()) return {}; + for (std::size_t i = 0; i < n; ++i) { + const auto row_start = i * width; + for (std::size_t c = 0; c < width; ++c) { + if ((c & 255) == 0 && !within_budget()) return {}; + auto value = previous[c]; + if (weights[i] <= static_cast(c)) { + const auto included = costs[i] + previous[c - static_cast(weights[i])]; + if (included < value) { + value = included; + const auto bit = row_start + c; + take[bit / 64] |= std::uint64_t{1} << (bit % 64); + } + } + current[c] = value; + } + previous.swap(current); +#ifdef GECODE_NATIVE_SEARCH_TEST_HOOKS + native_search_test_event("knapsack_row_completed"); +#endif + if (!within_budget()) return {}; + } + const auto normalized = previous[capacity]; + auto witness = std::make_shared>(n, 0); + auto remaining = static_cast(capacity); + for (std::size_t i = n; i > 0; --i) { + if (!within_budget()) return {}; + // Each bit belongs to its original item row, so overwritten rolling values + // cannot corrupt reconstruction. A tie consistently excludes the item. + const auto bit = (i - 1) * width + remaining; + if ((take[bit / 64] >> (bit % 64)) & std::uint64_t{1}) { + if (weights[i - 1] > static_cast(remaining)) + throw std::runtime_error("Native knapsack reconstruction exceeds capacity"); + (*witness)[i - 1] = 1; + remaining -= static_cast(weights[i - 1]); + } + } + std::int64_t load = 0, checked_cost = 0; + for (std::size_t i = 0; i < n; ++i) { + if (!within_budget()) return {}; + if ((*witness)[i]) { load += weights[i]; checked_cost += costs[i]; } + } + const auto raw = model.minimize ? normalized : -normalized; + if (load > capacity || checked_cost != normalized || + raw < model.cost_lower || raw > model.cost_upper) + throw std::runtime_error("Native knapsack witness failed exact reconstruction checks"); + if (!within_budget()) return {}; + return KnapsackBound{static_cast(raw), std::move(witness)}; +} + +class NativeSpace final : public Space { +public: + IntVarArray variables; + IntVar cost; + bool minimize; + std::shared_ptr> knapsack_witness; + + NativeSpace(const Compiled& model, const SolveBudget& budget, + NeighborhoodBudget* local=nullptr, bool root_knapsack=true, + bool depth_first_knapsack=false) + : variables(*this, static_cast(model.variables.size())), + cost(*this, model.cost_lower, model.cost_upper), minimize(model.minimize) { + for (std::size_t i = 0; i < model.variables.size(); ++i) { + construction_checkpoint(budget,local); + const auto& variable = model.variables[i]; + if (variable.semi) { + const int ranges[2][2] = {{0, 0}, {variable.lower, variable.upper}}; + variables[static_cast(i)] = IntVar(*this, IntSet(ranges, 2)); + } else { + variables[static_cast(i)] = IntVar(*this, variable.lower, variable.upper); + } + } + for (const auto& row : model.rows) { + construction_checkpoint(budget,local); post(row); + if(local) local->check("neighborhood_row_posted"); + } + for (const auto& indicator : model.indicators) { + construction_checkpoint(budget,local); + BoolVar enabled(*this, 0, 1); + rel(*this, variables[indicator.activator], IRT_EQ, + indicator.active_value ? 1 : 0, Reify(enabled, RM_EQV)); + post(indicator.row, enabled); + if (indicator.gate) { + IntArgs coefficients(2); IntVarArgs vars(2); + coefficients[0] = indicator.active_value ? 1 : -1; + coefficients[1] = 1; + vars[0] = variables[indicator.activator]; vars[1] = variables[*indicator.gate]; + linear(*this, coefficients, vars, IRT_EQ, indicator.active_value ? 1 : 0); + } + } + for(const auto* global:model.globals) { + construction_checkpoint(budget,local);post_global(*global,model,budget,local); + if(local) local->check("neighborhood_global_posted"); + } + IntArgs coefficients(static_cast(model.objective.size())); + IntVarArgs vars(static_cast(model.objective.size())); + for (std::size_t i = 0; i < model.objective.size(); ++i) { + if(local) local->check(); + coefficients[static_cast(i)] = model.objective[i].coefficient; + vars[static_cast(i)] = variables[model.objective[i].column]; + } + linear(*this, coefficients, vars, IRT_EQ, cost); +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + if (model.relaxation && !local) { + checkpoint(budget); + IntVar lp_cost = cost; + if (!minimize) { + lp_cost = IntVar(*this, -model.cost_upper, -model.cost_lower); + IntVarArgs pair(2); pair[0] = cost; pair[1] = lp_cost; + linear(*this, pair, IRT_EQ, 0); + } + LP::integer_linear_minimize(*this, variables, lp_cost, + model.relaxation, model.relaxation_options); + } +#endif + bool strengthen = root_knapsack && !local; +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + strengthen = strengthen && !model.relaxation; +#endif + const auto knapsack = strengthen ? knapsack_bound(model, budget) + : std::optional{}; + if (knapsack) { + checkpoint(budget); + knapsack_witness = knapsack->witness; + // A valid objective-side bound preserves every original feasible point. + // The witness only orders choices; it never fixes the original model. + rel(*this, cost, minimize ? IRT_GQ : IRT_LQ, knapsack->objective); + branch(*this, variables, INT_VAR_SIZE_MIN(), + INT_VAL([witness=knapsack->witness,depth_first_knapsack] + (const Space&, IntVar variable, int column) { + const int bit = (*witness)[static_cast(column)]; + const int value = depth_first_knapsack ? 1 - bit : bit; + return variable.in(value) ? value : variable.min(); + })); + } else { + branch(*this, variables, INT_VAR_SIZE_MIN(), INT_VAL_MIN()); + } + if(local) local->check("neighborhood_constructed"); + } + NativeSpace(NativeSpace& other) + : Space(other), minimize(other.minimize), knapsack_witness(other.knapsack_witness) { + variables.update(*this, other.variables); cost.update(*this, other.cost); + } + Space* copy() override { return new NativeSpace(*this); } + void constrain(const Space& best) override { + const auto& incumbent = static_cast(best); + rel(*this, cost, minimize ? IRT_LE : IRT_GR, incumbent.cost.val()); + } +private: + void post_global(const GlobalPayload& payload,const Compiled& model,const SolveBudget& budget, + NeighborhoodBudget* local=nullptr) { + const auto variable=[&](Variable handle) {return variables[model.columns[handle.id]];}; + const auto args=[&](const std::vector& handles) { + IntVarArgs out(static_cast(handles.size())); + for(std::size_t i=0;icheck();out[static_cast(i)]=variable(handles[i]); + } + // Some propagators reject repeated variable objects. Equal fresh views + // preserve aliases without weakening their mathematical meaning. + unshare(*this,out); + return out; + }; + const auto false_constraint=[&]() {rel(*this,IntVar(*this,0,0),IRT_EQ,1);}; + const auto position=[&](Variable handle,std::int64_t base,std::size_t size) { + IntVar index(*this,0,static_cast(size-1)); + IntVarArgs vars(2);vars[0]=variable(handle);vars[1]=index; + IntArgs coefficients(2);coefficients[0]=1;coefficients[1]=-1; + linear(*this,coefficients,vars,IRT_EQ,static_cast(base)); + return index; + }; + std::visit([&](const auto& data) { + using T=std::decay_t; + if constexpr(std::is_same_v) { + if(data.variables.size()>1) distinct(*this,args(data.variables),IPL_DOM); + } else if constexpr(std::is_same_v) { + if(data.elements.empty()) {false_constraint();return;} + element(*this,args(data.elements),position(data.index,data.index_base,data.elements.size()),variable(data.result),IPL_DOM); + } else if constexpr(std::is_same_v) { + if(data.tuples.empty()) {false_constraint();return;} + if(data.variables.empty()) return; // At least one empty tuple is true. + TupleSet tuples(static_cast(data.variables.size())); + for(const auto& tuple:data.tuples) { + construction_checkpoint(budget,local);IntArgs entry(static_cast(tuple.size())); + for(std::size_t i=0;i(i)]=static_cast(tuple[i]); + tuples.add(entry); + } + tuples.finalize();extensional(*this,args(data.variables),tuples,IPL_DOM); + } else if constexpr(std::is_same_v) { + std::vector starts;std::vector durations,heights; + for(std::size_t i=0;icheck(); + starts.push_back(data.starts[i]);durations.push_back(static_cast(data.durations[i])); + heights.push_back(static_cast(data.heights[i])); + } + // Mandatory positive-duration demand above capacity is independently + // infeasible. A singleton within capacity imposes no restriction on + // its start. Handle both before native cumulative's min-capacity + // calculation (whose second minimum uses an INT_MAX sentinel). + if(std::any_of(heights.begin(),heights.end(),[&](int h){return h>data.capacity;})) { + false_constraint();return; + } + if(starts.size()<=1) return; + IntArgs p(static_cast(starts.size())),h(static_cast(starts.size())); + for(std::size_t i=0;i(i)]=durations[i];h[static_cast(i)]=heights[i];} + // Select the same disjunctive case with wide addition. Native + // cumulative's int min_height+second_height can overflow for large + // resource units, even though both individual heights are supported. + std::int64_t first=Int::Limits::max,second=Int::Limits::max; + for(const auto height:heights) { + if(height(height)); + } + const auto level=static_cast(IPL_BASIC|IPL_ADVANCED); + if(first+second>data.capacity) unary(*this,args(starts),p,level); + else cumulative(*this,static_cast(data.capacity),args(starts),p,h,level); + } else if constexpr(std::is_same_v) { + const auto& prepared=model.regulars.at(&payload); + if(data.variables.empty()){if(!prepared.accepts_empty)false_constraint();return;} + if(data.transitions.empty()||data.final_states.empty()){false_constraint();return;} + // DFA's constructor may normalize the supplied arrays. Keep each copy + // private, disable optional minimization, and checkpoint both sides of + // the non-interruptible native construction/posting calls. + auto transitions=prepared.transitions;auto finals=prepared.finals; + construction_checkpoint(budget,local);DFA automaton(prepared.initial,transitions.data(),finals.data(),false);construction_checkpoint(budget,local); + extensional(*this,args(data.variables),automaton,IPL_DOM);construction_checkpoint(budget,local); + } else { + IntVarArgs successors(static_cast(data.successors.size())); + for(std::size_t i=0;icheck(); + successors[static_cast(i)]=position(data.successors[i],data.index_base,data.successors.size()); + } + circuit(*this,successors,IPL_DOM); + } + },payload); + } + void post(const NativeRow& row, std::optional enabled = {}) { + IntArgs coefficients(static_cast(row.terms.size())); + IntVarArgs vars(static_cast(row.terms.size())); + for (std::size_t i = 0; i < row.terms.size(); ++i) { + coefficients[static_cast(i)] = row.terms[i].coefficient; + vars[static_cast(i)] = variables[row.terms[i].column]; + } + const auto side = [&](IntRelType relation, int bound) { + if (enabled) linear(*this, coefficients, vars, relation, bound, Reify(*enabled, RM_IMP)); + else linear(*this, coefficients, vars, relation, bound); + }; + if (row.lower && row.upper && *row.lower == *row.upper) side(IRT_EQ, *row.lower); + else { + if (row.lower) side(IRT_GQ, *row.lower); + if (row.upper) side(IRT_LQ, *row.upper); + } + } +}; + +// Only assignment infeasibility has this type. Allocation/indexing/internal +// errors must never be relabelled as an invalid user start. +class ExactWitnessFailure : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; +void start_event(const char* event) { +#ifdef GECODE_NATIVE_START_TEST_HOOKS + native_start_test_event(event); +#else + (void)event; +#endif +} +void start_checkpoint(const SolveBudget& budget, const char* event) { + start_event(event); checkpoint(budget); +} +bool exact_domain(const VariableData& variable, double value) { + return std::isfinite(value) && value == std::trunc(value) && + ((variable.type == VariableType::SemiInteger && value == 0) || + (value >= variable.lower && value <= variable.upper)); +} + +// This checker reads the original snapshot and original slots, independently +// of NativeSpace's column mapping, propagation and cost variable. All casts and +// sums are justified by compile's integer and absolute-activity preconditions. +// Start-only checkpoints leave ordinary candidate/no-start scheduling unchanged. +std::int64_t check_exact(const ModelSnapshot& model, const std::vector& values, + const SolveBudget* start_budget = nullptr, NeighborhoodBudget* local=nullptr) { + const auto check = [&](const char* event) { + if (start_budget) start_checkpoint(*start_budget, event); + if(local) local->check(event); + }; + for (const auto& variable : model.variables) { + check("exact_variable"); + if (!variable.active) continue; + const double value = values.at(static_cast(variable.variable.id)); + if (!exact_domain(variable, value)) + throw ExactWitnessFailure("Native witness failed exact original variable check"); + } + const auto activity = [&](const std::vector& terms) { + std::int64_t sum = 0; + for (const auto& term : terms) { + check("exact_term"); + sum += static_cast(term.coefficient) * + static_cast(values.at(static_cast(term.variable.id))); + } + return sum; + }; + const auto row = [&](const std::vector& terms, double lower, double upper) { + const auto sum = activity(terms); + if ((std::isfinite(lower) && sum < static_cast(lower)) || + (std::isfinite(upper) && sum > static_cast(upper))) + throw ExactWitnessFailure("Native witness failed exact original row check"); + }; + for (const auto& original : model.rows) { + check("exact_row"); + if (original.active && !original.indicator_origin) + row(original.terms, original.lower, original.upper); + } + for (const auto& indicator : model.indicators) { + check("exact_indicator"); + if (!indicator.active) continue; + const bool enabled = values[indicator.activator.id] == (indicator.active_value ? 1 : 0); + if (enabled) row(indicator.terms, indicator.lower, indicator.upper); + if (indicator.inactive_gate && values[indicator.inactive_gate->id] != (enabled ? 0 : 1)) + throw ExactWitnessFailure("Native witness failed exact indicator gate check"); + } + for (const auto& global : model.globals) if (global.active) { + check("exact_global"); + std::string reason; + if (!Detail::global_satisfied(global.payload, values, 0, reason)) + throw ExactWitnessFailure("Native witness failed exact global check: " + reason); + check("after_exact_global"); + } + check("exact_objective"); + return static_cast(model.objective.offset) + activity(model.objective.terms); +} + +struct PreparedStart { + std::vector values; + std::vector active; + std::int64_t objective = 0; + int cost = 0; +}; +PreparedStart prepare_start(const ModelSnapshot& model, const Compiled& compiled, + const SolveOptions& options, const SolveBudget& budget) { + start_checkpoint(budget, "before_start"); + PreparedStart result; + result.values.assign(model.variables.size(), std::numeric_limits::quiet_NaN()); + result.active.reserve(model.variables.size()); + std::vector known; + known.reserve(model.variables.size()); + for (const auto& entry : options.primal_start) { + start_checkpoint(budget, "map_entry"); + const auto handle = entry.variable; + if (handle.model_id != model.model_id || handle.id >= model.variables.size() || + !model.variables[handle.id].active) + throw ModelError("Native primal start contains a foreign, invalid or deleted variable"); + auto& value = result.values[handle.id]; + if (!std::isnan(value)) throw ModelError("Native primal start contains a duplicate variable"); + if (!exact_domain(model.variables[handle.id], entry.value)) + throw ModelError("Native primal start violates an exact integer variable domain (no rounding is performed)"); + value = entry.value; known.push_back(static_cast(handle.id)); + } + // Only live original indicator equations determine omitted helper slots. + // A slot is queued once, and each dependency is visited once, including + // chains/reordered metadata. Unseeded cycles and removed gates stay unknown. + std::vector> dependents(model.variables.size()); + for (std::size_t i = 0; i < model.indicators.size(); ++i) { + start_checkpoint(budget, "gate_dependency"); + const auto& indicator = model.indicators[i]; + if (indicator.active && indicator.inactive_gate) + dependents[indicator.activator.id].push_back(i); + } + for (std::size_t i = 0; i < known.size(); ++i) { + start_checkpoint(budget, "known_slot"); + for (auto index : dependents[known[i]]) { + start_checkpoint(budget, "derive_gate"); + const auto& indicator = model.indicators[index]; + const double gate = result.values[indicator.activator.id] == (indicator.active_value ? 1 : 0) ? 0 : 1; + auto& value = result.values[indicator.inactive_gate->id]; + if (std::isnan(value)) { + value = gate; known.push_back(static_cast(indicator.inactive_gate->id)); + } else if (value != gate) { + throw ModelError("Native primal start contradicts a live indicator inactivity gate"); + } + } + } + for (const auto& variable : model.variables) { + start_checkpoint(budget, "start_completeness"); + result.active.push_back(variable.active); + if (variable.active && std::isnan(result.values[variable.variable.id])) + throw Unsupported("Native primal start must determine every active variable; only live indicator inactivity gates are completed automatically"); + } + start_checkpoint(budget, "before_exact_start"); + try { result.objective = check_exact(model, result.values, &budget); } + catch (const ExactWitnessFailure& error) { + throw ModelError(std::string("Complete native primal start is infeasible: ") + error.what()); + } + start_checkpoint(budget, "after_exact_start"); + // compile bounds both quantities by +/-2^53, so this int64 subtraction is + // exact and cannot overflow. The native cost must fit its preflighted range. + const auto cost = result.objective - compiled.offset; + if (cost < compiled.cost_lower || cost > compiled.cost_upper) + throw std::runtime_error("Native primal start cost disagrees with compiled objective bounds"); + result.cost = static_cast(cost); + start_checkpoint(budget, "before_numerical_start"); + const auto checked = validate(model, result.values, options.feasibility_tolerance, options.integrality_tolerance); + if (!checked.valid || !checked.objective || *checked.objective != static_cast(result.objective)) + throw ModelError("Complete native primal start failed independent original-model numerical check: " + checked.message); + start_checkpoint(budget, "after_numerical_start"); + return result; +} +void publish_start(PreparedStart&& start, SolveResult& result, const SolveBudget& budget) { + start_checkpoint(budget, "before_start_publication"); + result.values = std::move(start.values); result.active_variables = std::move(start.active); + result.objective = static_cast(start.objective); + result.solution_validated = result.start_submitted = true; + // Callers install their scalar incumbent before the next hook/checkpoint. +} +void start_cutoff(NativeSpace& root, int cost, const SolveBudget& budget) { + start_checkpoint(budget, "before_start_cutoff"); + rel(root, root.cost, root.minimize ? IRT_LE : IRT_GR, cost); + start_checkpoint(budget, "after_start_cutoff"); +} + +// All live search and LP ownership is released before this cooperative gate. +// Return true for a semantic failure requiring a conservative bound fallback. +bool finish_start(SolveResult& result, const SolveBudget& budget) { + bool failure = false; + try { start_event("after_start_release"); } + catch (const std::bad_alloc&) { + result.termination = Termination::MemoryLimit; result.message = "Native start cleanup allocation failed"; + } catch (const MemoryExhausted& error) { + result.termination = Termination::MemoryLimit; result.message = error.what(); + } catch (const std::exception& error) { + result.termination = Termination::BackendError; result.message = error.what(); failure = true; + } + if (budget.cancelled() || budget.time_limit_reached()) { + result.termination = budget.cancelled() ? Termination::Cancelled : Termination::TimeLimit; + result.message = "Native start solve time/cancellation budget stopped before return"; + } + return failure; +} + +class BudgetStop final : public Search::Stop { + SolveBudget budget_; + std::uint64_t observed_ = 0; +public: + explicit BudgetStop(SolveBudget budget) : budget_(std::move(budget)) {} + void observe(const Search::Statistics& statistics) { + const auto nodes = static_cast(statistics.node); + if (nodes > observed_) budget_.add_nodes(nodes - observed_); + observed_ = std::max(observed_, nodes); + } + bool stop(const Search::Statistics& statistics, const Search::Options&) override { + observe(statistics); return budget_.expired(); + } +}; +#endif +} // namespace + +BackendCapabilities native_capabilities() { + BackendCapabilities result; + result.name = "Gecode native"; +#ifdef GECODE_OPTIMIZE_WITH_NATIVE + result.available = result.mixed_integer_linear = result.exact_solving = true; + result.version = GECODE_VERSION; +#else + result.limitations.push_back("Built without the native Gecode bridge"); +#endif + result.limitations.push_back("Finite Integer/Binary/SemiInteger linear subset; integral bounds, coefficients and objective offset"); + result.limitations.push_back("Conservative native integer activity limits and exact double objective range; no continuous variables"); + result.limitations.push_back("Native reified indicators and all-different/element/table/cumulative/circuit/regular globals; scoped FlatZinc compilation is available separately"); + result.limitations.push_back("One deterministic worker; complete exact starts with live indicator gate completion; no partial starts, certificates or interrupted global bounds"); + result.limitations.push_back("Time/cancellation is cooperative; native propagation and recomputation cannot be interrupted"); + return result; +} + +namespace { +SolveResult solve_native_impl(const ModelSnapshot& model, const SolveOptions& options, + const NativeLpOptions* lp_options, + NativeLpStatistics* lp_statistics, + const SolveBudget* inherited_budget = nullptr, + [[maybe_unused]] bool root_knapsack = true) { + const auto started = std::chrono::steady_clock::now(); + SolveResult result; + result.model_id = model.model_id; result.revision = model.revision; + result.backend = lp_options ? "Gecode native + checked LP" : "Gecode native"; +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + RelaxationState relaxation; +#else + (void) lp_statistics; +#endif + if (lp_statistics && lp_options && lp_options->root_cover_cuts) { + lp_statistics->root_cover.requested=true; + lp_statistics->root_cover.completion=NativeRootCoverCompletion::NotStarted; +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + relaxation.root=lp_statistics->root_cover; +#endif + } + std::optional shared_budget; + try { + if (inherited_budget) shared_budget.emplace(*inherited_budget); + else shared_budget.emplace(options); + [[maybe_unused]] const auto& budget = *shared_budget; + result.guarantee=options.guarantee; + if (lp_options) lp_options->validate(); + validate_structure(model); + if (options.backend != Backend::Auto && options.backend != Backend::Native) + throw Unsupported("Native solve requires backend Auto or Native"); + if (options.guarantee == Guarantee::Certified) + throw Unsupported("Native bridge does not produce independently checkable proof certificates"); + if (options.threads != 1 || options.random_seed != 0) + throw Unsupported("Native bridge currently uses one deterministic worker and seed 0"); +#ifndef GECODE_OPTIMIZE_WITH_NATIVE + throw Unsupported("Built without the native Gecode bridge"); +#else + result.backend_version = GECODE_VERSION; +#ifndef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + if (lp_options) + throw Unsupported("Native checked LP requires HiGHS and compiler support for checked 128-bit integer arithmetic"); +#endif + checkpoint(budget); + auto compiled = compile(model, budget); + std::optional start_cost; + if (!options.primal_start.empty()) { + auto start = prepare_start(model, compiled, options, budget); + start_cost = start.cost; + publish_start(std::move(start), result, budget); + start_checkpoint(budget, "after_start_publication"); + } +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + if (lp_options) { + prepare_relaxation(compiled,*lp_options,budget,relaxation); + result.backend_version += std::string("; HiGHS ") + Highs().version(); + checkpoint(budget); + } +#endif + result.guarantee = options.guarantee; + std::vector active; + for (const auto& variable : model.variables) active.push_back(variable.active); + BudgetStop stop(budget); + Search::Options search_options; + search_options.threads = 1; search_options.clone = true; search_options.stop = &stop; + if (start_cost) start_checkpoint(budget, "start_root_alloc"); + auto root = std::make_unique(compiled, budget, nullptr, + root_knapsack && !lp_options); + if (start_cost) start_cutoff(*root, *start_cost, budget); + checkpoint(budget); + BAB search(root.get(), search_options); + root.reset(); + if (start_cost) start_checkpoint(budget, "start_search_ready"); + for (;;) { + checkpoint(budget); + if (start_cost) start_checkpoint(budget, "before_start_search_next"); + std::unique_ptr candidate(search.next()); + stop.observe(search.statistics()); + checkpoint(budget); + if (!candidate) { + if (search.stopped()) throw std::runtime_error("Native search stopped without a shared-budget reason"); + result.termination = result.has_solution() ? Termination::Optimal : Termination::Infeasible; + if (result.has_solution()) { + result.best_bound = result.objective; result.update_gaps(model.objective.sense); + } + result.message = "Native finite integer search exhausted; original integer semantics preserved"; + break; + } + std::vector values(model.variables.size(), std::numeric_limits::quiet_NaN()); + for (std::size_t i = 0; i < compiled.variables.size(); ++i) + values[compiled.variables[i].slot] = candidate->variables[static_cast(i)].val(); + if (start_cost) start_checkpoint(budget, "before_start_candidate_validation"); + const auto exact_objective = check_exact(model, values); + if (exact_objective != compiled.offset + candidate->cost.val()) + throw std::runtime_error("Native cost does not match exact original objective"); + const auto checked = validate(model, values, options.feasibility_tolerance, options.integrality_tolerance); + if (!checked.valid || !checked.objective || *checked.objective != static_cast(exact_objective)) + throw std::runtime_error("Native witness failed independent original-model numerical check: " + checked.message); + if (result.objective && (compiled.minimize ? exact_objective >= *result.objective : exact_objective <= *result.objective)) + throw std::runtime_error("Native search returned a non-improving incumbent"); + auto mask = active; // Allocate before replacing an earlier validated start. + if (start_cost) start_checkpoint(budget, "before_start_candidate_publication"); + checkpoint(budget); + result.values = std::move(values); result.active_variables = std::move(mask); + result.objective = static_cast(exact_objective); result.solution_validated = true; + } +#endif + } catch (const Unsupported& e) { + result.termination = Termination::Unsupported; result.message = e.what(); + } catch (const Interrupted& e) { + result.termination = e.reason; result.message = "Native shared solve budget exhausted"; + } catch (const ModelError& e) { + result.termination = Termination::InvalidModel; result.message = e.what(); + } catch (const std::bad_alloc&) { + result.termination = Termination::MemoryLimit; result.message = "Native bridge allocation failed"; +#ifdef GECODE_OPTIMIZE_WITH_NATIVE + } catch (const MemoryExhausted& e) { + result.termination = Termination::MemoryLimit; result.message = e.what(); +#endif + } catch (const std::exception& e) { + result.termination = Termination::BackendError; result.message = e.what(); + } +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + if (lp_statistics) *lp_statistics=relaxation_statistics(relaxation); +#endif +#ifdef GECODE_OPTIMIZE_WITH_NATIVE + if (result.start_submitted && shared_budget) { +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + relaxation = RelaxationState{}; +#endif + (void) finish_start(result, *shared_budget); + if (result.termination != Termination::Optimal) { + result.best_bound.reset(); result.absolute_gap.reset(); result.relative_gap.reset(); + } + } +#endif + result.elapsed_seconds = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + return result; +} +} // namespace + +SolveResult solve_native(const ModelSnapshot& model, const SolveOptions& options) { + return solve_native_impl(model, options, nullptr, nullptr); +} + +void NativeRootCoverSettings::validate() const { + if (!denominator || denominator>1048576 || (denominator&(denominator-1))) + throw ModelError("Native root cover denominator must be a power of two in [1,1048576]"); +} + +void NativeLpSettings::validate() const { + if (!bound_change_interval || + (frequency != NativeLpFrequency::Root && frequency != NativeLpFrequency::AfterBoundChanges)) + throw ModelError("Invalid native LP frequency or bound-change interval"); + if (root_cover_cuts) root_cover_cuts->validate(); +} + +void NativeLpOptions::validate() const { + solve.validate(); NativeLpSettings::validate(); +} + +BackendCapabilities native_lp_capabilities() { + auto result = native_capabilities(); + result.name = "Gecode native + checked LP"; +#ifndef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + result.available = result.mixed_integer_linear = result.exact_solving = false; + result.limitations.push_back("Requires native Gecode, HiGHS and checked 128-bit compiler arithmetic"); +#else + result.version += std::string("; HiGHS ") + Highs().version(); +#endif + result.limitations.push_back("Explicit LP API or conservative automatic native selection; sparse ordinary linear rows with coefficients and bounds of magnitude <= 1e9"); + result.limitations.push_back("Global constraints and indicators are enforced natively and omitted from the LP relaxation"); + result.limitations.push_back("LP attempt limits are cooperative (0.2 seconds / 10000 simplex iterations); no interrupted frontier bound"); + result.limitations.push_back("Root covers are verified only against original ordinary rows and original LP boxes; explicit LP defaults omit them, automatic native selection may enable them"); + return result; +} + +NativeLpResult solve_native_lp(const ModelSnapshot& model, const NativeLpOptions& options) { + NativeLpResult result; + result.result = solve_native_impl(model, options.solve, &options, &result.relaxation); + return result; +} + +NativeLpResult solve_native_lp(const Model& model, const NativeLpOptions& options) { + const auto started = std::chrono::steady_clock::now(); + NativeLpResult result; + result.result.model_id = model.id(); result.result.revision = model.revision(); + result.result.backend = "Gecode native + checked LP"; + result.result.guarantee=options.solve.guarantee; + if(options.root_cover_cuts) { + result.relaxation.root_cover.requested=true; + result.relaxation.root_cover.completion=NativeRootCoverCompletion::NotStarted; + } + try { + options.validate(); + auto snapshot = model.snapshot(); + auto remaining = options; + if (std::isfinite(remaining.solve.time_limit_seconds)) + remaining.solve.time_limit_seconds = std::max(0.0, remaining.solve.time_limit_seconds - + std::chrono::duration(std::chrono::steady_clock::now() - started).count()); + result = solve_native_lp(snapshot, remaining); + } catch (const ModelError& error) { + result.result.termination = Termination::InvalidModel; result.result.message = error.what(); + } catch (const std::bad_alloc&) { + result.result.termination = Termination::MemoryLimit; + result.result.message = "Native LP model snapshot allocation failed"; + } + result.result.elapsed_seconds = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + return result; +} + +SolveResult solve_native(const Model& model, const SolveOptions& options) { + const auto started = std::chrono::steady_clock::now(); + SolveResult result; + result.model_id = model.id(); result.revision = model.revision(); result.backend = "Gecode native"; + try { + options.validate(); + auto snapshot = model.snapshot(); + auto remaining = options; + if (std::isfinite(remaining.time_limit_seconds)) + remaining.time_limit_seconds = std::max(0.0, remaining.time_limit_seconds - + std::chrono::duration(std::chrono::steady_clock::now() - started).count()); + result = solve_native(snapshot, remaining); + } catch (const ModelError& e) { + result.termination = Termination::InvalidModel; result.message = e.what(); + } catch (const std::bad_alloc&) { + result.termination = Termination::MemoryLimit; result.message = "Native model snapshot allocation failed"; + } + result.elapsed_seconds = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + return result; +} + +void NativeBranchingSettings::validate() const { + if (policy != NativeBranchingPolicy::BinaryReliability) + throw ModelError("Unknown native branching policy"); + if (!reliability_samples) throw ModelError("Native branching reliability_samples must be positive"); +} + +void NativeSearchOptions::validate() const { + solve.validate(); + if (order != NativeSearchOrder::DepthFirst && order != NativeSearchOrder::BestBound) + throw ModelError("Unknown native frontier search order"); + if (relaxation) relaxation->validate(); + if (branching) branching->validate(); +} + +namespace { +#ifdef GECODE_OPTIMIZE_WITH_NATIVE +void frontier_event(const char* event) { +#ifdef GECODE_NATIVE_SEARCH_TEST_HOOKS + native_search_test_event(event); +#else + (void)event; +#endif +} +// An admitted node may finish at its node quota. Only time/cancellation can +// interrupt that work; the complete budget is checked before the next admission. +void frontier_checkpoint(const SolveBudget& budget) { + if (budget.cancelled()) throw Interrupted{Termination::Cancelled}; + if (budget.time_limit_reached()) throw Interrupted{Termination::TimeLimit}; +} +struct FrontierNode { + std::unique_ptr space; + int bound; + std::uint64_t serial; + bool contains_knapsack_witness = false; +}; +struct FrontierCompare { + bool operator()(const FrontierNode& a, const FrontierNode& b) const noexcept { + if (a.bound != b.bound) return a.bound > b.bound; + if (a.contains_knapsack_witness != b.contains_knapsack_witness) + return !a.contains_knapsack_witness; + return a.serial > b.serial; + } +}; + +void branching_event(const char* event, std::size_t slot, double down=0, double up=0) { +#ifdef GECODE_NATIVE_BRANCHING_TEST_HOOKS + native_branching_test_event(event,slot,down,up); +#else + (void)event; (void)slot; (void)down; (void)up; +#endif +} +void count_branching(std::uint64_t& value, std::uint64_t amount=1) { + if (amount > std::numeric_limits::max()-value) + throw Interrupted{Termination::MemoryLimit}; + value+=amount; +} +// Ranking state belongs to the coordinator, never a Space/Choice/certificate. +class BinaryReliability { + struct History { + int column; + std::uint64_t pairs=0; + double down=0,up=0; + }; + struct Candidate { + int column; + std::size_t slot; + std::optional history; + }; + struct Observation { bool failed=false; double gain=0; }; + const NativeSearchOptions& options; + const ModelSnapshot& source; + const Compiled& compiled; + SolveBudget& budget; + NativeBranchingStatistics& stats; + std::vector history; + + bool charge(std::size_t amount=1) { + frontier_checkpoint(budget); + if (amount>options.branching->max_branching_work-stats.work) return false; + stats.work+=amount; return true; + } + static void split(NativeSpace& space,int column,unsigned int direction) { + // Both arms partition the actual domain; no relaxation hull is posted. + const auto variable=space.variables[column]; + if (variable.min()!=0 || variable.max()!=1 || variable.size()!=2 || direction>1) + throw std::runtime_error("Invalid native binary branching split"); + rel(space,variable,direction ? IRT_GQ:IRT_LQ,direction ? 1:0); + } + Observation probe(const NativeSpace& parent,const Candidate& candidate, + unsigned int direction,int baseline,NativeFrontierStatistics& frontier, + std::size_t resident) { + checkpoint(budget); + frontier.peak_open_nodes=std::max(frontier.peak_open_nodes,resident+1); + branching_event("probe_clone",candidate.slot); + frontier_checkpoint(budget); + std::unique_ptr child(static_cast(parent.clone())); + branching_event("probe_cloned",candidate.slot); + frontier_checkpoint(budget); + split(*child,candidate.column,direction); + branching_event("probe_posted",candidate.slot); + checkpoint(budget); + if (budget.nodes()==std::numeric_limits::max()) + throw Interrupted{Termination::MemoryLimit}; + count_branching(stats.probe_status_calls); budget.add_nodes(); + StatusStatistics observed; +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + const auto before=compiled.relaxation ? compiled.relaxation->statistics():LP::Stats{}; + const auto observe_lp=[&] { + const auto after=compiled.relaxation ? compiled.relaxation->statistics():LP::Stats{}; + count_branching(stats.probe_lp_calls,after.lp_calls-before.lp_calls); + stats.probe_lp_seconds+=(after.lp_ms-before.lp_ms)/1000.0; + }; +#endif + SpaceStatus status; + try { + branching_event("probe_before_status",candidate.slot); + frontier_checkpoint(budget); + status=child->status(observed); + } catch (...) { + count_branching(stats.probe_propagations,observed.propagate); +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + observe_lp(); +#endif + throw; + } + count_branching(stats.probe_propagations,observed.propagate); +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + observe_lp(); +#endif + branching_event("probe_after_status",candidate.slot); + frontier_checkpoint(budget); + Observation result; + if (status==SS_FAILED) result.failed=true; + else { + if (status!=SS_BRANCH && status!=SS_SOLVED) + throw std::runtime_error("Unexpected native branching probe status"); + const std::int64_t bound=compiled.minimize ? child->cost.min():-child->cost.max(); + result.gain=static_cast(std::max(0,bound-baseline)); + } + child.reset(); + branching_event("probe_destroyed",candidate.slot); + frontier_checkpoint(budget); + return result; + } +public: + BinaryReliability(const NativeSearchOptions& o,const ModelSnapshot& m, + const Compiled& c,SolveBudget& b,NativeBranchingStatistics& s) + :options(o),source(m),compiled(c),budget(b),stats(s) {} + + static void commit(NativeSpace& child,int column,unsigned int direction) { + split(child,column,direction); + } + + std::optional select(const NativeSpace& parent,int baseline, + NativeFrontierStatistics& frontier,std::size_t resident) { + count_branching(stats.decisions); + const auto& settings=*options.branching; + const auto fallback=[&]() -> std::optional { + branching_event("branch_fallback",std::numeric_limits::max()); + frontier_checkpoint(budget); count_branching(stats.fallback_decisions); return {}; + }; + if (!settings.max_candidates_per_decision || !settings.max_history_entries || + !settings.max_nonimproving_pairs || !settings.max_branching_work) + return fallback(); + branching_event("candidate_scan",std::numeric_limits::max()); + std::vector candidates; + const auto capacity=std::min(settings.max_candidates_per_decision,compiled.variables.size()); + if (!charge(capacity)) return fallback(); + candidates.reserve(capacity); + for (std::size_t column=0;column(column)]; + if (source.variables[slot].type!=VariableType::Binary || source.variables[slot].indicator_origin || variable.min()!=0 || + variable.max()!=1 || variable.size()!=2) continue; + bool exhausted=false; + Candidate candidate{static_cast(column),slot,{}}; + for (std::size_t i=0;i best; + std::size_t best_slot=std::numeric_limits::max(),nonimproving=0; + unsigned int best_failed=0; + double best_min=0,best_max=0; + std::uint64_t decision_calls=0; + for (const auto& candidate:candidates) { + if (!charge()) break; + double down=0,up=0; unsigned int failed=0; + bool completed=false; + if (candidate.history && history[*candidate.history].pairs>=settings.reliability_samples) { + const auto& old=history[*candidate.history];down=old.down;up=old.up; + count_branching(stats.reliable_candidates); + } else { + // Preserve capacity for a complete pair and two ordinary child admissions. + const auto nodes=budget.nodes(); + const bool room=!options.solve.node_limit || (nodes<=*options.solve.node_limit && *options.solve.node_limit-nodes>=4); + if (!room || resident>=options.max_open_nodes || + settings.max_probe_status_calls-stats.probe_status_calls<2 || + settings.max_probe_status_calls_per_decision-decision_calls<2 || + (!candidate.history && history.size()>=settings.max_history_entries) || !charge(16)) continue; + branching_event("pair_begin",candidate.slot); + frontier_checkpoint(budget); + const auto first=probe(parent,candidate,0,baseline,frontier,resident); + const auto second=probe(parent,candidate,1,baseline,frontier,resident); + decision_calls+=2; + count_branching(stats.completed_pairs); + down=first.gain;up=second.gain;failed=unsigned(first.failed)+unsigned(second.failed); + branching_event("pair_before_publication",candidate.slot,down,up); + frontier_checkpoint(budget); + if (!failed) { + const auto previous=candidate.history ? history[*candidate.history]:History{candidate.column}; + if (previous.pairs==std::numeric_limits::max()) + throw Interrupted{Termination::MemoryLimit}; + auto next=previous;++next.pairs; + next.down+=(down-next.down)/static_cast(next.pairs); + next.up+=(up-next.up)/static_cast(next.pairs); + if (!std::isfinite(next.down) || !std::isfinite(next.up)) + throw std::runtime_error("Nonfinite native branching history"); + if (candidate.history) history[*candidate.history]=next; + else history.push_back(next); + stats.history_entries=history.size(); + count_branching(stats.published_pairs);count_branching(stats.finite_samples,2); + count_branching(stats.zero_gain_samples,unsigned(down==0)+unsigned(up==0)); + } + count_branching(stats.failed_directions,failed); + branching_event("pair_published",candidate.slot,down,up); + frontier_checkpoint(budget); completed=true; + } +#ifdef GECODE_NATIVE_BRANCHING_TEST_HOOKS + native_branching_test_event("candidate_score",candidate.slot,down,up); + frontier_checkpoint(budget); +#endif + // Scores never carry evidence. Invalid experimental scores lose priority. + if (!std::isfinite(down) || !std::isfinite(up) || down<0 || up<0) down=up=0; + const double low=std::min(down,up),high=std::max(down,up); + const bool better=(failed || high>0) && (!best || failed>best_failed || + (failed==best_failed && (low>best_min || (low==best_min && + (high>best_max || (high==best_max && candidate.slot=settings.max_nonimproving_pairs) break; + } + if (!best) return fallback(); + branching_event("branch_selected",best_slot,best_min,best_max); + frontier_checkpoint(budget);count_branching(stats.manual_splits);return best; + } +}; +#endif + +#ifdef GECODE_OPTIMIZE_WITH_NATIVE +// Source-size accounting counts logical fields, not bytes or propagation work. +// The preflight never allocates a flattened global argument/payload copy. +void neighborhood_source(const ModelSnapshot& model,NeighborhoodBudget& local) { + local.source(3); // Objective marker, offset, sense. + for(const auto& term:model.objective.terms) {(void)term;local.source();} + for(const auto& row:model.rows) { + local.work(); if(!row.active) continue; + local.source(3); // Row marker and bounds. + for(const auto& term:row.terms) {(void)term;local.source();} + } + for(const auto& indicator:model.indicators) { + local.work();if(!indicator.active) continue; + local.source(8); // Identity, activation, bounds, gate, and M fields. + for(const auto& term:indicator.terms) {(void)term;local.source();} + for(const auto& row:indicator.generated_rows) {(void)row;local.source();} + for(const auto& domain:indicator.domains) {(void)domain;local.source(3);} + } + for(const auto& global:model.globals) { + local.work();if(!global.active) continue;local.source(); + std::visit([&](const auto& data) { + using T=std::decay_t; + if constexpr(std::is_same_v) { + for(auto v:data.variables) {(void)v;local.source();} + } else if constexpr(std::is_same_v) { + local.source(3);for(auto v:data.elements) {(void)v;local.source();} + } else if constexpr(std::is_same_v) { + for(auto v:data.variables) {(void)v;local.source();} + for(const auto& row:data.tuples) {local.source();for(auto v:row) {(void)v;local.source();}} + } else if constexpr(std::is_same_v) { + local.source();for(auto v:data.starts) {(void)v;local.source(3);} + } else if constexpr(std::is_same_v) { + local.source();for(auto v:data.successors) {(void)v;local.source();} + } else { + static_assert(std::is_same_v,"Count every original global payload"); + local.source(2); + for(auto v:data.variables) {(void)v;local.source();} + for(const auto& edge:data.transitions) {(void)edge;local.source(3);} + for(auto state:data.final_states) {(void)state;local.source();} + } + },global.payload); + } +} + +struct NeighborhoodWitness { + std::vector values; + std::vector active; + std::int64_t objective=0; + int raw_cost=0; + double started_seconds=0; +}; +struct NeighborhoodPublicationTiming { + const SolveBudget& budget; + NativeNeighborhoodStatistics& statistics; + double started; + ~NeighborhoodPublicationTiming() {statistics.elapsed_seconds=budget.elapsed_seconds()-started;} +}; +struct NeighborhoodFrame { + std::unique_ptr space; + std::unique_ptr choice; + unsigned int next=0; +}; + +std::optional binary_neighborhood(const ModelSnapshot& source, + const Compiled& compiled,const NativeSearchOptions& search, + const NativeNeighborhoodSettings& settings,NativeNeighborhoodStatistics& statistics, + SolveBudget& shared,const std::vector& reference,int reference_normalized, + std::size_t main_resident) { + NeighborhoodBudget local(settings,statistics,shared); + std::vector stack; + std::unique_ptr current; + std::optional candidate; + const auto release=[&] {current.reset();stack.clear();}; + try { + local.check("before_neighborhood"); + if(!settings.max_status_calls) throw NeighborhoodStopped{NeighborhoodCompletion::StatusLimit}; + const auto room=[&] { + local.check();checkpoint(shared); + if(shared.nodes()==std::numeric_limits::max()) + throw Interrupted{Termination::MemoryLimit}; + if(search.solve.node_limit) { + const auto limit=*search.solve.node_limit; + if(shared.nodes()>limit || limit-shared.nodes()<3) + throw NeighborhoodStopped{NeighborhoodCompletion::SharedNodeReserve}; + } + }; + const auto reserve=[&] { + local.check(); + const auto resident=stack.size()+(current?1U:0U); + if(resident>=settings.max_local_spaces || main_resident>=search.max_open_nodes || + resident>=search.max_open_nodes-main_resident) + throw NeighborhoodStopped{NeighborhoodCompletion::LocalStorageLimit}; + statistics.peak_local_spaces=std::max(statistics.peak_local_spaces,resident+1); + statistics.peak_total_spaces=std::max(statistics.peak_total_spaces,main_resident+resident+1); + }; + room(); + std::vector columns; + for(const auto& variable:source.variables) { + local.work();if(!variable.active) continue;local.source(); + if(variable.type!=VariableType::Binary || variable.indicator_origin || + variable.lower!=0 || variable.upper!=1) continue; + if(statistics.eligible_variables==std::numeric_limits::max()) + throw NeighborhoodStopped{NeighborhoodCompletion::FormulationLimit}; + ++statistics.eligible_variables; + if(statistics.eligible_variables>settings.max_distance_variables || + statistics.eligible_variables>static_cast(Int::Limits::max)) + throw NeighborhoodStopped{NeighborhoodCompletion::FormulationLimit}; + columns.push_back(compiled.columns[variable.variable.id]); + } + if(columns.empty()) throw NeighborhoodStopped{NeighborhoodCompletion::NoEligibleBinary}; + if(settings.radius>=columns.size()) throw NeighborhoodStopped{NeighborhoodCompletion::NonrestrictingRadius}; + neighborhood_source(source,local); + // radius < size <= native limit; both RHS subtraction and total absolute + // activity are therefore within the conservative ordinary linear envelope. + IntArgs coefficients(static_cast(columns.size())); + int rhs=static_cast(settings.radius); + for(std::size_t i=0;i(i)]=value==1?-1:1; + if(value==1) --rhs; + } + reserve();room(); + statistics.attempts=1; + local.check("neighborhood_root_alloc"); + // The local budget parameter also suppresses LP attachment, without editing + // compiled data or the main search's backend/evidence/scheduling state. + current=std::make_unique(compiled,shared,&local); + IntVarArgs variables(static_cast(columns.size())); + for(std::size_t i=0;i(i)]=current->variables[columns[i]]; + } + local.check("before_neighborhood_distance"); + linear(*current,coefficients,variables,IRT_LQ,rhs); + local.check("before_neighborhood_cutoff"); + const int reference_raw=compiled.minimize?reference_normalized:-reference_normalized; + rel(*current,current->cost,compiled.minimize?IRT_LE:IRT_GR,reference_raw); + local.check("after_neighborhood_cutoff"); + for(;;) { + local.work();room(); + if(statistics.status_attempts>=settings.max_status_calls) + throw NeighborhoodStopped{NeighborhoodCompletion::StatusLimit}; + shared.add_nodes();++statistics.status_attempts; + local.check("before_neighborhood_status"); + const auto status=current->status(); + ++statistics.completed_status_calls; + local.check("after_neighborhood_status"); + if(status==SS_SOLVED) { + ++statistics.feasible_candidates; + NeighborhoodWitness staged; + staged.started_seconds=local.start_seconds(); + staged.values.assign(source.variables.size(),std::numeric_limits::quiet_NaN()); + staged.active.reserve(source.variables.size()); + for(const auto& variable:source.variables) {local.work();staged.active.push_back(variable.active);} + for(std::size_t i=0;ivariables[static_cast(i)].val(); + } + neighborhood_event("before_neighborhood_validation",0,0,0,&staged.values); + local.check(); + staged.objective=check_exact(source,staged.values,nullptr,&local); + staged.raw_cost=current->cost.val(); + if(staged.objective!=compiled.offset+staged.raw_cost) + throw std::runtime_error("Neighborhood cost disagrees with exact original objective"); + const auto checked=validate(source,staged.values,search.solve.feasibility_tolerance, + search.solve.integrality_tolerance); + local.check("after_neighborhood_validation"); + if(!checked.valid || !checked.objective || *checked.objective!=static_cast(staged.objective)) + throw std::runtime_error("Neighborhood original-model numerical validation failed: "+checked.message); + std::size_t distance=0; + for(int column:columns) { + local.work();const auto slot=compiled.variables[column].slot; + if(staged.values[slot]!=reference[slot]) ++distance; + } + const int normalized=compiled.minimize?staged.raw_cost:-staged.raw_cost; + if(distance>settings.radius || normalized>=reference_normalized) + throw std::runtime_error("Neighborhood witness violates its distance or strict improvement restriction"); + candidate=std::move(staged);statistics.completion=NeighborhoodCompletion::Improved;break; + } + if(status==SS_FAILED) {++statistics.failed_nodes;current.reset();} + else if(status==SS_BRANCH) { + local.work();local.check("neighborhood_choice"); + std::unique_ptr choice(current->choice()); + if(!choice || choice->alternatives()!=2) + throw std::runtime_error("Neighborhood requires the ordinary two-alternative brancher"); + stack.push_back({std::move(current),std::move(choice),0}); + } else throw std::runtime_error("Unexpected neighborhood Space status"); + while(!stack.empty() && stack.back().next==stack.back().choice->alternatives()) { + local.work();stack.pop_back(); + } + if(stack.empty()) {statistics.completion=NeighborhoodCompletion::NoImprovement;break;} + local.work();room();reserve(); + auto& frame=stack.back(); + local.check("neighborhood_child_clone"); + current.reset(static_cast(frame.space->clone())); + current->commit(*frame.choice,frame.next++); + local.check("neighborhood_child_committed"); + } + } catch(const NeighborhoodStopped& stop) { + statistics.completion=stop.reason;candidate.reset(); + } catch(const Interrupted& stop) { + statistics.completion=NeighborhoodCompletion::GlobalStop;statistics.stop_reason=stop.reason; + release();local.finish();throw; + } catch(...) { + statistics.completion=NeighborhoodCompletion::Error; + release();local.finish();throw; + } + release(); + try {local.check("after_neighborhood_release");} + catch(const NeighborhoodStopped& stop) {statistics.completion=stop.reason;candidate.reset();} + catch(const Interrupted& stop) { + statistics.completion=NeighborhoodCompletion::GlobalStop;statistics.stop_reason=stop.reason; + local.finish();throw; + } catch(...) {statistics.completion=NeighborhoodCompletion::Error;local.finish();throw;} + local.finish();return candidate; +} +#endif + +NativeSearchResult native_frontier_impl(const ModelSnapshot& model, + const NativeSearchOptions& options, SolveBudget& budget, + const NativeNeighborhoodSettings* neighborhoods=nullptr, + NativeNeighborhoodStatistics* neighborhood_statistics=nullptr) { + NativeSearchResult output; + auto& result = output.result; + auto& statistics = output.frontier; + result.model_id = model.model_id; result.revision = model.revision; + result.backend = options.relaxation ? "Gecode native frontier + checked LP" : "Gecode native frontier"; + if(neighborhoods) result.backend += " + BinaryHamming"; + output.branching.requested=bool(options.branching); +#ifdef GECODE_OPTIMIZE_WITH_NATIVE + std::vector frontier; + std::unique_ptr parent; + std::optional active_bound, initial_bound, incumbent; + std::int64_t offset = 0; + bool minimize = true, fallback_bound = false; + std::uint64_t serial = 0; +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + RelaxationState relaxation; +#endif +#else + (void)statistics; +#endif + if (options.relaxation && options.relaxation->root_cover_cuts) { + output.relaxation.root_cover.requested=true; + output.relaxation.root_cover.completion=NativeRootCoverCompletion::NotStarted; +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + relaxation.root=output.relaxation.root_cover; +#endif + } + try { + options.validate();if(neighborhoods) neighborhoods->validate();result.guarantee = options.solve.guarantee; + validate_structure(model); + if (options.solve.backend != Backend::Auto && options.solve.backend != Backend::Native) + throw Unsupported("Native frontier requires backend Auto or Native"); + if (options.solve.guarantee == Guarantee::Certified) + throw Unsupported("Native frontier does not export independently checkable solve certificates"); + if (options.solve.threads != 1 || options.solve.random_seed != 0) + throw Unsupported("Native frontier supports one worker and seed zero"); +#ifndef GECODE_OPTIMIZE_WITH_NATIVE + throw Unsupported("Built without the native Gecode bridge"); +#else + result.backend_version = GECODE_VERSION; +#ifndef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + if (options.relaxation) + throw Unsupported("Native frontier checked LP requires HiGHS and checked 128-bit compiler arithmetic"); +#endif + checkpoint(budget); + auto compiled = compile(model, budget); + minimize = compiled.minimize; offset = compiled.offset; + initial_bound = minimize ? compiled.cost_lower : -compiled.cost_upper; + active_bound = initial_bound; // The entire original region is represented. + if (!options.solve.primal_start.empty()) { + auto start = prepare_start(model, compiled, options.solve, budget); + const int normalized = minimize ? start.cost : -start.cost; + publish_start(std::move(start), result, budget); + incumbent = normalized; + start_checkpoint(budget, "after_start_publication"); + } +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + if (options.relaxation) { + prepare_relaxation(compiled,*options.relaxation,budget,relaxation); + result.backend_version += std::string("; HiGHS ") + Highs().version(); + checkpoint(budget); + } +#endif + const auto reserve_space = [&] { + const auto resident = frontier.size() + (active_bound ? 1U : 0U); + // The root's reservation is made separately, before construction. + if (resident >= options.max_open_nodes) throw Interrupted{Termination::MemoryLimit}; + statistics.peak_open_nodes = std::max(statistics.peak_open_nodes, resident + 1); + }; + const auto enqueue = [&](std::unique_ptr& space, int bound) { + if (serial == std::numeric_limits::max()) + throw Interrupted{Termination::MemoryLimit}; + bool contains_witness = options.order == NativeSearchOrder::BestBound && + bool(space->knapsack_witness); + for (std::size_t i = 0; contains_witness && i < space->knapsack_witness->size(); ++i) { + frontier_checkpoint(budget); + contains_witness = space->variables[static_cast(i)].in((*space->knapsack_witness)[i]); + } + // Compute preference once on the stable space. The comparator never + // scans domains, and the exact objective bound remains the primary key. + frontier.push_back({std::move(space), bound, serial++, contains_witness}); + if (options.order == NativeSearchOrder::BestBound) + std::push_heap(frontier.begin(), frontier.end(), FrontierCompare{}); + }; + const auto evaluate = [&](NativeSpace& space, int& inherited) { + checkpoint(budget); + if (budget.nodes()==std::numeric_limits::max()) + throw Interrupted{Termination::MemoryLimit}; + budget.add_nodes(); ++statistics.admitted_nodes; + frontier_event("before_propagation"); + frontier_checkpoint(budget); + const auto status = space.status(); + frontier_event("after_propagation"); + frontier_checkpoint(budget); + if (status == SS_FAILED) { ++statistics.failed_nodes; return false; } + inherited = std::max(inherited, minimize ? space.cost.min() : -space.cost.max()); + if (incumbent && inherited >= *incumbent) { + ++statistics.bound_pruned_nodes; return false; + } + if (status == SS_SOLVED) { + std::vector values(model.variables.size(), std::numeric_limits::quiet_NaN()); + std::vector mask; + for (const auto& variable : model.variables) mask.push_back(variable.active); + for (std::size_t i = 0; i < compiled.variables.size(); ++i) { + frontier_checkpoint(budget); + values[compiled.variables[i].slot] = space.variables[static_cast(i)].val(); + } + frontier_event("before_validation"); + const auto objective = check_exact(model, values); + if (objective != offset + space.cost.val()) + throw std::runtime_error("Native frontier cost disagrees with the exact original objective"); + const auto checked = validate(model, values, options.solve.feasibility_tolerance, + options.solve.integrality_tolerance); + if (!checked.valid || !checked.objective || *checked.objective != static_cast(objective)) + throw std::runtime_error("Native frontier original-model witness validation failed: " + checked.message); + frontier_event("after_validation"); + frontier_checkpoint(budget); + const int value = minimize ? space.cost.val() : -space.cost.val(); + // All allocating validation/copy work precedes this publication gate. + result.values = std::move(values); result.active_variables = std::move(mask); + result.objective = static_cast(objective); result.solution_validated = true; + incumbent = value; ++statistics.feasible_leaves; + return false; + } + if (status != SS_BRANCH) throw std::runtime_error("Unexpected native frontier space status"); + return true; + }; + if (!options.max_open_nodes) throw Interrupted{Termination::MemoryLimit}; + statistics.peak_open_nodes = 1; + frontier_event("root_alloc"); + parent = std::make_unique(compiled, budget, nullptr, + !options.relaxation, options.order == NativeSearchOrder::DepthFirst); + if (incumbent) start_cutoff(*parent, minimize ? *incumbent : -*incumbent, budget); + if (evaluate(*parent, *active_bound)) enqueue(parent, *active_bound); + parent.reset(); active_bound.reset(); + BinaryReliability branching(options,model,compiled,budget,output.branching); + while (!frontier.empty()) { + frontier_checkpoint(budget); + if (options.order == NativeSearchOrder::BestBound) + std::pop_heap(frontier.begin(), frontier.end(), FrontierCompare{}); + active_bound = frontier.back().bound; + parent = std::move(frontier.back().space); frontier.pop_back(); + if (incumbent && *active_bound >= *incumbent) { + ++statistics.bound_pruned_nodes; parent.reset(); active_bound.reset(); continue; + } + if(neighborhoods && incumbent && neighborhood_statistics->completion==NeighborhoodCompletion::NotStarted) { +#ifdef GECODE_NATIVE_NEIGHBORHOOD_TEST_HOOKS + for(std::size_t i=0;ivariables[static_cast(i)].min(),parent->variables[static_cast(i)].max()); +#endif + auto improvement=binary_neighborhood(model,compiled,options,*neighborhoods,*neighborhood_statistics, + budget,result.values,*incumbent,frontier.size()+1); + if(improvement) { + NeighborhoodPublicationTiming timing{budget,*neighborhood_statistics,improvement->started_seconds}; + // Local owners are already released. Complete nonthrowing publication + // precedes the next hook/checkpoint, including scalar cost installation. + frontier_checkpoint(budget); + neighborhood_event("before_neighborhood_publication"); + frontier_checkpoint(budget); + neighborhood_statistics->elapsed_seconds=budget.elapsed_seconds()-improvement->started_seconds; + if(neighborhood_statistics->elapsed_seconds>=neighborhoods->time_limit_seconds) { + neighborhood_statistics->completion=NeighborhoodCompletion::LocalTimeLimit; + improvement.reset(); + } else { + const int normalized=minimize?improvement->raw_cost:-improvement->raw_cost; + result.values=std::move(improvement->values);result.active_variables=std::move(improvement->active); + result.objective=static_cast(improvement->objective);result.solution_validated=true; + incumbent=normalized;++neighborhood_statistics->accepted_improvements; + neighborhood_event("after_neighborhood_publication"); + frontier_checkpoint(budget); + } + } + if(incumbent && *active_bound>=*incumbent) { + ++statistics.bound_pruned_nodes;parent.reset();active_bound.reset();continue; + } + } + // The stable parent remains represented until every alternative is + // evaluated and safely enqueued or discharged. Child failure cannot + // erase an unvisited sibling from the global-bound calculation. + const auto manual=options.branching ? branching.select(*parent,*active_bound,statistics,frontier.size()+1):std::optional{}; + std::unique_ptr choice; + if (!manual) choice.reset(parent->choice()); + if (!manual && (!choice || !choice->alternatives())) throw std::runtime_error("Native branch has no alternatives"); + const auto alternatives=manual ? 2U:choice->alternatives(); + ++statistics.expanded_nodes; + for (unsigned int alternative = 0; alternative < alternatives; ++alternative) { + checkpoint(budget); + reserve_space(); + frontier_event("child_clone"); + std::unique_ptr child(static_cast(parent->clone())); + if (manual) BinaryReliability::commit(*child,*manual,alternative); + else child->commit(*choice, alternative); + frontier_event("child_committed"); + if (incumbent) + rel(*child, child->cost, minimize ? IRT_LE : IRT_GR, minimize ? *incumbent : -*incumbent); + int child_bound = *active_bound; + if (evaluate(*child, child_bound)) { + enqueue(child, child_bound); + frontier_event("child_enqueued"); + frontier_checkpoint(budget); + } + } + parent.reset(); active_bound.reset(); + frontier_event("parent_discharged"); + } + frontier_checkpoint(budget); + result.termination = result.has_solution() ? Termination::Optimal : Termination::Infeasible; + result.message = "All native frontier regions exhausted or pruned by exact incumbent bounds"; + if(neighborhood_statistics && neighborhood_statistics->completion==NeighborhoodCompletion::NotStarted) + neighborhood_statistics->completion=incumbent?NeighborhoodCompletion::ProofCompletedBeforeAttempt:NeighborhoodCompletion::NoIncumbent; +#endif + } catch (const Unsupported& error) { + result.termination = Termination::Unsupported; result.message = error.what(); + } catch (const Interrupted& error) { + result.termination = error.reason; + result.message = error.reason == Termination::MemoryLimit ? "Native frontier open-node storage limit reached" + : "Native frontier shared solve budget stopped"; + } catch (const ModelError& error) { + result.termination = Termination::InvalidModel; result.message = error.what(); + } catch (const std::bad_alloc&) { + result.termination = Termination::MemoryLimit; result.message = "Native frontier allocation failed"; +#ifdef GECODE_OPTIMIZE_WITH_NATIVE + } catch (const MemoryExhausted& error) { + result.termination = Termination::MemoryLimit; result.message = error.what(); +#endif + } catch (const std::exception& error) { + result.termination = Termination::BackendError; result.message = error.what(); +#ifdef GECODE_OPTIMIZE_WITH_NATIVE + fallback_bound = true; +#endif + } +#ifdef GECODE_OPTIMIZE_WITH_NATIVE + output.branching.budget_nodes=budget.nodes(); + statistics.unresolved_regions = frontier.size() + (active_bound ? 1U : 0U); + std::optional global = incumbent; + const auto include = [&](int value) { global = global ? std::min(*global, value) : value; }; + for (const auto& node : frontier) include(node.bound); + if (active_bound) include(*active_bound); + // A semantic implementation failure cannot authorize propagated evidence. + if (fallback_bound) global = initial_bound; + if (result.termination != Termination::Infeasible && global) { + result.best_bound = static_cast(offset + (minimize ? *global : -*global)); + result.update_gaps(model.objective.sense); + } +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + output.relaxation=relaxation_statistics(relaxation); +#endif + // Charge release of the owning frontier to the same end-to-end deadline. + frontier.clear(); parent.reset(); + if (result.start_submitted) { +#ifdef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + relaxation = RelaxationState{}; +#endif + if (finish_start(result, budget)) { + result.best_bound = initial_bound ? std::optional(static_cast(offset + (minimize ? *initial_bound : -*initial_bound))) : std::nullopt; + result.update_gaps(model.objective.sense); + } + } + if (budget.cancelled() || budget.time_limit_reached()) { + result.termination = budget.cancelled() ? Termination::Cancelled : Termination::TimeLimit; + result.message = "Native frontier time/cancellation budget stopped before return"; + } +#endif + result.elapsed_seconds = budget.elapsed_seconds(); + if(neighborhood_statistics) { + neighborhood_statistics->budget_nodes=budget.nodes(); + neighborhood_statistics->peak_total_spaces=std::max(neighborhood_statistics->peak_total_spaces, + statistics.peak_open_nodes); + if(neighborhood_statistics->completion==NativeNeighborhoodCompletion::Error) + neighborhood_statistics->stop_reason=result.termination; + else if(neighborhood_statistics->completion==NativeNeighborhoodCompletion::Improved && + !neighborhood_statistics->accepted_improvements) { + neighborhood_statistics->completion=result.termination==Termination::BackendError + ?NativeNeighborhoodCompletion::Error:NativeNeighborhoodCompletion::GlobalStop; + neighborhood_statistics->stop_reason=result.termination; + } + else if(neighborhood_statistics->completion==NativeNeighborhoodCompletion::NotStarted && + result.termination!=Termination::Optimal && result.termination!=Termination::Infeasible) { + neighborhood_statistics->completion=NativeNeighborhoodCompletion::GlobalStop; + neighborhood_statistics->stop_reason=result.termination; + } + } + return output; +} +NativeSearchResult frontier_failure(ModelId id, Revision revision, const NativeSearchOptions& options, + Termination termination, const std::string& message) { + NativeSearchResult output; auto& result = output.result; + result.model_id = id; result.revision = revision; result.guarantee = options.solve.guarantee; + result.backend = options.relaxation ? "Gecode native frontier + checked LP" : "Gecode native frontier"; + output.branching.requested=bool(options.branching); + if(options.relaxation && options.relaxation->root_cover_cuts) { + output.relaxation.root_cover.requested=true; + output.relaxation.root_cover.completion=NativeRootCoverCompletion::NotStarted; + } + result.termination = termination; result.message = message; return output; +} +} // namespace + +NativeSearchResult solve_native_search(const ModelSnapshot& model, const NativeSearchOptions& options) { + try { + SolveBudget budget(options.solve); + return native_frontier_impl(model, options, budget); + } catch (const ModelError& error) { + return frontier_failure(model.model_id, model.revision, options, Termination::InvalidModel, error.what()); + } catch (const std::bad_alloc&) { + return frontier_failure(model.model_id, model.revision, options, Termination::MemoryLimit, "Native frontier allocation failed"); + } +} +NativeSearchResult solve_native_search(const Model& model, const NativeSearchOptions& options) { + try { + SolveBudget budget(options.solve); + return native_frontier_impl(model.snapshot(), options, budget); + } catch (const ModelError& error) { + return frontier_failure(model.id(), model.revision(), options, Termination::InvalidModel, error.what()); + } catch (const std::bad_alloc&) { + return frontier_failure(model.id(), model.revision(), options, Termination::MemoryLimit, "Native frontier snapshot allocation failed"); + } +} + +void NativeNeighborhoodSettings::validate() const { + if(policy!=NativeNeighborhoodPolicy::BinaryHamming) + throw ModelError("Unknown native neighborhood policy"); + if(!std::isfinite(time_limit_seconds) || time_limit_seconds<0) + throw ModelError("Native neighborhood time limit must be finite and nonnegative"); +} +void NativeNeighborhoodOptions::validate() const { + search.validate();neighborhood.validate(); +} +namespace { +NativeNeighborhoodResult neighborhood_failure(ModelId id,Revision revision, + const NativeNeighborhoodOptions& options,Termination reason,const std::string& message) { + NativeNeighborhoodResult out; + out.search=frontier_failure(id,revision,options.search,reason,message); + out.search.result.backend+=" + BinaryHamming"; + out.neighborhood.requested=true; + out.neighborhood.completion=NativeNeighborhoodCompletion::GlobalStop; + out.neighborhood.stop_reason=reason; + return out; +} +} +NativeNeighborhoodResult solve_native_neighborhoods(const ModelSnapshot& model, + const NativeNeighborhoodOptions& options) { + try { + NativeNeighborhoodResult out;out.neighborhood.requested=true; + SolveBudget budget(options.search.solve); + out.search=native_frontier_impl(model,options.search,budget,&options.neighborhood,&out.neighborhood); + return out; + } catch(const ModelError& error) { + return neighborhood_failure(model.model_id,model.revision,options,Termination::InvalidModel,error.what()); + } catch(const std::bad_alloc&) { + return neighborhood_failure(model.model_id,model.revision,options,Termination::MemoryLimit,"Native neighborhood allocation failed"); + } +} +NativeNeighborhoodResult solve_native_neighborhoods(const Model& model, + const NativeNeighborhoodOptions& options) { + try { + NativeNeighborhoodResult out;out.neighborhood.requested=true; + SolveBudget budget(options.search.solve); + out.search=native_frontier_impl(model.snapshot(),options.search,budget,&options.neighborhood,&out.neighborhood); + return out; + } catch(const ModelError& error) { + return neighborhood_failure(model.id(),model.revision(),options,Termination::InvalidModel,error.what()); + } catch(const std::bad_alloc&) { + return neighborhood_failure(model.id(),model.revision(),options,Termination::MemoryLimit,"Native neighborhood snapshot allocation failed"); + } +} + +namespace { +enum class AutomaticNativeRoute { Native, RootLp, UpdatedLp, ReliabilityLp }; +struct AutomaticNativeSelection { + AutomaticNativeRoute route = AutomaticNativeRoute::Native; + const char* reason = "ordinary native search"; + bool covers = false; +}; + +AutomaticNativeSelection select_native_route(const ModelSnapshot& model, + const SolveOptions& options, const SolveBudget& budget, + const NativeAutoSettings& settings) { + // Optional routes must never change acceptance of an explicit native option. + if ((options.backend != Backend::Auto && options.backend != Backend::Native) || + options.guarantee == Guarantee::Certified || options.threads != 1 || options.random_seed != 0) + return {AutomaticNativeRoute::Native,"native option compatibility"}; +#ifndef GECODE_OPTIMIZE_WITH_NATIVE + (void)model; (void)budget; (void)settings; + return {AutomaticNativeRoute::Native,"checked LP unavailable"}; +#else + // Bound selection's extra work before calling the full native validator or + // compiler. The ordinary route still validates every uninspected input. + constexpr std::size_t max_columns = 4096, max_rows = 4096, max_nonzeros = 65536; + if (model.variables.size() > max_columns || model.rows.size() > max_rows || + model.objective.terms.size() > max_nonzeros) + return {AutomaticNativeRoute::Native,"structural inspection cap"}; + if (!model.globals.empty() || !model.indicators.empty()) + return {AutomaticNativeRoute::Native,"global or indicator structure"}; + std::size_t nonzeros = model.objective.terms.size(), active_variables = 0; + for (const auto& variable : model.variables) { + checkpoint(budget); + if (!variable.active) continue; + ++active_variables; + if (variable.type != VariableType::Binary) + return {AutomaticNativeRoute::Native,"nonbinary variables"}; + } + if (active_variables <= 4) + return {AutomaticNativeRoute::Native,"tiny model"}; + for (const auto& row : model.rows) { + checkpoint(budget); + if (row.terms.size() > max_nonzeros-nonzeros) + return {AutomaticNativeRoute::Native,"structural inspection cap"}; + nonzeros += row.terms.size(); + } + validate_structure(model); + checkpoint(budget); + const auto compiled = compile(model,budget); + if (!compiled.binary_domains || compiled.rows.empty()) + return {AutomaticNativeRoute::Native,"binary domain or row structure"}; + if (settings.knapsack && prepare_knapsack(compiled,budget)) + return {AutomaticNativeRoute::Native,"eligible exact knapsack DP"}; +#ifndef GECODE_OPTIMIZE_NATIVE_LP_ENABLED + return {AutomaticNativeRoute::Native,"checked LP unavailable"}; +#else + // Use the same checked-LP numeric admission as the explicit LP API, without + // constructing an LP backend or solving a relaxation during selection. + try { (void)relaxation_model(compiled,budget); } + catch (const Unsupported&) { + return {AutomaticNativeRoute::Native,"checked LP numeric admission"}; + } + bool unit = true, nonnegative = true; + for (const auto& row : compiled.rows) for (const auto& term : row.terms) { + checkpoint(budget); + unit = unit && (term.coefficient == 1 || term.coefficient == -1); + nonnegative = nonnegative && term.coefficient >= 0; + } + if (unit && nonnegative) + return {AutomaticNativeRoute::RootLp,"root checked LP for nonnegative unit rows"}; + if (unit) + return {AutomaticNativeRoute::UpdatedLp,"updated checked LP for signed unit rows"}; + if (options.relative_gap != 0 || options.absolute_gap != 0) + return {AutomaticNativeRoute::UpdatedLp,"updated checked LP with covers; gap-compatible BAB",true}; + return {AutomaticNativeRoute::ReliabilityLp,"updated checked LP, covers and binary reliability",true}; +#endif +#endif +} + +SolveResult native_auto_impl(const ModelSnapshot& model, const SolveOptions& options, + SolveBudget& budget, const NativeAutoSettings& settings) { + SolveResult result; + result.model_id=model.model_id; result.revision=model.revision; + result.backend="Gecode native"; result.guarantee=options.guarantee; + AutomaticNativeSelection selected; + try { + selected=select_native_route(model,options,budget,settings); + if (selected.route == AutomaticNativeRoute::Native) { + result=solve_native_impl(model,options,nullptr,nullptr,&budget,settings.knapsack); + } else { + NativeLpOptions lp; lp.solve=options; + if (selected.route != AutomaticNativeRoute::RootLp) { + lp.frequency=NativeLpFrequency::AfterBoundChanges; + lp.bound_change_interval=4; + } + if (selected.covers) lp.root_cover_cuts=NativeRootCoverSettings{}; + if (selected.route == AutomaticNativeRoute::ReliabilityLp) { + NativeSearchOptions search; search.solve=options; + search.order=NativeSearchOrder::DepthFirst; + search.relaxation=static_cast(lp); + search.branching=NativeBranchingSettings{}; + search.branching->max_probe_status_calls=128; + // Keep the public search default's frontier storage allowance. + result=native_frontier_impl(model,search,budget).result; + } else { + NativeLpStatistics statistics; + result=solve_native_impl(model,options,&lp,&statistics,&budget); + } + } + } catch (const Interrupted& error) { + // Preserve native structural-error precedence even if bounded inspection + // observes a stop before reaching validate_structure. No search can start + // because the same exhausted/cancelled budget reaches the native bridge. + (void)error; + result=solve_native_impl(model,options,nullptr,nullptr,&budget,settings.knapsack); + } catch (const Unsupported& error) { + result.termination=Termination::Unsupported; result.message=error.what(); + } catch (const ModelError& error) { + result.termination=Termination::InvalidModel; result.message=error.what(); + } catch (const std::bad_alloc&) { + result.termination=Termination::MemoryLimit; result.message="Native automatic selection allocation failed"; +#ifdef GECODE_OPTIMIZE_WITH_NATIVE + } catch (const MemoryExhausted& error) { + result.termination=Termination::MemoryLimit; result.message=error.what(); +#endif + } catch (const std::exception& error) { + result.termination=Termination::BackendError; result.message=error.what(); + } + result.message=std::string("Automatic native policy: ")+selected.reason+"; "+result.message; + result.elapsed_seconds=budget.elapsed_seconds(); + return result; +} + +// Transformations are internal and single-pass. Explicit native APIs retain +// their original behavior; all callbacks keep the same global budget. +SolveResult native_auto_pipeline(const ModelSnapshot& model, const SolveOptions& options, + SolveBudget& budget, const NativeAutoSettings& settings) { +#ifndef GECODE_OPTIMIZE_WITH_NATIVE + return native_auto_impl(model,options,budget,settings); +#else + const auto fallback=[&]{return native_auto_impl(model,options,budget,settings);}; + if ((!settings.presolve && !settings.components && !settings.symmetry) || + !options.primal_start.empty() || options.guarantee==Guarantee::Certified || + (options.backend!=Backend::Auto && options.backend!=Backend::Native) || + options.threads!=1 || options.random_seed!=0 || + !model.globals.empty() || !model.indicators.empty() || + model.variables.size()>4096 || model.rows.size()>4096 || + model.objective.terms.size()>65536) return fallback(); + std::size_t nonzeros=model.objective.terms.size(); + for(const auto& v:model.variables) if(v.active && + v.type!=VariableType::Integer && v.type!=VariableType::Binary)return fallback(); + for(const auto& row:model.rows){ + if(row.terms.size()>65536-nonzeros)return fallback(); + nonzeros+=row.terms.size(); + } + try { + // Preserve original native numeric/structural admission before reductions + // could erase an unsupported term or contradiction. + validate_structure(model);checkpoint(budget); + const auto compiled=compile(model,budget); + if(settings.knapsack && prepare_knapsack(compiled,budget))return fallback(); + const Detail::NativeSolveContinuation leaf=[&](const ModelSnapshot& m, + const SolveOptions& o,SolveBudget& b){return native_auto_impl(m,o,b,settings);}; + const Detail::NativeSolveContinuation symmetric=[&](const ModelSnapshot& m, + const SolveOptions& o,SolveBudget& b){ + if(!settings.symmetry)return leaf(m,o,b); + // A reduced/component model can itself become a DP candidate. + try { + if(settings.knapsack && prepare_knapsack(compile(m,b),b))return leaf(m,o,b); + } catch(const Unsupported&) { + // Let the ordinary leaf report admission rather than throwing through + // the presolve coordinator, which can then retain the original model. + return leaf(m,o,b); + } + if(auto result=Detail::native_symmetry(m,o,b,leaf))return *result; + return leaf(m,o,b); + }; + const Detail::NativeSolveContinuation components=[&](const ModelSnapshot& m, + const SolveOptions& o,SolveBudget& b){ + if(settings.components) + if(auto result=Detail::native_components(m,o,b,symmetric))return *result; + return symmetric(m,o,b); + }; + const Detail::NativeSolveContinuation presolved=[&](const ModelSnapshot& m, + const SolveOptions& o,SolveBudget& b){ + if(settings.presolve) + if(auto result=Detail::native_presolve(m,o,b,components))return *result; + return components(m,o,b); + }; + auto normalized=settings.presolve ? Detail::native_objective_auxiliary(model,options,budget,presolved): + std::optional{}; + auto solved=normalized ? std::move(*normalized):presolved(model,options,budget); + solved.message="Automatic native policy: preprocessing; "+solved.message; + solved.elapsed_seconds=budget.elapsed_seconds(); + return solved; + } catch(const Interrupted&) {return fallback();} + catch(const Unsupported&) {return fallback();} + catch(const ModelError&) {return fallback();} + catch(const std::bad_alloc&) { + SolveResult result;result.model_id=model.model_id;result.revision=model.revision; + result.backend="Gecode native";result.guarantee=options.guarantee; + result.termination=Termination::MemoryLimit; + result.message="Automatic native policy: preprocessing allocation failed"; + result.elapsed_seconds=budget.elapsed_seconds();return result; + } +#endif +} + +template +SolveResult native_auto_entry(const Source& source, const SolveOptions& options, + const NativeAutoSettings& settings, ModelId id, Revision revision) { + const auto started=std::chrono::steady_clock::now(); + SolveResult result; result.model_id=id; result.revision=revision; + result.backend="Gecode native"; result.guarantee=options.guarantee; + try { + SolveBudget budget(options); + if constexpr (std::is_same_v) + result=native_auto_pipeline(source.snapshot(),options,budget,settings); + else result=native_auto_pipeline(source,options,budget,settings); + // Include transformation/artifact cleanup in the publication deadline. + // Ordinary timed incumbents and the explicit-start path retain their existing + // capture semantics; a newly completed preprocessing proof must be timely. + if (result.message.find("Automatic native policy: preprocessing;")==0 && + (result.termination==Termination::Optimal || result.termination==Termination::Infeasible) && + (budget.cancelled() || budget.time_limit_reached())) { + result=SolveResult{};result.model_id=id;result.revision=revision; + result.backend="Gecode native";result.guarantee=options.guarantee; + result.termination=budget.cancelled()?Termination::Cancelled:Termination::TimeLimit; + result.message="Automatic native policy: preprocessing cleanup exhausted deadline"; + } + } catch (const ModelError& error) { + result.termination=Termination::InvalidModel; + result.message=std::string("Automatic native policy: native validation; ")+error.what(); + } catch (const std::bad_alloc&) { + result.termination=Termination::MemoryLimit; + result.message="Automatic native policy: native preparation allocation failed"; + } + result.elapsed_seconds=std::chrono::duration(std::chrono::steady_clock::now()-started).count(); + return result; +} +} + +SolveResult solve_native_auto(const ModelSnapshot& model, const SolveOptions& options) { + return native_auto_entry(model,options,{},model.model_id,model.revision); +} +SolveResult solve_native_auto(const Model& model, const SolveOptions& options) { + return native_auto_entry(model,options,{},model.id(),model.revision()); +} + +SolveResult solve_native_auto_configured(const ModelSnapshot& model, const NativeAutoOptions& options) { + return native_auto_entry(model,options.solve,options.settings,model.model_id,model.revision); +} +SolveResult solve_native_auto_configured(const Model& model, const NativeAutoOptions& options) { + return native_auto_entry(model,options.solve,options.settings,model.id(),model.revision()); +} + +void NativeRaceOptions::validate() const { + solve.validate(); + if (!std::isfinite(exploration_seconds) || exploration_seconds<0.0) + throw ModelError("native race exploration time must be nonnegative and finite"); + if (!probe_node_limit) + throw ModelError("native race probe node allowance must be positive"); +} + +namespace { +SolveResult native_race_impl(const ModelSnapshot& model,const NativeRaceOptions& options, + SolveBudget& budget) { + const auto& solve=options.solve; + // Never reinterpret incompatible options or disturb supplied-start semantics. + if (options.exploration_seconds==0 || !solve.primal_start.empty() || + !model.globals.empty() || !model.indicators.empty() || + solve.guarantee==Guarantee::Certified || solve.threads!=1 || solve.random_seed!=0 || + (solve.backend!=Backend::Auto && solve.backend!=Backend::Native) || + !native_capabilities().available) { + auto result=native_auto_pipeline(model,solve,budget,options.automatic); + result.message="Native race skipped (disabled or direct-path compatibility); "+result.message; + return result; + } + const bool minimize=model.objective.sense==ObjectiveSense::Minimize; + const auto better_value=[&](double a,double b){return minimize ? ab;}; + const auto better_bound=[&](double a,double b){return minimize ? a>b:a incumbent; + std::optional bound; + unsigned probes=0; + auto observe=[&](const SolveResult& result){ + // Each candidate solves this same original model; preprocessing restores and + // checks original coordinates before returning. Never compare partial points. + if(result.has_solution() && (!incumbent || better_value(*result.objective,*incumbent->objective))) + incumbent=result; + if(result.best_bound && !std::isnan(*result.best_bound) && + (!bound || better_bound(*result.best_bound,*bound)))bound=result.best_bound; + }; + const auto run=[&](bool automatic,SolveBudget& allowance){ + auto result=automatic ? native_auto_pipeline(model,solve,allowance,options.automatic): + solve_native_impl(model,solve,nullptr,nullptr,&allowance); + // A completed proof is only published after destruction of candidate-local + // artifacts. Reaching a node cap on the final admitted node is permitted. + if((result.termination==Termination::Optimal || result.termination==Termination::Infeasible) && + (allowance.cancelled() || allowance.time_limit_reached())) { + result=SolveResult{};result.model_id=model.model_id;result.revision=model.revision; + result.backend="Gecode native";result.guarantee=solve.guarantee; + result.termination=allowance.cancelled()?Termination::Cancelled:Termination::TimeLimit; + result.message="Native race candidate cleanup exhausted deadline"; + } + return result; + }; + const auto complete=[](const SolveResult& r){return r.termination==Termination::Optimal || + r.termination==Termination::Infeasible;}; + const auto failed=[](const SolveResult& r){return r.termination==Termination::InvalidModel || + r.termination==Termination::Unsupported || r.termination==Termination::BackendError || + r.termination==Termination::MemoryLimit || r.termination==Termination::NumericalFailure;}; + const auto finish=[&](SolveResult result,const char* selected){ + if(!complete(result)) { + if(incumbent && (!result.has_solution() || better_value(*incumbent->objective,*result.objective))) { + result.values=std::move(incumbent->values); + result.active_variables=std::move(incumbent->active_variables); + result.objective=incumbent->objective;result.solution_validated=true; + } + if(bound && (!result.best_bound || better_bound(*bound,*result.best_bound)))result.best_bound=bound; + if(auto reason=budget.stop_reason())result.termination=*reason; + result.update_gaps(model.objective.sense); + } + result.message="Native sequential race: "+std::to_string(probes)+" probes; selected "+selected+ + "; cumulative nodes="+std::to_string(budget.nodes())+ + "; exploration/restart may increase CPU work and solve time; "+result.message; + result.elapsed_seconds=budget.elapsed_seconds();return result; + }; + // Budget slices share the original clock/cancellation/node counter. Reserve + // most of a finite deadline for exploitation; no hidden fresh solve budget. + const auto exploration=std::min(options.exploration_seconds,budget.remaining_seconds()*0.25); + auto automatic_budget=budget.slice(exploration/2,options.probe_node_limit); + auto automatic=run(true,automatic_budget);++probes;observe(automatic); + if(complete(automatic) || failed(automatic) || budget.expired())return finish(std::move(automatic),"automatic"); + auto plain_budget=budget.slice(exploration/2,options.probe_node_limit); + auto plain=run(false,plain_budget);++probes;observe(plain); + if(complete(plain) || budget.expired())return finish(std::move(plain),"ordinary native"); + bool use_automatic=true; + if(!failed(plain)) { + if(plain.has_solution() != automatic.has_solution())use_automatic=automatic.has_solution(); + else if(plain.has_solution() && plain.objective!=automatic.objective) + use_automatic=!better_value(*plain.objective,*automatic.objective); + else if(plain.best_bound && (!automatic.best_bound || better_bound(*plain.best_bound,*automatic.best_bound))) + use_automatic=false; + } + // Intentionally restart: these engines do not export resumable search state. + // Keep probe incumbents separately so an unproductive restart cannot lose them. + auto result=run(use_automatic,budget);observe(result); + return finish(std::move(result),use_automatic?"automatic":"ordinary native"); +} + +template +SolveResult native_race_entry(const Source& source,const NativeRaceOptions& options, + ModelId id,Revision revision) { + const auto started=std::chrono::steady_clock::now(); + SolveResult result;result.model_id=id;result.revision=revision; + result.backend="Gecode native";result.guarantee=options.solve.guarantee; + try { + SolveBudget budget(options.solve);options.validate(); + if constexpr(std::is_same_v)result=native_race_impl(source.snapshot(),options,budget); + else result=native_race_impl(source,options,budget); + if((result.termination==Termination::Optimal || result.termination==Termination::Infeasible) && + (budget.cancelled() || budget.time_limit_reached())) { + result=SolveResult{};result.model_id=id;result.revision=revision; + result.backend="Gecode native";result.guarantee=options.solve.guarantee; + result.termination=budget.cancelled()?Termination::Cancelled:Termination::TimeLimit; + result.message="Native race publication cleanup exhausted deadline"; + } + } catch(const ModelError& error) { + result.termination=Termination::InvalidModel;result.message=error.what(); + } catch(const std::bad_alloc&) { + result.termination=Termination::MemoryLimit;result.message="Native race allocation failed"; + } catch(const std::exception& error) { + result.termination=Termination::BackendError;result.message=error.what(); + } + result.elapsed_seconds=std::chrono::duration(std::chrono::steady_clock::now()-started).count(); + return result; +} +} + +SolveResult solve_native_race(const ModelSnapshot& model,const NativeRaceOptions& options) { + return native_race_entry(model,options,model.model_id,model.revision); +} +SolveResult solve_native_race(const Model& model,const NativeRaceOptions& options) { + return native_race_entry(model,options,model.id(),model.revision()); +} + +}} diff --git a/gecode/optimize/native.hpp b/gecode/optimize/native.hpp new file mode 100644 index 0000000000..85ffa0f263 --- /dev/null +++ b/gecode/optimize/native.hpp @@ -0,0 +1,112 @@ +/* Optional bridge from the sparse integer model to native Gecode search. */ +#ifndef GECODE_OPTIMIZE_NATIVE_HPP +#define GECODE_OPTIMIZE_NATIVE_HPP + +#include + +namespace Gecode { namespace Optimize { + +BackendCapabilities native_capabilities(); + +/** + * Solve the supported finite integer subset with native Gecode propagation/BAB. + * Original indicators use native reification, not their numerical big-M rows. + * Exact requests use integer arithmetic checks; Certified is unsupported. + * Interrupted results have no global bound. See NATIVE.md for numeric limits. + * This explicit entry point accepts backend Auto or Native. Complete exact + * starts are supported, including deterministic completion of omitted live + * indicator inactivity gates. Unresolved partial starts, multiple workers and + * nonzero random seeds are unsupported. The Model overload includes snapshotting + * in its deadline and translates moved-from model errors to InvalidModel. + */ +SolveResult solve_native(const ModelSnapshot& model, + const SolveOptions& options = {}); +SolveResult solve_native(const Model& model, const SolveOptions& options = {}); + +/** + * Conservatively select a native algorithm from bounded model structure. + * Globals, indicators, nonbinary/tiny/large models, unsupported optional-route + * options and unavailable checked LP retain ordinary native propagation/BAB. + * Eligible binary knapsack models retain their exact DP strengthening. Other + * moderate binary linear models may use checked root/updated LP, verified + * covers and bounded binary reliability branching. No problem-family metadata, + * reference objective or external start is inferred. Bounded exact presolve, + * independent-component solving and duplicate-column symmetry are automatic + * for compatible ordinary integer models without supplied starts. Actual backend identity + * and a policy explanation are returned in the ordinary SolveResult. + * Selection, compilation, search, validation and Model snapshotting share the + * solve deadline. Explicit solve_native/LP/search entry points are unchanged. + */ +SolveResult solve_native_auto(const ModelSnapshot& model, + const SolveOptions& options = {}); +SolveResult solve_native_auto(const Model& model, + const SolveOptions& options = {}); + +/** Optional automatic transformations. Disabling a feature skips its work; + * enabling it permits the conservative structural policy to use it when safe. + * These controls apply throughout reduced models and independent components. + * They do not change the behavior of explicit solve_native/LP/search APIs. + */ +struct NativeAutoSettings { + /** Exact reductions, including one safely reconstructable affine objective + * auxiliary (for example a MiniZinc compiler-generated objective variable). + */ + bool presolve = true; + bool components = true; + bool symmetry = true; + bool knapsack = true; +}; + +struct NativeAutoOptions { + SolveOptions solve; + NativeAutoSettings settings; +}; + +/** Run the automatic policy with explicit transformation controls. A separate + * name preserves unambiguous existing calls such as solve_native_auto(model,{}). + */ +SolveResult solve_native_auto_configured(const ModelSnapshot& model, + const NativeAutoOptions& options = {}); +SolveResult solve_native_auto_configured(const Model& model, + const NativeAutoOptions& options = {}); + +/** Opt-in sequential probe-and-select policy for native Gecode optimization. + * Racing can INCREASE total CPU work and solve time: probes and restarting the + * selected strategy repeat work. Several seconds (or longer when configured) + * can nevertheless discover a substantially better search route. Early progress + * is a heuristic, not a prediction or a guarantee of eventual speedup. + * The optional Optimize MiniZinc/FlatZinc frontend exposes this policy separately. + */ +struct NativeRaceOptions { + SolveOptions solve; + /** Total nominal exploration time, shared equally by two sequential probes. + * Finite solve limits reserve at least 75% for the selected restart. Zero + * disables exploration and runs solve_native_auto directly. Cooperative + * propagation/LP calls can overrun a probe's local deadline. + */ + double exploration_seconds = 2.0; + /** Per-probe node allowance; all probe/restart nodes also count globally. */ + std::uint64_t probe_node_limit = 4096; + /** Controls only the automatic candidate, including a selected restart or + * direct-path fallback. The ordinary native comparator retains its existing + * eligible exact knapsack DP, independently of these settings. + */ + NativeAutoSettings automatic; + void validate() const; +}; + +/** Compare automatic preprocessing/LP policy with ordinary native BAB, preserving + * eligible exact knapsack DP by default in both. Return immediately on a complete proof; + * otherwise restart the candidate with the best original validated incumbent, + * then strongest valid bound, preferring automatic policy on ties. Validated + * incumbents and original-model bounds from probes are retained. Search trees + * are not resumed. All phases share one wall/node/cancellation budget. + * Globals, indicators and incompatible options retain the automatic direct path. + */ +SolveResult solve_native_race(const ModelSnapshot& model, + const NativeRaceOptions& options = {}); +SolveResult solve_native_race(const Model& model, + const NativeRaceOptions& options = {}); + +}} +#endif diff --git a/gecode/optimize/native_components.cpp b/gecode/optimize/native_components.cpp new file mode 100644 index 0000000000..85fcf56cd4 --- /dev/null +++ b/gecode/optimize/native_components.cpp @@ -0,0 +1,149 @@ +/* Exact additive decomposition; components share the original solve budget. */ +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { namespace Detail { +namespace { +bool integral(double value) { return std::isfinite(value) && std::floor(value)==value; } +struct Disjoint { + std::vector parent, size; + explicit Disjoint(std::size_t n):parent(n),size(n,1) {std::iota(parent.begin(),parent.end(),0);} + std::size_t root(std::size_t x) { + while(parent[x]!=x) {parent[x]=parent[parent[x]];x=parent[x];}return x; + } + void join(std::size_t a,std::size_t b) { + a=root(a);b=root(b);if(a==b)return; + if(size[a] native_components(const ModelSnapshot& model, + const SolveOptions& options,SolveBudget& budget,const NativeSolveContinuation& continuation) { + // Original native validation/compilation is the caller's admission gate. + // These additional checks keep decomposition finite and its arithmetic exact. + constexpr std::size_t columns_cap=4096, rows_cap=4096, nonzeros_cap=65536, components_cap=64; + if(!options.primal_start.empty() || options.guarantee==Guarantee::Certified || + (options.backend!=Backend::Native && options.backend!=Backend::Auto) || + options.threads!=1 || options.random_seed!=0 || !model.globals.empty() || !model.indicators.empty() || + model.variables.size()>columns_cap || model.rows.size()>rows_cap || + model.objective.terms.size()>nonzeros_cap || !integral(model.objective.offset)) return {}; + auto result=[&](Termination reason,const std::string& message) { + SolveResult out;out.model_id=model.model_id;out.revision=model.revision; + out.backend="Gecode native components";out.guarantee=options.guarantee; + out.termination=reason;out.message=message;out.elapsed_seconds=budget.elapsed_seconds();return out; + }; + auto stopped=[&]() -> std::optional { + if(auto reason=budget.stop_reason())return result(*reason,"Independent components stopped by shared budget"); + return {}; + }; + if(auto stop=stopped())return stop; + std::size_t nonzeros=model.objective.terms.size(),active=0; + for(const auto& v:model.variables) { + if(auto stop=stopped())return stop; + if(!v.active)continue; + ++active; + if(v.indicator_origin || (v.type!=VariableType::Integer && v.type!=VariableType::Binary) || + !integral(v.lower) || !integral(v.upper))return {}; + } + if(active<2)return {}; + for(const auto& t:model.objective.terms)if(!integral(t.coefficient))return {}; + for(const auto& row:model.rows) { + if(auto stop=stopped())return stop; + if(!row.active)continue; + if(row.indicator_origin || row.terms.size()>nonzeros_cap-nonzeros || + (std::isfinite(row.lower) && !integral(row.lower)) || + (std::isfinite(row.upper) && !integral(row.upper)))return {}; + nonzeros+=row.terms.size(); + for(const auto& t:row.terms)if(!integral(t.coefficient))return {}; + } + // The function is internal, but reject corrupt slot references before indexing. + validate_structure(model); + Disjoint sets(model.variables.size()); + for(const auto& row:model.rows)if(row.active && !row.terms.empty()) { + for(const auto& term:row.terms) { + if(auto stop=stopped())return stop; + sets.join(row.terms.front().variable.id,term.variable.id); + } + } + const auto missing=std::numeric_limits::max(); + std::vector component(model.variables.size(),missing),root_component(model.variables.size(),missing); + std::vector> slots; + for(std::size_t i=0;i> row_ids(slots.size()); + std::vector> objective(slots.size()); + for(std::size_t i=0;i assembled(model.variables.size(),std::numeric_limits::quiet_NaN()); + std::vector mapped(model.variables.size()); + std::string backend_version; + for(std::size_t c=0;c terms;terms.reserve(row.terms.size()); + for(const auto& t:row.terms)terms.push_back({mapped[t.variable.id],t.coefficient}); + submodel.add_row(terms,row.lower,row.upper); + } + std::vector terms;terms.reserve(objective[c].size()); + for(const auto& t:objective[c])terms.push_back({mapped[t.variable.id],t.coefficient}); + // The original offset is included only in the final original evaluation. + submodel.set_objective(terms,model.objective.sense,0); + const auto snapshot=submodel.snapshot(); + if(auto stop=stopped())return stop; + const auto solved=continuation(snapshot,options,budget); + if(budget.cancelled() || budget.time_limit_reached())return stopped(); + if(solved.model_id!=snapshot.model_id || solved.revision!=snapshot.revision || + solved.guarantee!=options.guarantee) + return result(Termination::BackendError,"Independent component result identity or guarantee differs"); + if(solved.termination==Termination::Infeasible) + return result(Termination::Infeasible,"An independent component proved the original model infeasible"); + if(solved.termination!=Termination::Optimal) + return result(solved.termination,"Independent component stopped; no complete original incumbent assembled"); + if(!solved.has_solution() || solved.best_bound!=solved.objective || + solved.values.size()!=slots[c].size() || solved.active_variables.size()!=slots[c].size()) + return result(Termination::BackendError,"Incomplete independent component optimum"); + const auto checked=validate(snapshot,solved.values,0,0); + if(!checked.valid || checked.objective!=solved.objective) + return result(Termination::BackendError,"Independent component witness failed exact validation"); + for(auto slot:slots[c])assembled[slot]=solved.values[mapped[slot].id]; + if(backend_version.empty())backend_version=solved.backend_version; + } + const auto checked=validate(model,assembled,0,0); + if(budget.cancelled() || budget.time_limit_reached())return stopped(); + if(!checked.valid || !checked.objective) + return result(Termination::BackendError,"Assembled original witness failed exact validation"); + auto out=result(Termination::Optimal,"Exact independent components: "+std::to_string(slots.size())); + out.backend_version=std::move(backend_version);out.values=std::move(assembled); + for(const auto& v:model.variables)out.active_variables.push_back(v.active); + out.objective=checked.objective;out.best_bound=checked.objective; + out.solution_validated=true;out.update_gaps(model.objective.sense); + if(budget.cancelled() || budget.time_limit_reached())return stopped(); + out.elapsed_seconds=budget.elapsed_seconds();return out; +} + +}}} diff --git a/gecode/optimize/native_lp.hpp b/gecode/optimize/native_lp.hpp new file mode 100644 index 0000000000..081fa07c43 --- /dev/null +++ b/gecode/optimize/native_lp.hpp @@ -0,0 +1,97 @@ +/* Explicit native search with independently checked integer LP deductions. */ +#ifndef GECODE_OPTIMIZE_NATIVE_LP_HPP +#define GECODE_OPTIMIZE_NATIVE_LP_HPP + +#include + +namespace Gecode { namespace Optimize { + +enum class NativeLpFrequency { Root, AfterBoundChanges }; + +/** Optional root strengthening limits, independent of the native search limits. + * Zero work/round/storage caps are valid and preserve the original solve. + */ +struct NativeRootCoverSettings { + std::size_t max_rounds = 4, max_work = 4000000; + std::size_t max_cuts = 64, max_cut_nonzeros = 4096; + std::size_t max_model_columns = 200000, max_model_rows = 200000; + std::size_t max_model_nonzeros = 2000000; + std::size_t max_separation_rows = 256, max_terms_per_row = 512; + unsigned int denominator = 1048576; + void validate() const; +}; + +enum class NativeRootCoverCompletion { + NotRequested, NotStarted, NoNewCuts, RoundLimit, WorkLimit, StorageLimit, + SeparationLimit, Cancelled, TimeLimit, NoPrimalSuggestion, InvalidSuggestion, + CallbackError, BackendError, AllocationFailure +}; + +struct NativeRootCoverStatistics { + bool requested = false; + NativeRootCoverCompletion completion = NativeRootCoverCompletion::NotRequested; + std::size_t rounds = 0, augmentations = 0, cuts = 0, nonzeros = 0, work = 0; + std::size_t projected_coordinates = 0, unsupported_rows = 0, oversized_rows = 0; + std::size_t separated_cuts = 0, duplicate_cuts = 0, arithmetic_rejections = 0; + // Root-loop totals, including intermediate backend instances. Already included + // in the enclosing NativeLpStatistics totals, not additional calls to add. + std::uint64_t lp_calls = 0, valid_bounds = 0, rejected_bounds = 0; + std::uint64_t numerical_infeasibility_reports = 0; + double lp_seconds = 0; +}; + +struct NativeLpSettings { + NativeLpFrequency frequency = NativeLpFrequency::Root; + bool bound_tightening = true; + /** Reoptimize after this many observed interval changes; always try at root. */ + unsigned int bound_change_interval = 1; + /** Absent by default. Adds verified global covers of original ordinary rows. */ + std::optional root_cover_cuts; + void validate() const; +}; + +struct NativeLpOptions : NativeLpSettings { + SolveOptions solve; + void validate() const; +}; + +struct NativeLpStatistics { + std::uint64_t lp_calls = 0; + double lp_seconds = 0; + std::uint64_t valid_bounds = 0; + std::uint64_t rejected_bounds = 0; + /** Diagnostic LP reports, never sufficient to prune native search. */ + std::uint64_t numerical_infeasibility_reports = 0; + std::uint64_t certificate_evaluations = 0; + std::uint64_t conditional_checks = 0; + std::uint64_t variable_fixings = 0; + std::uint64_t variable_bound_tightenings = 0; + NativeRootCoverStatistics root_cover; +}; + +struct NativeLpResult { + SolveResult result; + NativeLpStatistics relaxation; +}; + +BackendCapabilities native_lp_capabilities(); + +/** + * Explicit opt-in: ordinary solve()/Auto/Native defaults are unchanged. + * Native propagation enforces every original constraint. Only ordinary linear + * rows enter the sparse relaxation; semivariables use their convex-hull box. + * LP bounds and interval cuts require checked integer certificates. This is + * exact discrete solving, not a complete independently checkable solve proof. + * Missing native/HiGHS/checked-wide-integer support is Unsupported, never a + * fallback. Original identities, limits and native arithmetic guards apply. + * Deadlines are cooperative: an LP attempt has a 0.2-second/10,000-iteration + * backend limit, but neither that limit nor native propagation is preemptive. + * See NATIVE-LP.md for scope, scheduling and interrupted-bound semantics. + */ +NativeLpResult solve_native_lp(const ModelSnapshot& model, + const NativeLpOptions& options = {}); +NativeLpResult solve_native_lp(const Model& model, + const NativeLpOptions& options = {}); + +}} +#endif diff --git a/gecode/optimize/native_neighborhoods.hpp b/gecode/optimize/native_neighborhoods.hpp new file mode 100644 index 0000000000..065379e286 --- /dev/null +++ b/gecode/optimize/native_neighborhoods.hpp @@ -0,0 +1,81 @@ +/* Explicit bounded primal neighborhoods alongside the native proof frontier. */ +#ifndef GECODE_OPTIMIZE_NATIVE_NEIGHBORHOODS_HPP +#define GECODE_OPTIMIZE_NATIVE_NEIGHBORHOODS_HPP + +#include + +namespace Gecode { namespace Optimize { + +enum class NativeNeighborhoodPolicy { BinaryHamming }; + +/** Limits apply to one optional attempt, sharing the ordinary solve budget. + * Finite provisional settings for this explicit experimental API. Zero caps + * skip optional work; no existing solve route enables neighborhoods implicitly. + */ +struct NativeNeighborhoodSettings { + NativeNeighborhoodPolicy policy = NativeNeighborhoodPolicy::BinaryHamming; + std::size_t radius = 1; + std::uint64_t max_status_calls = 128; + std::size_t max_distance_variables = 4096; + std::size_t max_source_entries = 100000; + std::size_t max_coordinator_work = 100000; + std::size_t max_local_spaces = 64; + double time_limit_seconds = 0.05; + void validate() const; +}; + +enum class NativeNeighborhoodCompletion { + NotStarted, NoIncumbent, ProofCompletedBeforeAttempt, NoEligibleBinary, + NonrestrictingRadius, FormulationLimit, SourceLimit, WorkLimit, StatusLimit, + SharedNodeReserve, LocalStorageLimit, LocalTimeLimit, NoImprovement, Improved, + GlobalStop, Error +}; + +struct NativeNeighborhoodStatistics { + bool requested = false; + NativeNeighborhoodCompletion completion = NativeNeighborhoodCompletion::NotStarted; + std::optional stop_reason; + std::uint64_t attempts = 0; + std::size_t eligible_variables = 0, source_entries = 0, coordinator_work = 0; + // An admitted status attempt can be stopped before propagation actually runs. + std::uint64_t status_attempts = 0, completed_status_calls = 0; + std::uint64_t failed_nodes = 0, feasible_candidates = 0, accepted_improvements = 0; + std::size_t peak_local_spaces = 0, peak_total_spaces = 0; + // Shared total, not another component to add to frontier/probe/local counts. + std::uint64_t budget_nodes = 0; + // Includes eligibility, construction, checking, and release; no local LP work. + double elapsed_seconds = 0; +}; + +struct NativeNeighborhoodOptions { + NativeSearchOptions search; + NativeNeighborhoodSettings neighborhood; + void validate() const; +}; + +struct NativeNeighborhoodResult { + NativeSearchResult search; + NativeNeighborhoodStatistics neighborhood; +}; + +/** Run at most one global BinaryHamming improvement attempt after a checked + * incumbent, at a stable main-parent boundary. Distance counts active nonfixed + * original Binary slots without indicator_origin; all other original variables + * and native constraints remain in the isolated subproblem. There is no partial + * start repair, current-node LP suggestion, local proof sharing or recursive + * heuristic. Only a timely exact original-model-validated improvement is shared. + * + * Shared node attempts = ordinary frontier admissions + reliability probe + * attempts + neighborhood status attempts. Optional admissions reserve two ordinary + * child slots. Local caps end only the heuristic; global limits stop the solve. + * Main queued/active regions continue to define any interrupted global bound. + * Native propagation/posting/checking is cooperative, not hard-preemptible. + * Existing NativeSearch option/result layouts and existing APIs are unchanged. + */ +NativeNeighborhoodResult solve_native_neighborhoods( + const ModelSnapshot&, const NativeNeighborhoodOptions& = {}); +NativeNeighborhoodResult solve_native_neighborhoods( + const Model&, const NativeNeighborhoodOptions& = {}); + +}} +#endif diff --git a/gecode/optimize/native_preprocess.hpp b/gecode/optimize/native_preprocess.hpp new file mode 100644 index 0000000000..f2a9aa8562 --- /dev/null +++ b/gecode/optimize/native_preprocess.hpp @@ -0,0 +1,20 @@ +/* Internal bounded, exact transformations in the automatic native pipeline. */ +#ifndef GECODE_OPTIMIZE_NATIVE_PREPROCESS_HPP +#define GECODE_OPTIMIZE_NATIVE_PREPROCESS_HPP +#include +#include +#include +namespace Gecode { namespace Optimize { namespace Detail { +using NativeSolveContinuation = std::function; +std::optional native_presolve(const ModelSnapshot&, const SolveOptions&, + SolveBudget&, const NativeSolveContinuation&); +/** Eliminate one singleton affine objective auxiliary after native admission. */ +std::optional native_objective_auxiliary(const ModelSnapshot&, const SolveOptions&, + SolveBudget&, const NativeSolveContinuation&); +std::optional native_components(const ModelSnapshot&, const SolveOptions&, + SolveBudget&, const NativeSolveContinuation&); +std::optional native_symmetry(const ModelSnapshot&, const SolveOptions&, + SolveBudget&, const NativeSolveContinuation&); +}}} +#endif diff --git a/gecode/optimize/native_presolve.cpp b/gecode/optimize/native_presolve.cpp new file mode 100644 index 0000000000..f322c86036 --- /dev/null +++ b/gecode/optimize/native_presolve.cpp @@ -0,0 +1,280 @@ +/* Exact, bounded presolve composition for the automatic native coordinator. */ +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { namespace Detail { +namespace { +std::optional deadline_stop(const SolveBudget& budget) { + if (budget.cancelled()) return Termination::Cancelled; + if (budget.time_limit_reached()) return Termination::TimeLimit; + return {}; +} + +SolveResult original_result(const ModelSnapshot& model, const SolveOptions& options) { + SolveResult result; + result.model_id=model.model_id; result.revision=model.revision; + result.guarantee=options.guarantee; + result.backend="Gecode native exact presolve"; + return result; +} +} + +std::optional native_objective_auxiliary(const ModelSnapshot& model, + const SolveOptions& options, SolveBudget& budget, + const NativeSolveContinuation& continuation) { + // A single exact equality can hide a binary objective behind a nonbinary + // FlatZinc auxiliary. This is one bounded substitution, never recursive + // algebra or an assumption that a name/annotation implies equivalence. + if (!options.primal_start.empty() || options.guarantee==Guarantee::Certified || + (options.backend!=Backend::Auto && options.backend!=Backend::Native) || + options.threads!=1 || options.random_seed!=0 || + !model.globals.empty() || !model.indicators.empty() || + model.variables.size()>4096 || model.rows.size()>4096 || + model.objective.terms.size()!=1 || model.objective.terms[0].coefficient!=1) return {}; + auto result=original_result(model,options); + const auto finish=[&](SolveResult value) -> std::optional { + if(const auto stop=deadline_stop(budget)) { + value=original_result(model,options);value.termination=*stop; + value.message="Exact objective auxiliary substitution stopped before original-result publication"; + } + value.elapsed_seconds=budget.elapsed_seconds();return value; + }; + try { + if(const auto stop=budget.stop_reason()) {result.termination=*stop;return finish(std::move(result));} + validate_structure(model); + const auto auxiliary=model.objective.terms[0].variable.id; + const auto& variable=model.variables[auxiliary]; + if(variable.type!=VariableType::Integer || + (variable.lower>=0 && variable.upper<=1)) return {}; + // Independently bound every conversion/product below, even though callers + // have already compiled the original model under stricter native limits. + constexpr std::int64_t limit=INT64_C(2147483647), exact=INT64_C(9007199254740992); + const auto integer=[](double value) { + return std::isfinite(value) && std::trunc(value)==value && std::abs(value)<=2147483647.0; + }; + for(const auto& v:model.variables) if(v.active && + ((v.type!=VariableType::Integer && v.type!=VariableType::Binary) || + !integer(v.lower) || !integer(v.upper))) return {}; + std::optional equality; + double sign=0;std::size_t nonzeros=0; + for(std::size_t i=0;i65536-nonzeros)return {};nonzeros+=row.terms.size(); + if(!row.active)continue; + for(const auto& term:row.terms) if(term.variable.id==auxiliary) { + if(equality || row.lower!=row.upper || !integer(row.lower) || + std::abs(term.coefficient)!=1)return {}; + equality=i;sign=term.coefficient; + } + } + if(!equality)return {}; + const auto& equation=model.rows[*equality]; + const auto constant=static_cast(sign*equation.lower); + std::int64_t lower=constant,upper=constant,magnitude=0; + std::vector terms; + for(const auto& term:equation.terms) if(term.variable.id!=auxiliary) { + if(!integer(term.coefficient))return {}; + const auto coefficient=static_cast(-sign*term.coefficient); + const auto& v=model.variables[term.variable.id]; + const auto a=coefficient*static_cast(v.lower); + const auto b=coefficient*static_cast(v.upper); + const auto size=std::max(std::abs(a),std::abs(b)); + if(size>limit-magnitude)return {};magnitude+=size; + lower+=std::min(a,b);upper+=std::max(a,b); + terms.push_back({term.variable,static_cast(coefficient)}); + } + if(!std::isfinite(model.objective.offset) || std::trunc(model.objective.offset)!=model.objective.offset || + std::abs(model.objective.offset)>static_cast(exact))return {}; + const auto offset=static_cast(model.objective.offset); + if(std::abs(offset+constant)>exact || std::abs(offset+lower)>exact || std::abs(offset+upper)>exact)return {}; + auto reduced=model;reduced.variables[auxiliary].active=false; + reduced.objective.terms=terms;reduced.objective.offset=static_cast(offset+constant); + auto& row=reduced.rows[*equality]; + row.terms=terms;row.lower=variable.lower-constant;row.upper=variable.upper-constant; + // Auxiliary domain bounds are real constraints. Remove the defining row + // only when the remaining variable domain box independently implies both. + if(lower>=variable.lower && upper<=variable.upper){row.active=false;row.terms.clear();} + validate_structure(reduced); + auto exact_options=options;exact_options.guarantee=Guarantee::Exact; + auto solved=continuation(reduced,exact_options,budget); + if(const auto stop=deadline_stop(budget)){result.termination=*stop;return finish(std::move(result));} + if(solved.model_id!=reduced.model_id || solved.revision!=reduced.revision || + solved.guarantee!=Guarantee::Exact || solved.start_submitted) + throw std::runtime_error("Objective auxiliary solve returned foreign identity, guarantee or start"); + if(solved.termination==Termination::Unsupported)return {}; // Preserve original numeric admission. + if(solved.termination==Termination::InvalidModel || solved.termination==Termination::Unbounded || + solved.termination==Termination::InfeasibleOrUnbounded || + (solved.best_bound && !std::isfinite(*solved.best_bound))) + throw std::runtime_error("Objective auxiliary solve returned inconsistent status or bound"); + if(solved.has_solution()) { + if(solved.values.size()!=model.variables.size() || solved.active_variables.size()!=model.variables.size()) + throw std::runtime_error("Objective auxiliary solve returned incomplete coordinates"); + for(std::size_t i=0;i(term.coefficient)* + static_cast(solved.values[term.variable.id]); + solved.values[auxiliary]=static_cast(value);solved.active_variables[auxiliary]=true; + const auto restored=validate(model,solved.values,0,0); + if(!restored.valid || restored.objective!=solved.objective) + throw std::runtime_error("Objective auxiliary original witness failed exact validation"); + solved.solution_validated=true; + } + if((solved.termination==Termination::Optimal && !solved.has_solution()) || + (solved.termination==Termination::Infeasible && solved.has_solution())) + throw std::runtime_error("Objective auxiliary solve returned inconsistent proof status"); + // Full objectives (including the constant) are equal, so original bounds + // transfer unchanged. No transformed point/proof is published before QA. + solved.guarantee=options.guarantee;solved.update_gaps(model.objective.sense); + solved.message="Exact objective auxiliary substitution; "+solved.message; + return finish(std::move(solved)); + } catch(const std::bad_alloc&) { + result=original_result(model,options);result.termination=Termination::MemoryLimit; + result.message="Exact objective auxiliary substitution allocation failed"; + } catch(const std::exception& error) { + result=original_result(model,options);result.termination=Termination::BackendError; + result.message=std::string("Exact objective auxiliary substitution: ")+error.what(); + } + return finish(std::move(result)); +} + +std::optional native_presolve(const ModelSnapshot& model, + const SolveOptions& options, SolveBudget& budget, + const NativeSolveContinuation& continuation) { + // A previously supplied original start must remain available even when the + // global deadline leaves no time for reconstruction. The existing native + // start path owns that publication contract, so composition skips starts. + if (!options.primal_start.empty() || options.guarantee==Guarantee::Certified || + (options.backend!=Backend::Auto && options.backend!=Backend::Native) || + options.threads!=1 || options.random_seed!=0 || + !model.globals.empty() || !model.indicators.empty()) return {}; + constexpr std::size_t max_columns=4096, max_rows=4096, max_nonzeros=65536; + if (model.variables.size()>max_columns || model.rows.size()>max_rows || + model.objective.terms.size()>max_nonzeros) return {}; + + auto result=original_result(model,options); + const auto finish=[&](SolveResult value) -> std::optional { + // A reduced incumbent is not an original incumbent until exact postsolve + // finishes. Never publish a newly reconstructed point or proof too late. + // Node quotas intentionally do not enter this check: the final admitted + // node may complete, and its timely result remains valid at the quota. + if (const auto stop=deadline_stop(budget)) { + auto stopped=original_result(model,options); + stopped.backend=std::move(value.backend); + stopped.backend_version=std::move(value.backend_version); + stopped.termination=*stop; + stopped.message="Exact integer presolve composition stopped before original-result publication"; + value=std::move(stopped); + } + value.elapsed_seconds=budget.elapsed_seconds(); + return value; + }; + const auto stopped=[&](Termination reason) { + result.termination=reason; + result.message="Exact integer presolve composition budget stopped"; + return finish(std::move(result)); + }; + try { + if (const auto reason=budget.stop_reason()) return stopped(*reason); + for (const auto& variable:model.variables) { + if (const auto reason=deadline_stop(budget)) return stopped(*reason); + if (variable.active && variable.type!=VariableType::Integer && + variable.type!=VariableType::Binary) return {}; + } + std::size_t nonzeros=model.objective.terms.size(); + for (const auto& row:model.rows) { + if (const auto reason=deadline_stop(budget)) return stopped(*reason); + if (row.terms.size()>max_nonzeros-nonzeros) return {}; + nonzeros+=row.terms.size(); + } + PresolveOptions settings; + settings.time_limit_seconds=budget.remaining_seconds(); + settings.cancellation=budget.cancellation(); + settings.max_passes=4; settings.max_row_visits=65536; + const auto prepared=presolve_integer(model,settings); + if (const auto reason=deadline_stop(budget)) return stopped(*reason); + if (prepared.model_id!=model.model_id || prepared.revision!=model.revision || + prepared.guarantee!=Guarantee::Exact) + throw std::runtime_error("Exact presolve returned foreign identity or guarantee"); + if (prepared.status==PresolveStatus::Infeasible && + prepared.termination==Termination::Infeasible) { + result.termination=Termination::Infeasible; + result.message="Exact integer presolve proved an original-model contradiction"; + return finish(std::move(result)); + } + // Only finalized artifacts carry the equivalence contract. Work-limited + // artifacts are valid; a deadline-interrupted partial working state is not. + if (!prepared.model || + !((prepared.status==PresolveStatus::Fixpoint && prepared.termination==Termination::Optimal) || + (prepared.status==PresolveStatus::Incomplete && prepared.termination==Termination::IterationLimit))) + return {}; + if (!prepared.fixed_variables && !prepared.removed_rows && prepared.changes.empty()) return {}; + const auto& artifact=*prepared.model; + if (artifact.original().model_id!=model.model_id || artifact.original().revision!=model.revision) + throw std::runtime_error("Exact presolve artifact has foreign original identity"); + const auto& reduced=artifact.reduced(); + auto exact_options=options; + exact_options.guarantee=Guarantee::Exact; + exact_options.time_limit_seconds=budget.remaining_seconds(); + exact_options.cancellation=budget.cancellation(); + auto solved=continuation(reduced,exact_options,budget); + result.backend=solved.backend; result.backend_version=solved.backend_version; + if (const auto reason=deadline_stop(budget)) return stopped(*reason); + if (solved.model_id!=reduced.model_id || solved.revision!=reduced.revision || + solved.guarantee!=Guarantee::Exact || solved.start_submitted) + throw std::runtime_error("Reduced native solve returned inconsistent identity, guarantee or start"); + // Reduction can produce row sides outside a narrower optional/native + // compiler's admission range. Preserve the original route's acceptance. + if (solved.termination==Termination::Unsupported) return {}; + if (solved.termination==Termination::InvalidModel) + throw std::runtime_error("Reduced native model was rejected as invalid"); + if (solved.has_solution()) { + auto restored=artifact.postsolve(solved,0); + if (!restored.exact_witness_validated || !restored.solution.has_solution() || + restored.solution.objective!=solved.objective) + throw std::runtime_error("Reduced native witness failed exact original postsolve: "+restored.solution.message); + result=std::move(restored.solution); + result.guarantee=options.guarantee; + } + if ((solved.termination==Termination::Optimal && !result.has_solution()) || + (solved.termination==Termination::Infeasible && result.has_solution()) || + solved.termination==Termination::Unbounded || solved.termination==Termination::InfeasibleOrUnbounded) + throw std::runtime_error("Reduced finite native solve returned inconsistent proof status"); + // PresolveBuilder folds fixed costs into the reduced objective offset; + // original and reduced FULL objective values are identical, including for + // maximization. Bounds therefore transfer unchanged, never with a second + // offset correction. Only the actual Exact native result supplies a proof; + // PresolveStatus::Fixpoint's Optimal termination is not used here. + result.termination=solved.termination; + if (solved.best_bound && !std::isfinite(*solved.best_bound)) + throw std::runtime_error("Reduced native solve returned a nonfinite bound"); + if (solved.termination!=Termination::Infeasible) result.best_bound=solved.best_bound; + if (solved.termination==Termination::Optimal) result.best_bound=result.objective; + result.update_gaps(model.objective.sense); + result.message=std::string("Exact integer presolve (")+ + (prepared.status==PresolveStatus::Fixpoint ? "fixpoint reduction" : "partial reduction")+ + "): "+solved.message; + return finish(std::move(result)); + } catch (const std::bad_alloc&) { + result=original_result(model,options); result.termination=Termination::MemoryLimit; + result.message="Exact integer presolve composition allocation failed"; + } catch (const std::exception& error) { + result=original_result(model,options); result.termination=Termination::BackendError; + result.message=std::string("Exact integer presolve composition: ")+error.what(); + } + return finish(std::move(result)); +} + +}}} diff --git a/gecode/optimize/native_regular_limits.hpp b/gecode/optimize/native_regular_limits.hpp new file mode 100644 index 0000000000..ae8b6dddf6 --- /dev/null +++ b/gecode/optimize/native_regular_limits.hpp @@ -0,0 +1,29 @@ +// Private native storage admission, shared only with its arithmetic tests. +#ifndef GECODE_OPTIMIZE_NATIVE_REGULAR_LIMITS_HPP +#define GECODE_OPTIMIZE_NATIVE_REGULAR_LIMITS_HPP +#include +#include +namespace Gecode { namespace Optimize { namespace Detail { +inline const char* native_regular_size_error(std::uint64_t words,std::uint64_t states, + std::uint64_t transitions,std::uint64_t symbols, + bool short_symbols) noexcept { + constexpr auto imax=static_cast(std::numeric_limits::max()); + constexpr auto umax=static_cast(std::numeric_limits::max()); + // DFA init and layer allocation both need signed-int count+1 sentinels. + if(words>=imax||!states||states>=imax||transitions>=imax) + return "Native regular word/state/transition count exceeds signed array limits"; + // LayeredGraph::initialize initializes int(max_states)*(word_length+1). + if(states>imax/(words+1)) + return "Native regular layer-state product exceeds signed integer limits"; + if(words&&transitions>umax/words) + return "Native regular layer-edge total exceeds unsigned integer limits"; + // DFA::fill allocates 1<=((imax/2)+1)) + return "Native regular alphabet hash exceeds positive signed power-of-two limits"; + // The short-valued graph stores its per-layer support count in ushort. + if(short_symbols&&symbols>std::numeric_limits::max()) + return "Native regular short-symbol support count exceeds native storage"; + return nullptr; +} +}}} +#endif diff --git a/gecode/optimize/native_search.hpp b/gecode/optimize/native_search.hpp new file mode 100644 index 0000000000..110e8775ce --- /dev/null +++ b/gecode/optimize/native_search.hpp @@ -0,0 +1,101 @@ +/* Explicit bounded native frontier search with conservative global bounds. */ +#ifndef GECODE_OPTIMIZE_NATIVE_SEARCH_HPP +#define GECODE_OPTIMIZE_NATIVE_SEARCH_HPP + +#include + +namespace Gecode { namespace Optimize { + +enum class NativeSearchOrder { DepthFirst, BestBound }; + +enum class NativeBranchingPolicy { BinaryReliability }; + +/** Experimental binary directional propagation gains; not LP pseudocosts. + * Resource caps may be zero to skip optional work. The sample threshold must + * be positive. Only completed finite probe pairs update solve-local history. + */ +struct NativeBranchingSettings { + NativeBranchingPolicy policy = NativeBranchingPolicy::BinaryReliability; + std::size_t max_candidates_per_decision = 8; + std::uint64_t max_probe_status_calls = 128; + std::uint64_t max_probe_status_calls_per_decision = 8; + std::size_t max_branching_work = 100000; + std::uint64_t reliability_samples = 2; + std::size_t max_nonimproving_pairs = 2; + std::size_t max_history_entries = 4096; + void validate() const; +}; + +struct NativeBranchingStatistics { + bool requested = false; + std::uint64_t decisions = 0, manual_splits = 0, fallback_decisions = 0; + // Admitted attempts: a final pre-status stop can consume a slot without + // executing propagation, just as for ordinary frontier admissions. + std::uint64_t probe_status_calls = 0, completed_pairs = 0; + std::uint64_t published_pairs = 0, finite_samples = 0, zero_gain_samples = 0; + std::uint64_t failed_directions = 0, reliable_candidates = 0; + std::uint64_t probe_propagations = 0; + // Shared total: ordinary admissions plus probe status attempts. The explicit + // neighborhood wrapper additionally charges its local status attempts here. + // This repeats SolveBudget::nodes(), not another component to add to it. + std::uint64_t budget_nodes = 0; + std::size_t work = 0, history_entries = 0; + // Subsets of the solve-wide relaxation totals, not additional calls to add. + std::uint64_t probe_lp_calls = 0; + double probe_lp_seconds = 0; +}; + +struct NativeSearchOptions { + SolveOptions solve; + NativeSearchOrder order = NativeSearchOrder::BestBound; + // Counts queued spaces + expanding parent + in-flight child, not bytes. + std::size_t max_open_nodes = 100000; + // Absent means native propagation only, with no LP dependency or attempt. + std::optional relaxation; + // Absent preserves the existing native brancher and admission accounting. + std::optional branching; + void validate() const; +}; + +struct NativeFrontierStatistics { + std::uint64_t admitted_nodes = 0; + std::uint64_t expanded_nodes = 0; + std::uint64_t failed_nodes = 0; + std::uint64_t bound_pruned_nodes = 0; + std::uint64_t feasible_leaves = 0; + std::size_t peak_open_nodes = 0; + // Queued nodes plus any parent/root whose region remains unresolved. + std::size_t unresolved_regions = 0; +}; + +struct NativeSearchResult { + SolveResult result; + NativeLpStatistics relaxation; + NativeFrontierStatistics frontier; + NativeBranchingStatistics branching; +}; + +/** + * Explicit opt-in; solve(), solve_native() and solve_native_lp() are unchanged. + * Existing finite native integer/global/indicator semantics and limits apply. + * Stable propagated nodes use normalized minimization bounds. Every unfinished + * region remains represented, including a parent during partial expansion. + * Interrupted global bounds aggregate all unresolved regions and an incumbent; + * they are absent if compilation did not establish the initial objective box. + * max_open_nodes exhaustion returns MemoryLimit; this is not a byte guarantee. + * Node quotas restrict admission, allowing the final admitted node to finish + * when time/cancellation permits. Complete exact starts are supported; no + * partial starts, parallel workers or gap stopping. + * An optional relaxation requires native checked LP capability explicitly. + * BinaryReliability charges probe status attempts to the shared node quota, + * separately reported from admitted frontier nodes. Probes select a split; + * they never publish incumbents or pruning evidence. Limits are cooperative + * around native propagation, which itself runs to fixpoint or failure. + */ +NativeSearchResult solve_native_search(const ModelSnapshot& model, + const NativeSearchOptions& options = {}); +NativeSearchResult solve_native_search(const Model& model, + const NativeSearchOptions& options = {}); + +}} +#endif diff --git a/gecode/optimize/native_symmetry.cpp b/gecode/optimize/native_symmetry.cpp new file mode 100644 index 0000000000..fc9730c087 --- /dev/null +++ b/gecode/optimize/native_symmetry.cpp @@ -0,0 +1,82 @@ +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { namespace Detail { +std::optional native_symmetry(const ModelSnapshot& model, + const SolveOptions& options, SolveBudget& budget, + const NativeSolveContinuation& continuation) { + // Exact duplicate columns only: arbitrary permutation of each group must + // preserve every original row, domain, variable type and objective coefficient. + if (!options.primal_start.empty() || !model.globals.empty() || !model.indicators.empty() || + model.variables.size()>4096 || model.rows.size()>4096 || model.objective.terms.size()>65536) + return {}; + const auto stopped=[&]{return budget.cancelled() || budget.time_limit_reached();}; + if(stopped())return {}; + std::size_t nonzeros=model.objective.terms.size(); + for(const auto& row:model.rows){ + if(row.terms.size()>65536-nonzeros)return {}; + nonzeros+=row.terms.size(); + } + for(const auto& v:model.variables)if(v.active && + (v.type!=VariableType::Integer && v.type!=VariableType::Binary))return {}; + validate_structure(model); + using Column=std::vector>; + std::vector columns(model.variables.size()); + std::vector costs(model.variables.size(),0); + for(const auto& t:model.objective.terms)costs[t.variable.id]=t.coefficient; + for(std::size_t i=0;i; + std::map last; + std::vector> order; + for(std::size_t i=0;i500000000)continue; + Key key{v.type,v.lower,v.upper,costs[i],std::move(columns[i])}; + auto added=last.emplace(std::move(key),i); + if(!added.second){order.push_back({added.first->second,i});added.first->second=i;} + } + if(order.empty())return {}; + auto reduced=model; // Private derived snapshot; original handles remain stable. + for(auto pair:order){ + if(stopped())return {}; + RowData row;row.constraint={model.model_id,reduced.rows.size()}; + row.terms={{model.variables[pair.first].variable,1},{model.variables[pair.second].variable,-1}}; + row.upper=0;row.name="automatic interchangeable-column order"; + reduced.rows.push_back(std::move(row)); + } + auto result=continuation(reduced,options,budget); + if(result.has_solution()){ + // The outer native admission bounds all integer row/objective activities; + // tolerance-zero original evaluation is exact within that admitted range. + const auto checked=validate(model,result.values,0,0); + if(!checked.valid || checked.objective!=result.objective){ + SolveResult failed;failed.model_id=model.model_id;failed.revision=model.revision; + failed.backend=result.backend;failed.guarantee=options.guarantee; + failed.termination=Termination::BackendError; + failed.message="Automatic symmetry original-model validation failed"; + return failed; + } + result.solution_validated=true; + } + if(stopped()){ + const auto why=budget.cancelled()?Termination::Cancelled:Termination::TimeLimit; + const auto backend=result.backend; + result=SolveResult{};result.model_id=model.model_id;result.revision=model.revision; + result.backend=backend;result.guarantee=options.guarantee;result.termination=why; + } + result.message="Automatic duplicate-column symmetry: "+std::to_string(order.size())+ + " ordering constraints; "+result.message; + result.elapsed_seconds=budget.elapsed_seconds(); + return result; +} +}}} diff --git a/gecode/optimize/pool.cpp b/gecode/optimize/pool.cpp new file mode 100644 index 0000000000..9def2802a1 --- /dev/null +++ b/gecode/optimize/pool.cpp @@ -0,0 +1,311 @@ +#include +#include + +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { +namespace { +constexpr std::int64_t exact_limit = INT64_C(9007199254740992); +class UnsupportedPool : public std::runtime_error { using std::runtime_error::runtime_error; }; +class InvalidPool : public std::runtime_error { using std::runtime_error::runtime_error; }; +struct Interrupted { Termination reason; }; +void checkpoint(const SolveBudget& budget) { + if (auto reason = budget.stop_reason()) throw Interrupted{*reason}; +} +struct Domain { Variable variable; std::int64_t lower, upper; }; + +std::vector projection(const ModelSnapshot& source, const PoolOptions& options) { + for (const auto& indicator : source.indicators) + if (indicator.active) throw UnsupportedPool("Pools with active indicators are not yet supported"); + for (const auto& global : source.globals) + if (global.active) throw UnsupportedPool("Pools with active globals are not yet supported"); + std::size_t active = 0; + std::vector variables; + for (const auto& variable : source.variables) if (variable.active) { + ++active; + if (variable.type == VariableType::SemiContinuous || variable.type == VariableType::SemiInteger) + throw UnsupportedPool("Pools with semi-variable domains are not yet supported"); + if (!options.projection && variable.type != VariableType::Continuous) + variables.push_back(variable.variable); + } + if (options.projection) variables = *options.projection; + if (variables.empty() && active != 0) + throw UnsupportedPool("A pool requires a nonempty finite discrete projection; continuous-only enumeration is undefined"); + std::set seen; + std::vector domains; + for (auto handle : variables) { + if (handle.model_id != source.model_id || handle.id >= source.variables.size() || + !source.variables[handle.id].active) throw ModelError("Pool projection contains a foreign, absent, or deleted variable"); + if (!seen.insert(handle.id).second) throw ModelError("Pool projection repeats a variable"); + const auto& data = source.variables[handle.id]; + if (data.type != VariableType::Integer && data.type != VariableType::Binary) + throw UnsupportedPool("Projected variables must be Integer or Binary"); + if (!std::isfinite(data.lower) || !std::isfinite(data.upper)) + throw UnsupportedPool("Projected variables require finite bounds"); + const double lower = std::ceil(data.lower), upper = std::floor(data.upper); + if (std::fabs(lower) > exact_limit || std::fabs(upper) > exact_limit) + throw UnsupportedPool("Projected integer endpoints exceed exact double representation"); + const auto lo = static_cast(lower), hi = static_cast(upper); + if (hi >= lo && hi - lo > exact_limit) + throw UnsupportedPool("Projected domain width exceeds exact no-good coefficient representation"); + domains.push_back({handle, lo, hi}); + } + return domains; +} + +void layout(const SolveResult& result, const ModelSnapshot& model) { + if (result.model_id != model.model_id || result.revision != model.revision || + result.values.size() != model.variables.size() || result.active_variables.size() != model.variables.size()) + throw InvalidPool("Pool oracle returned inconsistent identity or slot layout"); + for (std::size_t i = 0; i < model.variables.size(); ++i) + if (result.active_variables[i] != model.variables[i].active) + throw InvalidPool("Pool oracle returned an inconsistent active-variable mask"); +} + +std::vector canonical(const SolveResult& result, const ModelSnapshot& model, + const SolveOptions& options) { + layout(result, model); + auto values = result.values; + if (!validate(model, values, options.feasibility_tolerance, options.integrality_tolerance).valid) + throw InvalidPool("Pool candidate failed independent remaining-model validation"); + for (const auto& variable : model.variables) if (variable.active && variable.type != VariableType::Continuous) { + auto& value = values[variable.variable.id]; + if (std::fabs(value) > exact_limit) + throw UnsupportedPool("Integer pool values exceed exact canonical representation"); + value = std::round(value); + if (value < variable.lower || value > variable.upper) + throw InvalidPool("Canonical integer pool value is outside its exact original interval"); + } + if (!validate(model, values, options.feasibility_tolerance, 0).valid) + throw InvalidPool("Rounded pool candidate violates hard constraints or no-good exclusions"); + return values; +} + +double allowance(double a, double b, const SolveOptions& options) { + if (options.guarantee == Guarantee::Exact) return 0; + return std::max(options.feasibility_tolerance, 64 * std::numeric_limits::epsilon() * + std::max({1.0, std::fabs(a), std::fabs(b)})); +} +void optimum(const SolveResult& result, double objective, ObjectiveSense sense, + const SolveOptions& options) { + if (result.guarantee != options.guarantee || !result.best_bound || !std::isfinite(*result.best_bound)) + throw InvalidPool("Pool rank lacks a matching guarantee and finite global bound"); + if (!result.objective || std::fabs(*result.objective - objective) > allowance(*result.objective, objective, options)) + throw InvalidPool("Pool objective disagrees with the independently recomputed original objective"); + auto checked = result; + checked.objective = objective; + try { checked.update_gaps(sense); } + catch (const ModelError&) { throw InvalidPool("Pool global bound has inconsistent ordering"); } + if (!checked.absolute_gap || !std::isfinite(*checked.absolute_gap) || + *checked.absolute_gap > allowance(objective, *checked.best_bound, options)) + throw InvalidPool("Pool rank lacks a closed independently recomputed gap"); +} + +void exclude(ModelSnapshot& model, const std::vector& domains, + const std::vector& values) { + std::vector witnesses; + auto witness = [&](const Domain& domain, std::int64_t coefficient, bool below) { + if (coefficient <= 0 || coefficient > exact_limit) + throw UnsupportedPool("No-good coefficient is not a positive exactly representable integer"); + if (model.variables.size() >= std::numeric_limits::max() || + model.rows.size() >= std::numeric_limits::max()) + throw UnsupportedPool("Pool no-good exceeds handle capacity"); + VariableData binary; + binary.variable = {model.model_id, static_cast(model.variables.size())}; + binary.type = VariableType::Binary; binary.lower = 0; binary.upper = 1; + binary.name = "__pool_witness_" + std::to_string(binary.variable.id); + model.variables.push_back(binary); + RowData row; + row.constraint = {model.model_id, static_cast(model.rows.size())}; + row.terms = {{domain.variable, 1}, {binary.variable, below ? double(coefficient) : -double(coefficient)}}; + if (below) row.upper = double(domain.upper); + else row.lower = double(domain.lower); + row.name = "__pool_implication_" + std::to_string(row.constraint.id); + model.rows.push_back(std::move(row)); + witnesses.push_back({binary.variable, 1}); + }; + for (std::size_t i = 0; i < domains.size(); ++i) { + const auto& domain = domains[i]; const auto value = values[i]; + if (value < domain.lower || value > domain.upper) + throw InvalidPool("Canonical projection value is outside its original integer domain"); + if (value > domain.lower) witness(domain, domain.upper - value + 1, true); + if (value < domain.upper) witness(domain, value + 1 - domain.lower, false); + } + if (model.rows.size() >= std::numeric_limits::max()) + throw UnsupportedPool("Pool no-good exceeds row capacity"); + RowData disjunction; + disjunction.constraint = {model.model_id, static_cast(model.rows.size())}; + disjunction.terms = std::move(witnesses); disjunction.lower = 1; + disjunction.name = "__pool_exclusion_" + std::to_string(disjunction.constraint.id); + model.rows.push_back(std::move(disjunction)); + validate_structure(model); +} + +PoolResult run(const ModelSnapshot& original, const PoolOptions& options, SolveBudget& budget) { + PoolResult output; + output.model_id = original.model_id; output.revision = original.revision; + output.guarantee = options.solve.guarantee; + auto finish = [&]() { + if (auto reason = budget.stop_reason()) { + output.termination = *reason; output.completion = PoolCompletion::Incomplete; + output.message = "Shared pool budget stopped; previously accepted entries remain historical results"; + } + output.elapsed_seconds = budget.elapsed_seconds(); return std::move(output); + }; + try { + validate_structure(original); + if (options.max_solutions == 0) throw ModelError("Pool max_solutions must be positive"); + if (options.solve.guarantee == Guarantee::Certified) + throw UnsupportedPool("Certified pool enumeration is not implemented"); + if (options.solve.guarantee == Guarantee::Exact && options.solve.backend != Backend::Native) + throw UnsupportedPool("Exact pool enumeration requires the explicit Native backend"); + if (options.max_solutions > 1 && options.solve.node_limit) + throw UnsupportedPool("Multi-solve pool node budgets require cumulative consumed-node reporting"); + const auto domains = projection(original, options); + for (const auto& domain : domains) output.projection.push_back(domain.variable); + checkpoint(budget); + auto working = original; + std::set> seen; + for (;;) { + checkpoint(budget); + auto solve_options = options.solve; + solve_options.cancellation = budget.cancellation(); + solve_options.time_limit_seconds = budget.remaining_seconds(); + solve_options.relative_gap = solve_options.absolute_gap = 0; + if (!output.attempts.empty()) solve_options.primal_start.clear(); + auto result = solve(working, solve_options); + output.attempts.push_back({result.termination, result.guarantee, result.objective, result.best_bound, false, false}); + checkpoint(budget); + auto& attempt = output.attempts.back(); + if (result.model_id != working.model_id || result.revision != working.revision) + throw InvalidPool("Pool oracle result belongs to a different model or revision"); + if ((result.objective && !std::isfinite(*result.objective)) || + (result.best_bound && std::isnan(*result.best_bound))) + throw InvalidPool("Pool oracle returned non-finite objective or NaN bound evidence"); + if (result.solution_validated && !result.has_solution()) + throw InvalidPool("Pool oracle claimed a malformed validated solution"); + ValidationReport raw_check; + if ((output.ranked_prefix != 0 || result.termination == Termination::Infeasible) && + result.values.size() == working.variables.size()) + raw_check = validate(working, result.values, options.solve.feasibility_tolerance, + options.solve.integrality_tolerance); + if (raw_check.valid && raw_check.objective && output.ranked_prefix != 0) { + const auto previous = *output.entries.back().solution.objective; + const auto improvement = original.objective.sense == ObjectiveSense::Minimize + ? previous - *raw_check.objective : *raw_check.objective - previous; + if (improvement > allowance(previous, *raw_check.objective, options.solve)) { + for (auto& entry : output.entries) entry.rank_established = false; + for (auto& prior : output.attempts) prior.rank_established = false; + output.ranked_prefix = 0; + throw InvalidPool("An independently feasible raw witness disproves earlier pool ranks"); + } + } + if (result.termination == Termination::Infeasible) { + if (result.has_solution() || raw_check.valid || result.guarantee != options.solve.guarantee) + throw InvalidPool("Pool infeasibility conflicts with an independently feasible raw candidate or requested evidence guarantee"); + output.termination = Termination::Optimal; + output.completion = PoolCompletion::Exhausted; + output.message = "All feasible discrete projection classes exhausted under the requested evidence guarantee"; + return finish(); + } + if (result.has_solution()) { + if (result.guarantee != options.solve.guarantee) + throw InvalidPool("Pool candidate does not provide the requested evidence guarantee"); + auto values = canonical(result, working, options.solve); + std::vector original_values(values.begin(), values.begin() + original.variables.size()); + auto checked = validate(original, original_values, options.solve.feasibility_tolerance, 0); + if (!checked.valid || !checked.objective || !std::isfinite(*checked.objective)) + throw InvalidPool("Pool candidate failed independent original-model validation"); + std::vector key; + for (const auto& domain : domains) key.push_back(static_cast(values[domain.variable.id])); + if (seen.count(key)) throw InvalidPool("Pool oracle repeated an already excluded projection"); + const bool ranked = result.termination == Termination::Optimal; + if (output.ranked_prefix != 0) { + const auto previous = *output.entries.back().solution.objective; + const auto tolerance = allowance(previous, *checked.objective, options.solve); + const auto improvement = original.objective.sense == ObjectiveSense::Minimize + ? previous - *checked.objective : *checked.objective - previous; + if (improvement > tolerance) { + // A feasible better remaining point disproves prior ranking even + // when this new solve is interrupted. Retain feasible values only. + for (auto& entry : output.entries) entry.rank_established = false; + for (auto& prior : output.attempts) prior.rank_established = false; + output.ranked_prefix = 0; + throw InvalidPool("A later feasible pool candidate disproves earlier rank evidence"); + } + } + if (ranked) { + optimum(result, *checked.objective, original.objective.sense, options.solve); + } else if (result.termination != Termination::TimeLimit && result.termination != Termination::NodeLimit && + result.termination != Termination::MemoryLimit && result.termination != Termination::IterationLimit && result.termination != Termination::SolutionLimit && + result.termination != Termination::ObjectiveLimit && result.termination != Termination::Cancelled) { + output.termination = result.termination; output.message = result.message; + return finish(); + } + SolveResult historical = result; + historical.model_id = original.model_id; historical.revision = original.revision; + historical.values = std::move(original_values); + historical.active_variables.resize(original.variables.size()); + historical.objective = checked.objective; historical.solution_validated = true; + historical.best_bound.reset(); historical.absolute_gap.reset(); historical.relative_gap.reset(); + historical.native_backend_gap.reset(); historical.termination = Termination::Unknown; + historical.message = "Validated original-model pool representative; see its pool rank evidence"; + checkpoint(budget); + seen.insert(key); + output.entries.push_back({std::move(historical), key, ranked}); + if (budget.expired()) { output.entries.pop_back(); checkpoint(budget); } + attempt.candidate_accepted = true; attempt.rank_established = ranked; + if (ranked) ++output.ranked_prefix; + if (!ranked) { + output.termination = result.termination; + output.message = "A timely validated final candidate is unranked because its solve did not establish optimality"; + return finish(); + } + if (output.entries.size() == options.max_solutions) { + output.termination = Termination::SolutionLimit; output.completion = PoolCompletion::RequestedLimit; + output.message = "Requested number of ranked projection representatives reached; exhaustion was not tested"; + return finish(); + } + exclude(working, domains, key); + } else { + if (result.termination == Termination::Optimal) + throw InvalidPool("Pool optimum lacks a validated candidate"); + output.termination = result.termination; output.message = result.message; + return finish(); + } + } + } catch (const Interrupted& stopped) { + output.termination = stopped.reason; + } catch (const UnsupportedPool& error) { + output.termination = Termination::Unsupported; output.message = error.what(); + } catch (const InvalidPool& error) { + output.termination = Termination::NumericalFailure; output.message = error.what(); + } catch (const ModelError& error) { + output.termination = Termination::InvalidModel; output.message = error.what(); + } catch (const std::bad_alloc&) { + output.termination = Termination::MemoryLimit; output.message = "Pool allocation failed"; + } catch (const std::exception& error) { + output.termination = Termination::BackendError; output.message = error.what(); + } + return finish(); +} +PoolResult failure(ModelId id, Revision revision, Guarantee guarantee, Termination reason, const std::string& message) { + PoolResult output; output.model_id = id; output.revision = revision; + output.guarantee = guarantee; output.termination = reason; output.message = message; return output; +} +} +PoolResult solve_pool(const ModelSnapshot& model, const PoolOptions& options) { + try { SolveBudget budget(options.solve); return run(model, options, budget); } + catch (const ModelError& error) { return failure(model.model_id, model.revision, options.solve.guarantee, Termination::InvalidModel, error.what()); } + catch (const std::bad_alloc&) { return failure(model.model_id, model.revision, options.solve.guarantee, Termination::MemoryLimit, "Allocation failed"); } +} +PoolResult solve_pool(const Model& model, const PoolOptions& options) { + try { SolveBudget budget(options.solve); return run(model.snapshot(), options, budget); } + catch (const ModelError& error) { return failure(model.id(), model.revision(), options.solve.guarantee, Termination::InvalidModel, error.what()); } + catch (const std::bad_alloc&) { return failure(model.id(), model.revision(), options.solve.guarantee, Termination::MemoryLimit, "Allocation failed"); } +} +}} diff --git a/gecode/optimize/pool.hpp b/gecode/optimize/pool.hpp new file mode 100644 index 0000000000..30f5a012b6 --- /dev/null +++ b/gecode/optimize/pool.hpp @@ -0,0 +1,65 @@ +/* Ranked solutions over a finite discrete projection. */ +#ifndef GECODE_OPTIMIZE_POOL_HPP +#define GECODE_OPTIMIZE_POOL_HPP +#include + +namespace Gecode { namespace Optimize { + +enum class PoolCompletion { Incomplete, RequestedLimit, Exhausted }; +struct PoolOptions { + SolveOptions solve; + std::size_t max_solutions = 10; + // Default: every active Integer/Binary variable. Explicit projections retain + // caller order, contain no duplicate handles, and must be finite and nonempty + // unless the original model has no active variables. + std::optional> projection; +}; +struct PoolEntry { + // Original historical identity/slots, independently validated. Its scalar + // objective is original; bounds/status are cleared because later pool solves + // optimize a restricted feasible set, not the entire original model. + SolveResult solution; + std::vector projection_values; + bool rank_established = false; +}; +struct PoolAttempt { + Termination termination = Termination::Unknown; + Guarantee guarantee = Guarantee::Numerical; + std::optional objective; + std::optional remaining_bound; + bool candidate_accepted = false; + bool rank_established = false; +}; +struct PoolResult { + ModelId model_id = 0; + Revision revision = 0; + Termination termination = Termination::Unknown; + PoolCompletion completion = PoolCompletion::Incomplete; + Guarantee guarantee = Guarantee::Numerical; + std::string message; + std::vector projection; + std::vector entries; + std::vector attempts; + std::size_t ranked_prefix = 0; + double elapsed_seconds = 0; + bool exhausted() const noexcept { return completion == PoolCompletion::Exhausted; } +}; + +/** + * Repeated optimization plus safe no-good exclusions. Returns one representative + * per finite Integer/Binary projection assignment, ranked by its best original + * objective when each remaining-model solve completes. Continuous and unbounded + * non-projected Integer recourse are allowed by the HiGHS backend. Real-valued + * completions of a projection are not enumerated. Ties have unspecified order. + * An interrupted timely candidate may be a final unranked entry. Only definite + * infeasibility of the remaining model establishes projection exhaustion. + * Exhaustion is a workflow Optimal status; the last oracle attempt is Infeasible. + * RequestedLimit uses SolutionLimit and never asserts exhaustion. Numerical + * completion is tolerance-qualified; Exact requires explicit Native support. + * Semis, active indicators/globals, Certified, and multistage node budgets are + * explicitly Unsupported. No original model edits occur. See POOLS.md. + */ +PoolResult solve_pool(const ModelSnapshot& model, const PoolOptions& options = {}); +PoolResult solve_pool(const Model& model, const PoolOptions& options = {}); +}} +#endif diff --git a/gecode/optimize/presolve.cpp b/gecode/optimize/presolve.cpp new file mode 100644 index 0000000000..032c2235d9 --- /dev/null +++ b/gecode/optimize/presolve.cpp @@ -0,0 +1,356 @@ +#include +#include + +#include +#include +#include + +namespace Gecode { namespace Optimize { +namespace { +using Integer = std::int64_t; +constexpr Integer minimum = std::numeric_limits::min(); +constexpr Integer maximum = std::numeric_limits::max(); +constexpr Integer exact_limit = INT64_C(9007199254740992); +constexpr double infinity = std::numeric_limits::infinity(); +class Unsupported : public std::runtime_error { using std::runtime_error::runtime_error; }; +class InvalidWitness : public std::runtime_error { using std::runtime_error::runtime_error; }; +struct Interrupted { Termination reason; }; +struct Contradiction { Constraint row; std::optional variable; }; +void checkpoint(const SolveBudget& budget) { + if (auto reason = budget.stop_reason()) throw Interrupted{*reason}; +} +Integer add(Integer a, Integer b) { + if ((b > 0 && a > maximum-b) || (b < 0 && a < minimum-b)) + throw Unsupported("Exact presolve addition exceeds int64 range"); + return a+b; +} +Integer subtract(Integer a, Integer b) { + if ((b > 0 && a < minimum+b) || (b < 0 && a > maximum+b)) + throw Unsupported("Exact presolve subtraction exceeds int64 range"); + return a-b; +} +Integer multiply(Integer a, Integer b) { + if (a && b && ((a > 0 && ((b > 0 && a > maximum/b) || (b < 0 && b < minimum/a))) || + (a < 0 && ((b > 0 && a < minimum/b) || (b < 0 && a < maximum/b))))) + throw Unsupported("Exact presolve multiplication exceeds int64 range"); + return a*b; +} +Integer divide(Integer value, Integer divisor, bool ceiling) { + if (!divisor || (value == minimum && divisor == -1)) + throw Unsupported("Exact presolve division exceeds int64 range"); + Integer quotient = value/divisor, remainder = value%divisor; + if (remainder && ((remainder > 0) == (divisor > 0))) { + if (ceiling) quotient = add(quotient, 1); + } else if (remainder && !ceiling) quotient = subtract(quotient, 1); + return quotient; +} +Integer integer(double value) { + if (!std::isfinite(value) || value != std::trunc(value) || std::fabs(value) > exact_limit) + throw Unsupported("Exact presolve requires integral data of magnitude at most 2^53"); + return static_cast(value); +} +double exported(Integer value) { + if (value < -exact_limit || value > exact_limit) + throw Unsupported("Reduced model or reconstructed objective exceeds exact double integer range"); + return static_cast(value); +} +struct IntegerTerm { std::size_t slot; Integer coefficient; }; +struct IntegerRow { + Constraint original; + std::vector terms; + std::optional lower, upper; + bool active = false, redundant = false; +}; +struct Data { + std::vector lower, upper; + std::vector active; + std::vector rows; + std::vector objective; + Integer offset = 0; +}; +Data parse(const ModelSnapshot& source, const SolveBudget* budget = nullptr) { + validate_structure(source); + for (const auto& indicator : source.indicators) + if (indicator.active) throw Unsupported("Exact presolve does not yet map active indicators"); + for (const auto& global : source.globals) + if (global.active) throw Unsupported("Exact presolve does not yet map active globals"); + Data data; + data.lower.resize(source.variables.size()); data.upper.resize(source.variables.size()); + for (const auto& variable : source.variables) { + if (budget) checkpoint(*budget); + data.active.push_back(variable.active); + if (!variable.active) continue; + if (variable.type != VariableType::Integer && variable.type != VariableType::Binary) + throw Unsupported("Exact presolve requires bounded Integer/Binary variables"); + data.lower[variable.variable.id] = integer(variable.lower); + data.upper[variable.variable.id] = integer(variable.upper); + } + for (const auto& original : source.rows) { + if (budget) checkpoint(*budget); + IntegerRow row; row.original = original.constraint; row.active = original.active; + if (row.active) { + if (std::isfinite(original.lower)) row.lower = integer(original.lower); + if (std::isfinite(original.upper)) row.upper = integer(original.upper); + for (const auto& term : original.terms) row.terms.push_back({static_cast(term.variable.id), integer(term.coefficient)}); + } + data.rows.push_back(std::move(row)); + } + data.offset = integer(source.objective.offset); + for (const auto& term : source.objective.terms) + data.objective.push_back({static_cast(term.variable.id), integer(term.coefficient)}); + return data; +} + +bool propagate(Data& data, const PresolveOptions& options, PresolveResult& output, const SolveBudget& budget) { + for (std::size_t pass = 0; pass < options.max_passes; ++pass) { + bool changed = false; + for (auto& row : data.rows) { + checkpoint(budget); + if (!row.active || row.redundant) continue; + if (options.max_row_visits && output.row_visits >= *options.max_row_visits) return false; + if (output.row_visits == std::numeric_limits::max()) + throw Unsupported("Presolve row-visit counter exhausted"); + ++output.row_visits; + if (!row.lower && !row.upper) { row.redundant = true; continue; } + Integer lo = 0, hi = 0; + std::vector minima, maxima; + for (const auto& term : row.terms) { + checkpoint(budget); + const auto a = multiply(term.coefficient, data.lower[term.slot]); + const auto b = multiply(term.coefficient, data.upper[term.slot]); + minima.push_back(std::min(a,b)); maxima.push_back(std::max(a,b)); + lo = add(lo, minima.back()); hi = add(hi, maxima.back()); + } + if ((row.lower && hi < *row.lower) || (row.upper && lo > *row.upper)) + throw Contradiction{row.original, {}}; + if ((!row.lower || lo >= *row.lower) && (!row.upper || hi <= *row.upper)) { + row.redundant = true; continue; + } + struct Proposal { std::size_t slot; Integer lower, upper; }; + std::vector proposals; + // Every proposal uses the unchanged box seen at the beginning of this row. + for (std::size_t i = 0; i < row.terms.size(); ++i) { + checkpoint(budget); + const auto& term = row.terms[i]; + Integer lower = data.lower[term.slot], upper = data.upper[term.slot]; + if (row.lower) { + const auto rhs = subtract(*row.lower, subtract(hi, maxima[i])); + if (term.coefficient > 0) lower = std::max(lower, divide(rhs, term.coefficient, true)); + else upper = std::min(upper, divide(rhs, term.coefficient, false)); + } + if (row.upper) { + const auto rhs = subtract(*row.upper, subtract(lo, minima[i])); + if (term.coefficient > 0) upper = std::min(upper, divide(rhs, term.coefficient, false)); + else lower = std::max(lower, divide(rhs, term.coefficient, true)); + } + if (lower > upper) throw Contradiction{row.original, Variable{row.original.model_id, term.slot}}; + proposals.push_back({term.slot, lower, upper}); + } + for (const auto& proposal : proposals) { + checkpoint(budget); + const Variable variable{row.original.model_id, proposal.slot}; + if (proposal.lower != data.lower[proposal.slot]) { + output.changes.push_back({variable, row.original, PresolveBoundSide::Lower, + data.lower[proposal.slot], proposal.lower, pass}); + data.lower[proposal.slot] = proposal.lower; changed = true; + } + if (proposal.upper != data.upper[proposal.slot]) { + output.changes.push_back({variable, row.original, PresolveBoundSide::Upper, + data.upper[proposal.slot], proposal.upper, pass}); + data.upper[proposal.slot] = proposal.upper; changed = true; + } + } + } + ++output.passes; + if (!changed) return true; + } + return false; +} + +Integer exact_check(const ModelSnapshot& model, const std::vector& values) { + const auto data = parse(model); + if (values.size() != model.variables.size()) throw InvalidWitness("Postsolve assignment has incorrect slot count"); + std::vector point(values.size()); + for (std::size_t i = 0; i < values.size(); ++i) if (data.active[i]) { + point[i] = integer(values[i]); + if (point[i] < data.lower[i] || point[i] > data.upper[i]) + throw InvalidWitness("Postsolve assignment violates an exact original variable interval"); + } + for (const auto& row : data.rows) if (row.active) { + Integer value = 0; + for (const auto& term : row.terms) value = add(value, multiply(term.coefficient, point[term.slot])); + if ((row.lower && value < *row.lower) || (row.upper && value > *row.upper)) + throw InvalidWitness("Postsolve assignment violates an exact original linear row"); + } + Integer objective = data.offset; + for (const auto& term : data.objective) objective = add(objective, multiply(term.coefficient, point[term.slot])); + return objective; +} +SolveOptions budget_options(const PresolveOptions& options) { + SolveOptions result; result.time_limit_seconds = options.time_limit_seconds; + result.cancellation = options.cancellation; return result; +} +} + +namespace Detail { +struct PresolveBuilder { + static std::shared_ptr build(const ModelSnapshot& source, const Data& data, + PresolveResult& output, const SolveBudget& budget) { + Model reduced; + if (reduced.id() == source.model_id) reduced = Model{}; + std::vector variables; + std::vector rows; + for (std::size_t i = 0; i < source.variables.size(); ++i) { + checkpoint(budget); + const auto& original = source.variables[i]; + PresolveVariableMap mapping; + mapping.original = original.variable; mapping.active = original.active; + if (original.active) { + mapping.lower = data.lower[i]; mapping.upper = data.upper[i]; + if (mapping.lower == mapping.upper) { mapping.fixed_value = mapping.lower; ++output.fixed_variables; } + else mapping.reduced = reduced.add_variable(original.type, exported(mapping.lower), exported(mapping.upper), original.name); + } + variables.push_back(mapping); + } + for (std::size_t i = 0; i < data.rows.size(); ++i) { + checkpoint(budget); + const auto& row = data.rows[i]; + PresolveRowMap mapping; mapping.original = row.original; mapping.active = row.active; + if (row.active) { + if (row.redundant) { mapping.redundant = true; ++output.removed_rows; } + else { + Integer shift = 0; + std::vector terms; + for (const auto& term : row.terms) { + checkpoint(budget); + if (variables[term.slot].fixed_value) shift = add(shift, multiply(term.coefficient, *variables[term.slot].fixed_value)); + else terms.push_back({*variables[term.slot].reduced, exported(term.coefficient)}); + } + const auto lower = row.lower ? std::optional(subtract(*row.lower, shift)) : std::nullopt; + const auto upper = row.upper ? std::optional(subtract(*row.upper, shift)) : std::nullopt; + mapping.substituted_constant = shift; + if (terms.empty()) { + if ((lower && *lower > 0) || (upper && *upper < 0)) throw Contradiction{row.original, {}}; + mapping.redundant = true; ++output.removed_rows; + } else { + mapping.reduced = reduced.add_row(terms, lower ? exported(*lower) : -infinity, + upper ? exported(*upper) : infinity, source.rows[i].name); + } + } + } + rows.push_back(mapping); + } + Integer offset = data.offset; + std::vector objective; + for (const auto& term : data.objective) { + checkpoint(budget); + if (variables[term.slot].fixed_value) offset = add(offset, multiply(term.coefficient, *variables[term.slot].fixed_value)); + else objective.push_back({*variables[term.slot].reduced, exported(term.coefficient)}); + } + reduced.set_objective(objective, source.objective.sense, exported(offset)); + auto snapshot = reduced.snapshot(); + validate_structure(snapshot); + checkpoint(budget); + return std::shared_ptr(new PresolvedModel(source, std::move(snapshot), std::move(variables), std::move(rows))); + } +}; +} + +PresolvedModel::PresolvedModel(ModelSnapshot original, ModelSnapshot reduced, + std::vector variables, std::vector rows) + : original_(std::move(original)), reduced_(std::move(reduced)), variables_(std::move(variables)), rows_(std::move(rows)) {} + +PostsolveResult PresolvedModel::postsolve(const SolveResult& candidate, double tolerance) const { + PostsolveResult output; + auto& result = output.solution; + result.model_id = original_.model_id; result.revision = original_.revision; + try { + if (!std::isfinite(tolerance) || tolerance < 0 || tolerance >= 0.5) + throw ModelError("Postsolve integer tolerance must be finite and in [0,0.5)"); + if (candidate.model_id != reduced_.model_id || candidate.revision != reduced_.revision || + candidate.values.size() != reduced_.variables.size() || candidate.active_variables.size() != reduced_.variables.size()) + throw ModelError("Postsolve candidate has foreign identity, stale revision, or incorrect slot layout"); + if (candidate.guarantee == Guarantee::Certified) + throw Unsupported("Postsolve does not transfer certificate guarantees"); + if (candidate.guarantee != Guarantee::Numerical && candidate.guarantee != Guarantee::Exact) + throw ModelError("Unknown postsolve input guarantee"); + auto values = candidate.values; + for (std::size_t i = 0; i < values.size(); ++i) { + if (candidate.active_variables[i] != reduced_.variables[i].active) + throw ModelError("Postsolve candidate has an inconsistent active mask"); + if (!std::isfinite(values[i]) || std::fabs(values[i]) > exact_limit || + std::fabs(values[i]-std::round(values[i])) > tolerance) + throw InvalidWitness("Postsolve candidate is not a finite integer assignment within tolerance"); + values[i] = std::round(values[i]); + } + const auto reduced_objective = exact_check(reduced_, values); + std::vector original_values(original_.variables.size(), std::numeric_limits::quiet_NaN()); + for (const auto& mapping : variables_) if (mapping.active) + original_values[mapping.original.id] = mapping.fixed_value ? exported(*mapping.fixed_value) : values[mapping.reduced->id]; + const auto original_objective = exact_check(original_, original_values); + if (original_objective != reduced_objective) throw InvalidWitness("Postsolve objective does not agree under the owned transformation"); + result.objective = exported(original_objective); + result.values = std::move(original_values); + for (const auto& variable : original_.variables) result.active_variables.push_back(variable.active); + result.guarantee = candidate.guarantee; result.backend = candidate.backend; result.backend_version = candidate.backend_version; + result.solution_validated = true; result.termination = Termination::Unknown; + result.message = "Exact original feasible witness reconstructed; no scalar optimum status or global bound transferred"; + output.exact_witness_validated = true; + } catch (const Unsupported& error) { result.termination = Termination::Unsupported; result.message = error.what(); } + catch (const InvalidWitness& error) { result.termination = Termination::NumericalFailure; result.message = error.what(); } + catch (const ModelError& error) { result.termination = Termination::InvalidModel; result.message = error.what(); } + catch (const std::bad_alloc&) { result.termination = Termination::MemoryLimit; result.message = "Allocation failed"; } + if (!output.exact_witness_validated) result.solution_validated = false; + return output; +} + +namespace { +PresolveResult run(const ModelSnapshot& source, const PresolveOptions& options, SolveBudget& budget) { + PresolveResult output; output.model_id = source.model_id; output.revision = source.revision; + try { + checkpoint(budget); + auto data = parse(source, &budget); + const bool fixed = propagate(data, options, output, budget); + auto model = Detail::PresolveBuilder::build(source, data, output, budget); + checkpoint(budget); + output.model = std::move(model); + output.status = fixed ? PresolveStatus::Fixpoint : PresolveStatus::Incomplete; + output.termination = fixed ? Termination::Optimal : Termination::IterationLimit; + output.message = fixed ? "Exact propagation reached its fixpoint; the owned reduction preserves the integer feasible set" + : "Propagation work limit reached; the finalized partial reduction is exactly equivalent"; + } catch (const Contradiction& proof) { + output.status = PresolveStatus::Infeasible; output.termination = Termination::Infeasible; + output.infeasible_row = proof.row; output.infeasible_variable = proof.variable; + output.message = "Exact integer interval arithmetic establishes an original-model contradiction"; + } catch (const Interrupted& stopped) { output.status = PresolveStatus::Incomplete; output.termination = stopped.reason; } + catch (const Unsupported& error) { output.status = PresolveStatus::Unsupported; output.termination = Termination::Unsupported; output.message = error.what(); } + catch (const ModelError& error) { output.status = PresolveStatus::InvalidModel; output.termination = Termination::InvalidModel; output.message = error.what(); } + catch (const std::bad_alloc&) { output.status = PresolveStatus::Error; output.termination = Termination::MemoryLimit; output.message = "Allocation failed"; } + catch (const std::exception& error) { output.status = PresolveStatus::Error; output.termination = Termination::BackendError; output.message = error.what(); } + if (output.status != PresolveStatus::Fixpoint && + !(output.status == PresolveStatus::Incomplete && output.termination == Termination::IterationLimit)) + output.model.reset(); + if (auto reason = budget.stop_reason()) { + output.model.reset(); output.infeasible_row.reset(); output.infeasible_variable.reset(); + output.status = PresolveStatus::Incomplete; output.termination = *reason; + output.message = "Presolve budget stopped; no new reduction or infeasibility result was published"; + } + output.elapsed_seconds = budget.elapsed_seconds(); return output; +} +PresolveResult failure(ModelId id, Revision revision, Termination reason, const std::string& message) { + PresolveResult output; output.model_id = id; output.revision = revision; + output.status = reason == Termination::InvalidModel ? PresolveStatus::InvalidModel : PresolveStatus::Error; + output.termination = reason; output.message = message; return output; +} +} +PresolveResult presolve_integer(const ModelSnapshot& model, const PresolveOptions& options) { + try { SolveBudget budget(budget_options(options)); return run(model, options, budget); } + catch (const ModelError& error) { return failure(model.model_id, model.revision, Termination::InvalidModel, error.what()); } + catch (const std::bad_alloc&) { return failure(model.model_id, model.revision, Termination::MemoryLimit, "Allocation failed"); } +} +PresolveResult presolve_integer(const Model& model, const PresolveOptions& options) { + try { SolveBudget budget(budget_options(options)); return run(model.snapshot(), options, budget); } + catch (const ModelError& error) { return failure(model.id(), model.revision(), Termination::InvalidModel, error.what()); } + catch (const std::bad_alloc&) { return failure(model.id(), model.revision(), Termination::MemoryLimit, "Allocation failed"); } +} +}} diff --git a/gecode/optimize/presolve.hpp b/gecode/optimize/presolve.hpp new file mode 100644 index 0000000000..223363f213 --- /dev/null +++ b/gecode/optimize/presolve.hpp @@ -0,0 +1,88 @@ +/* Explicit exact bounded-integer reductions and owning reconstruction. */ +#ifndef GECODE_OPTIMIZE_PRESOLVE_HPP +#define GECODE_OPTIMIZE_PRESOLVE_HPP +#include +#include + +namespace Gecode { namespace Optimize { +enum class PresolveStatus { Fixpoint, Incomplete, Infeasible, Unsupported, InvalidModel, Error }; +enum class PresolveBoundSide { Lower, Upper }; +struct PresolveOptions { + double time_limit_seconds = std::numeric_limits::infinity(); + std::shared_ptr cancellation; + std::size_t max_passes = 100; + // Counts propagation row visits, excluding structural checks/finalization. + std::optional max_row_visits; +}; +struct PresolveBoundChange { + Variable variable; + Constraint row; + PresolveBoundSide side = PresolveBoundSide::Lower; + std::int64_t before = 0, after = 0; + std::size_t pass = 0; +}; +struct PresolveVariableMap { + Variable original; + bool active = false; + std::optional reduced; + std::optional fixed_value; + std::int64_t lower = 0, upper = 0; +}; +struct PresolveRowMap { + Constraint original; + bool active = false; + std::optional reduced; + bool redundant = false; + // Exact fixed-variable contribution subtracted from retained row bounds. + std::optional substituted_constant; +}; +struct PostsolveResult { + SolveResult solution; + bool exact_witness_validated = false; +}; +namespace Detail { struct PresolveBuilder; } +class PresolvedModel { +public: + const ModelSnapshot& original() const noexcept { return original_; } + const ModelSnapshot& reduced() const noexcept { return reduced_; } + const std::vector& variables() const noexcept { return variables_; } + const std::vector& rows() const noexcept { return rows_; } + // An original historical feasible solution only: scalar optimum status, + // global bounds and gaps are not transferred. Integer values may be rounded + // within tolerance, then both models are checked with exact integer arithmetic. + PostsolveResult postsolve(const SolveResult& candidate, + double integrality_tolerance = 1e-6) const; +private: + friend struct Detail::PresolveBuilder; + PresolvedModel(ModelSnapshot original, ModelSnapshot reduced, + std::vector variables, + std::vector rows); + ModelSnapshot original_, reduced_; + std::vector variables_; + std::vector rows_; +}; +struct PresolveResult { + ModelId model_id = 0; + Revision revision = 0; + PresolveStatus status = PresolveStatus::Incomplete; + // Optimal means propagation reached its fixpoint, not a solved optimization. + Termination termination = Termination::Unknown; + Guarantee guarantee = Guarantee::Exact; + std::string message; + std::shared_ptr model; + std::vector changes; + std::optional infeasible_row; + std::optional infeasible_variable; + std::size_t passes = 0, row_visits = 0, fixed_variables = 0, removed_rows = 0; + double elapsed_seconds = 0; +}; + +// Finite Integer/Binary variables and integral ordinary linear data only. +// Checked int64 arithmetic; exported integer doubles have magnitude <=2^53. +// Unsupported arithmetic/data never authorize a reduction or infeasibility. +// Explicit utility; automatic Native solving uses a separate bounded exact +// composition. The postsolve() feasible-witness-only contract is unchanged. +PresolveResult presolve_integer(const ModelSnapshot& model, const PresolveOptions& options = {}); +PresolveResult presolve_integer(const Model& model, const PresolveOptions& options = {}); +}} +#endif diff --git a/gecode/optimize/quadratic.cpp b/gecode/optimize/quadratic.cpp new file mode 100644 index 0000000000..477b80cb8e --- /dev/null +++ b/gecode/optimize/quadratic.cpp @@ -0,0 +1,133 @@ +#include +#include +#include +#include +#include +#include +namespace Gecode { namespace Optimize { +struct QuadraticSnapshot::Data { + ModelSnapshot core; + std::vector squares; +}; +QuadraticSnapshot::QuadraticSnapshot(ModelSnapshot core, std::vector squares) + : data_(std::make_shared(Data{std::move(core), std::move(squares)})) {} +ModelId QuadraticSnapshot::id() const noexcept { return data_->core.model_id; } +Revision QuadraticSnapshot::revision() const noexcept { return data_->core.revision; } +const std::vector& QuadraticSnapshot::variables() const noexcept { return data_->core.variables; } +const std::vector& QuadraticSnapshot::rows() const noexcept { return data_->core.rows; } +const ObjectiveData& QuadraticSnapshot::linear_part() const noexcept { return data_->core.objective; } +const std::vector& QuadraticSnapshot::squares() const noexcept { return data_->squares; } +const ModelSnapshot& Detail::QuadraticAccess::core(const QuadraticSnapshot& q) { return q.data_->core; } +namespace { +void finite_box(double l, double u) { + if (!std::isfinite(l) || !std::isfinite(u) || l > u) + throw ModelError("Quadratic variables require finite ordered bounds"); +} +class Sum { + long double sum_ = 0, correction_ = 0; +public: + void add(long double x) { + const auto next = sum_ + x; + correction_ += std::abs(sum_) >= std::abs(x) ? (sum_-next)+x : (x-next)+sum_; + sum_ = next; + } + long double value() const { return sum_ + correction_; } +}; +double finite_value(long double x) { + const double d = static_cast(x); + if (!std::isfinite(x) || !std::isfinite(d)) throw ModelError("Quadratic evaluation overflow"); + return d; +} +} +Variable QuadraticModel::add_continuous(double l, double u, std::string name) { + finite_box(l,u); return core_.add_continuous(l,u,std::move(name)); +} +Constraint QuadraticModel::add_row(const std::vector& terms, double l, double u, std::string name) { + return core_.add_row(terms,l,u,std::move(name)); +} +void QuadraticModel::set_bounds(Variable v, double l, double u) { finite_box(l,u); core_.set_bounds(v,l,u); } +void QuadraticModel::set_bounds(Constraint r, double l, double u) { core_.set_bounds(r,l,u); } +void QuadraticModel::set_coefficient(Constraint r, Variable v, double a) { core_.set_coefficient(r,v,a); } +void QuadraticModel::remove(Constraint r) { core_.remove(r); } +void QuadraticModel::remove(Variable v) { + core_.variable(v); + for (const auto& square : squares_) + for (const auto& t : square.terms) + if (t.variable == v) throw ModelError("Variable is referenced by a square"); + core_.remove(v); +} +void QuadraticModel::objective(const std::vector& squares, + const std::vector& linear, double offset, ObjectiveSense sense) { + if (!core_.id()) throw ModelError("Moved-from quadratic model"); + auto staged = squares; + for (auto& square : staged) { + if (!std::isfinite(square.weight) || square.weight <= 0 || !std::isfinite(square.offset)) + throw ModelError("Square requires finite positive weight and finite offset"); + for (const auto& t : square.terms) { + core_.variable(t.variable); + if (!std::isfinite(t.coefficient)) throw ModelError("Nonfinite square coefficient"); + } + std::stable_sort(square.terms.begin(), square.terms.end(), [](const Term& a, const Term& b) { + return a.variable.id < b.variable.id; + }); + std::vector canonical; + for (std::size_t i=0; i& s, const std::vector& l, double o) { + objective(s,l,o,ObjectiveSense::Minimize); +} +void QuadraticModel::maximize_concave_squares(const std::vector& s, const std::vector& l, double o) { + objective(s,l,o,ObjectiveSense::Maximize); +} +QuadraticSnapshot QuadraticModel::snapshot() const { return QuadraticSnapshot(core_.snapshot(),squares_); } +void QuadraticOptions::validate() const { + solve.validate(); + for (double t : {stationarity_tolerance,complementarity_tolerance,optimality_tolerance}) + if (!std::isfinite(t) || t < 0) throw ModelError("QP checking tolerances must be finite and nonnegative"); +} +QuadraticValidation validate_quadratic(const QuadraticSnapshot& model, + const std::vector& values, double tolerance) { + QuadraticValidation out; + try { + const auto& core = Detail::QuadraticAccess::core(model); + const auto linear = validate(core,values,tolerance); + out.primal_valid = linear.valid; + if (!linear.valid) { out.message = linear.message; return out; } + const int sign = core.objective.sense == ObjectiveSense::Minimize ? 1 : -1; + Sum objective; + objective.add(core.objective.offset); + std::vector gradient(core.variables.size()); + for (const auto& t : core.objective.terms) { + objective.add(static_cast(t.coefficient)*values[t.variable.id]); + gradient[t.variable.id].add(t.coefficient); + } + for (const auto& square : model.squares()) { + Sum residual; residual.add(square.offset); + for (const auto& t : square.terms) + residual.add(static_cast(t.coefficient)*values[t.variable.id]); + const auto r = residual.value(); + out.square_values.push_back(finite_value(r)); + objective.add(sign*static_cast(square.weight)*r*r); + for (const auto& t : square.terms) + gradient[t.variable.id].add(2*sign*static_cast(square.weight)*r*t.coefficient); + } + out.original_objective = finite_value(objective.value()); + out.original_gradient.resize(core.variables.size(),0); + for (const auto& v : core.variables) if (v.active) + out.original_gradient[v.variable.id] = finite_value(gradient[v.variable.id].value()); + out.objective_valid = true; + } catch (const ModelError& e) { out.message = e.what(); } + return out; +} +}} diff --git a/gecode/optimize/quadratic.hpp b/gecode/optimize/quadratic.hpp new file mode 100644 index 0000000000..642d7e48b4 --- /dev/null +++ b/gecode/optimize/quadratic.hpp @@ -0,0 +1,100 @@ +/* Explicit bounded convex/concave continuous quadratic optimization. */ +#ifndef GECODE_OPTIMIZE_QUADRATIC_HPP +#define GECODE_OPTIMIZE_QUADRATIC_HPP +#include +#include +namespace Gecode { namespace Optimize { +namespace Detail { struct QuadraticAccess; } +struct WeightedSquare { + std::vector terms; + double offset = 0; + double weight = 1; + std::string name; +}; +/** Immutable, owning, distinct snapshot; no conversion to a linear model. */ +class QuadraticSnapshot { +public: + QuadraticSnapshot(const QuadraticSnapshot&) noexcept = default; + QuadraticSnapshot& operator=(const QuadraticSnapshot&) noexcept = default; + // Moving a snapshot preserves a usable historical view in both objects. + QuadraticSnapshot(QuadraticSnapshot&& other) noexcept : data_(other.data_) {} + QuadraticSnapshot& operator=(QuadraticSnapshot&& other) noexcept { data_=other.data_; return *this; } + ModelId id() const noexcept; + Revision revision() const noexcept; + const std::vector& variables() const noexcept; + const std::vector& rows() const noexcept; + const ObjectiveData& linear_part() const noexcept; + const std::vector& squares() const noexcept; +private: + struct Data; + std::shared_ptr data_; + explicit QuadraticSnapshot(ModelSnapshot, std::vector); + friend class QuadraticModel; + friend struct Detail::QuadraticAccess; +}; +/** f = linear + offset + sum(w*r^2) for min, minus sum(w*r^2) for max. */ +class QuadraticModel { +public: + QuadraticModel() = default; + QuadraticModel(QuadraticModel&&) noexcept = default; + QuadraticModel& operator=(QuadraticModel&&) noexcept = default; + QuadraticModel(const QuadraticModel&) = delete; + QuadraticModel& operator=(const QuadraticModel&) = delete; + ModelId id() const noexcept { return core_.id(); } + Revision revision() const noexcept { return core_.revision(); } + Variable add_continuous(double lower, double upper, std::string name = {}); + Constraint add_row(const std::vector&, double lower, double upper, + std::string name = {}); + void set_bounds(Variable, double lower, double upper); + void set_bounds(Constraint, double lower, double upper); + void set_coefficient(Constraint, Variable, double); + void remove(Variable); + void remove(Constraint); + void minimize_squares(const std::vector&, + const std::vector& linear = {}, double offset = 0); + void maximize_concave_squares(const std::vector&, + const std::vector& linear = {}, double offset = 0); + QuadraticSnapshot snapshot() const; +private: + Model core_; + std::vector squares_; + void objective(const std::vector&, const std::vector&, + double, ObjectiveSense); +}; +struct QuadraticOptions { + SolveOptions solve; + std::uint64_t iteration_limit = 100000; + std::size_t max_auxiliary_variables = 100000; + std::size_t max_lifted_nonzeros = 2000000; + double stationarity_tolerance = 1e-7; + double complementarity_tolerance = 1e-7; + /** Absolute original objective units; no relaxation by objective constants. */ + double optimality_tolerance = 1e-6; + void validate() const; +}; +struct QuadraticValidation { + bool primal_valid = false, objective_valid = false; + bool kkt_available = false, kkt_valid = false, bound_valid = false; + double max_stationarity = 0, max_complementarity = 0; + std::optional original_objective, normalized_lower_bound; + /** Outward upper bound on original normalized primal minus dual, offset free. */ + std::optional gap_upper_bound; + std::vector square_values, original_gradient; + std::string message; +}; +struct QuadraticResult { + SolveResult result; + QuadraticValidation checks; + std::optional vendor_objective, vendor_dual_estimate; + std::uint64_t qp_iterations = 0; + double regularization = 0; +}; +/** Independent original primal/objective/gradient check; no optimality claim. */ +QuadraticValidation validate_quadratic(const QuadraticSnapshot&, + const std::vector&, + double feasibility_tolerance = 1e-7); +QuadraticResult solve_quadratic(const QuadraticSnapshot&, const QuadraticOptions& = {}); +QuadraticResult solve_quadratic(const QuadraticModel&, const QuadraticOptions& = {}); +BackendCapabilities quadratic_capabilities(); +}} +#endif diff --git a/gecode/optimize/quadratic_bound.cpp b/gecode/optimize/quadratic_bound.cpp new file mode 100644 index 0000000000..bb9e7cc4a4 --- /dev/null +++ b/gecode/optimize/quadratic_bound.cpp @@ -0,0 +1,120 @@ +#include +#include +#include +#include +#include +#include +#include +namespace Gecode { namespace Optimize { namespace Detail { +namespace { +void finite(QpInterval v) { + if (!std::isfinite(v.lower) || !std::isfinite(v.upper) || v.lower > v.upper) + throw std::overflow_error("Unavailable QP enclosure"); +} +QpInterval widen(double lower, double upper) { + QpInterval r{std::nextafter(lower,-std::numeric_limits::infinity()), + std::nextafter(upper,std::numeric_limits::infinity())}; + finite(r); return r; +} +void checkpoint(const SolveBudget* b) { + if (b && b->expired()) throw std::overflow_error("QP bound budget exhausted"); +} +QpInterval affine(const std::vector& terms, double offset, const std::vector& x) { + auto r = qp_point(offset); + for (const auto& t : terms) r = qp_add(r,qp_multiply(qp_point(t.coefficient),qp_point(x.at(t.variable.id)))); + return r; +} +} +bool quadratic_arithmetic_supported() noexcept { +#if defined(__FAST_MATH__) || (defined(__FINITE_MATH_ONLY__) && __FINITE_MATH_ONLY__ != 0) + return false; +#else + if (!std::numeric_limits::is_iec559 || FLT_RADIX != 2 || DBL_MANT_DIG != 53 || + sizeof(double) != 8 || std::fegetround() != FE_TONEAREST) return false; + // Reject flush-to-zero/denormals-are-zero environments as well as other formats. + volatile double tiny = std::numeric_limits::denorm_min(); + volatile double two = 2.0; + volatile double preserved = tiny * two; + return preserved == std::numeric_limits::denorm_min()*2 && preserved != 0; +#endif +} +QpInterval qp_point(double x) { QpInterval r{x,x}; finite(r); return r; } +QpInterval qp_negate(QpInterval x) { finite(x); return {-x.upper,-x.lower}; } +QpInterval qp_add(QpInterval a, QpInterval b) { + finite(a); finite(b); + if (a.lower == 0 && a.upper == 0) return b; + if (b.lower == 0 && b.upper == 0) return a; + volatile double lo = a.lower+b.lower, hi = a.upper+b.upper; + return widen(lo,hi); +} +QpInterval qp_multiply(QpInterval a, QpInterval b) { + finite(a); finite(b); + if ((a.lower == 0 && a.upper == 0) || (b.lower == 0 && b.upper == 0)) return {0,0}; + if (a.lower == 1 && a.upper == 1) return b; + if (b.lower == 1 && b.upper == 1) return a; + if (a.lower == -1 && a.upper == -1) return qp_negate(b); + if (b.lower == -1 && b.upper == -1) return qp_negate(a); + volatile double p0 = a.lower*b.lower, p1 = a.lower*b.upper; + volatile double p2 = a.upper*b.lower, p3 = a.upper*b.upper; + const double lo = std::min(std::min(double(p0),double(p1)),std::min(double(p2),double(p3))); + const double hi = std::max(std::max(double(p0),double(p1)),std::max(double(p2),double(p3))); + return widen(lo,hi); +} +QuadraticBound quadratic_bound(const QuadraticSnapshot& q, const std::vector& x, + const std::vector& tangents, + const std::vector& duals, const SolveBudget* budget) { + QuadraticBound out; + if (!quadratic_arithmetic_supported()) return out; + try { + checkpoint(budget); + if (tangents.size() != q.squares().size() || duals.size() != q.rows().size()) return out; + const double sign = q.linear_part().sense == ObjectiveSense::Minimize ? 1 : -1; + std::vector r(q.variables().size(),{0,0}); + for (const auto& t : q.linear_part().terms) r[t.variable.id] = qp_point(sign*t.coefficient); + auto k = qp_point(0); + for (std::size_t i=0; i= 0 ? row.lower : row.upper; + if (!std::isfinite(side)) d = 0; + if (!d) continue; + k = qp_add(k,qp_multiply(qp_point(d),qp_point(side))); + for (const auto& a : row.terms) + r[a.variable.id] = qp_add(r[a.variable.id],qp_negate(qp_multiply(qp_point(d),qp_point(a.coefficient)))); + } + auto centered = k; + for (const auto& v : q.variables()) if (v.active) { + checkpoint(budget); + const auto product = qp_multiply(r[v.variable.id],{v.lower,v.upper}); + // Only this minimum's lower endpoint is used in the proof. + centered = qp_add(centered,qp_point(product.lower)); + } + out.normalized_lower = qp_add(centered,qp_point(sign*q.linear_part().offset)).lower; + if (x.size() == q.variables().size()) { + auto p = affine(q.linear_part().terms,0,x); + if (sign < 0) p = qp_negate(p); + for (const auto& sq : q.squares()) { + checkpoint(budget); + const auto residual = affine(sq.terms,sq.offset,x); + p = qp_add(p,qp_multiply(qp_point(sq.weight),qp_multiply(residual,residual))); + } + // Offsets cancel algebraically before enclosure, even when huge. + out.gap_upper = qp_add(p,qp_negate(qp_point(centered.lower))).upper; + } + checkpoint(budget); + } catch (const std::overflow_error&) { return {}; } + catch (const std::out_of_range&) { return {}; } + return out; +} +}}} diff --git a/gecode/optimize/quadratic_bound.hpp b/gecode/optimize/quadratic_bound.hpp new file mode 100644 index 0000000000..599d65a077 --- /dev/null +++ b/gecode/optimize/quadratic_bound.hpp @@ -0,0 +1,41 @@ +/* Private QP compilation/checking helpers. Not a supported public model API. */ +#ifndef GECODE_OPTIMIZE_QUADRATIC_BOUND_HPP +#define GECODE_OPTIMIZE_QUADRATIC_BOUND_HPP +#include +namespace Gecode { namespace Optimize { namespace Detail { +struct QuadraticAccess { + static const ModelSnapshot& core(const QuadraticSnapshot&); +}; +struct QpInterval { double lower, upper; }; +bool quadratic_arithmetic_supported() noexcept; +QpInterval qp_add(QpInterval, QpInterval); +QpInterval qp_multiply(QpInterval, QpInterval); +QpInterval qp_negate(QpInterval); +QpInterval qp_point(double); +struct QuadraticBound { + std::optional normalized_lower; + std::optional gap_upper; +}; +QuadraticBound quadratic_bound(const QuadraticSnapshot&, const std::vector& values, + const std::vector& tangents, + const std::vector& row_duals, + const SolveBudget* = nullptr); +struct QuadraticRaw { + ModelId model_id = 0; + Revision revision = 0; + Termination termination = Termination::Unknown; + bool value_valid = false, dual_valid = false; + std::vector values, residual_values, row_duals, column_duals; + std::vector active_variables; + std::optional objective, dual_estimate; + std::uint64_t iterations = 0; + double regularization = 0; + std::string message; +}; +#ifdef GECODE_QUADRATIC_TEST_HOOKS +bool quadratic_test_oracle(const QuadraticSnapshot&, const QuadraticOptions&, QuadraticRaw&); +void quadratic_test_event(const char*); +extern double quadratic_test_regularization; +#endif +}}} +#endif diff --git a/gecode/optimize/quadratic_solve.cpp b/gecode/optimize/quadratic_solve.cpp new file mode 100644 index 0000000000..0e199d4920 --- /dev/null +++ b/gecode/optimize/quadratic_solve.cpp @@ -0,0 +1,383 @@ +#include +#include +#include +#include +#include +#include +#ifdef GECODE_OPTIMIZE_WITH_HIGHS +#include +#endif +namespace Gecode { namespace Optimize { +namespace { +using Detail::QuadraticRaw; +struct Unsupported : std::runtime_error { using std::runtime_error::runtime_error; }; +struct Stopped { Termination reason; }; +void checkpoint(const SolveBudget& budget) { + if (auto reason = budget.stop_reason()) throw Stopped{*reason}; +} +void event(const char* name) { +#ifdef GECODE_QUADRATIC_TEST_HOOKS + Detail::quadratic_test_event(name); +#else + (void)name; +#endif +} +void support(const QuadraticOptions& o) { + if (o.solve.backend != Backend::Auto && o.solve.backend != Backend::Highs) + throw Unsupported("QP requires the HiGHS backend"); + if (o.solve.guarantee != Guarantee::Numerical) + throw Unsupported("QP supports Numerical guarantees only"); + if (o.solve.threads != 1 || o.solve.random_seed != 0 || o.solve.node_limit || !o.solve.primal_start.empty()) + throw Unsupported("QP supports one worker, zero seed, no node quota or primal start"); + if (!Detail::quadratic_arithmetic_supported()) + throw Unsupported("QP requires IEEE binary64 nearest rounding without fast-math or flush-to-zero"); +} +class Sum { + long double s_=0,c_=0; +public: + void add(long double x) { const auto n=s_+x; c_+=std::abs(s_)>=std::abs(x)?(s_-n)+x:(x-n)+s_; s_=n; } + long double value() const { return s_+c_; } +}; +long double activity(const std::vector& terms, const std::vector& x, double offset=0) { + Sum sum; sum.add(offset); + for (const auto& t : terms) sum.add(static_cast(t.coefficient)*x[t.variable.id]); + return sum.value(); +} +void check_kkt(const QuadraticSnapshot& q, const QuadraticRaw& raw, + const QuadraticOptions& o, QuadraticValidation& checked, const SolveBudget& budget) { + if (!raw.dual_valid || raw.row_duals.size()!=q.rows().size() || + raw.column_duals.size()!=q.variables().size()) return; + for (const auto& row : q.rows()) if (row.active && !std::isfinite(raw.row_duals[row.constraint.id])) return; + for (const auto& v : q.variables()) if (v.active && !std::isfinite(raw.column_duals[v.variable.id])) return; + checked.kkt_available = true; + bool valid = true; + const int sign = q.linear_part().sense == ObjectiveSense::Minimize ? 1 : -1; + std::vector residual(q.variables().size()); + std::vector scales(q.variables().size(),1); + for (const auto& v : q.variables()) if (v.active) { + const auto j=v.variable.id; + residual[j].add(sign*checked.original_gradient[j]); + residual[j].add(-raw.column_duals[j]); + scales[j]+=std::abs(checked.original_gradient[j])+std::abs(raw.column_duals[j]); + } + auto complementary = [&](long double d, long double slack, bool side_finite) { + if (d==0) return; + if (!side_finite) { valid=false; return; } + const auto magnitude=std::abs(d*slack); + if (!std::isfinite(magnitude)) { valid=false; return; } + checked.max_complementarity=std::max(checked.max_complementarity,static_cast(magnitude)); + if (magnitude>o.complementarity_tolerance) valid=false; + }; + for (const auto& row : q.rows()) if (row.active) { + checkpoint(budget); + const double d=raw.row_duals[row.constraint.id]; + const double side=d>=0?row.lower:row.upper; + complementary(d,std::isfinite(side)?activity(row.terms,raw.values,-side):0,std::isfinite(side)); + for (const auto& t : row.terms) { + const auto value=static_cast(t.coefficient)*d; + residual[t.variable.id].add(-value); + scales[t.variable.id]+=std::abs(value); + } + } + for (const auto& v : q.variables()) if (v.active) { + checkpoint(budget); + const auto j=v.variable.id; + const double d=raw.column_duals[j], side=d>=0?v.lower:v.upper; + complementary(d,static_cast(raw.values[j])-side,true); + const auto magnitude=std::abs(residual[j].value()); + if (!std::isfinite(magnitude) || !std::isfinite(scales[j])) { valid=false; continue; } + checked.max_stationarity=std::max(checked.max_stationarity,static_cast(magnitude)); + if (magnitude>o.stationarity_tolerance*scales[j]) valid=false; + } + checked.kkt_valid=valid; +} +void accept(const QuadraticSnapshot& q, const QuadraticOptions& o, const QuadraticRaw& raw, + QuadraticResult& out, const SolveBudget& budget) { + checkpoint(budget); + auto& result=out.result; + result.termination=raw.termination; + result.message=raw.message; + out.qp_iterations=raw.iterations; + out.regularization=raw.regularization; + if ((raw.objective && !std::isfinite(*raw.objective)) || + (raw.dual_estimate && !std::isfinite(*raw.dual_estimate))) { + result.termination=Termination::NumericalFailure; result.message="Nonfinite vendor objective evidence"; return; + } + out.vendor_objective=raw.objective; out.vendor_dual_estimate=raw.dual_estimate; + if (raw.model_id!=q.id() || raw.revision!=q.revision() || raw.active_variables!=result.active_variables) { + result.termination=Termination::NumericalFailure; result.message="QP raw result identity or layout mismatch"; return; + } + // Even an oracle declining value_valid cannot conceal a contradictory feasible witness. + if (raw.values.size()==q.variables().size()) out.checks=validate_quadratic(q,raw.values,o.solve.feasibility_tolerance); + const bool original_valid=out.checks.primal_valid && out.checks.objective_valid; + if (raw.termination==Termination::Infeasible) { + if (original_valid) { result.termination=Termination::NumericalFailure; result.message="Infeasibility contradicted by original primal witness"; } + else result.message="HiGHS numerical infeasibility conclusion; no independently checked certificate"; + return; + } + if (raw.termination==Termination::Unbounded || raw.termination==Termination::InfeasibleOrUnbounded) { + result.termination=Termination::NumericalFailure; result.message="Finite-box QP cannot be unbounded"; return; + } + if (!raw.value_valid || !original_valid || raw.residual_values.size()!=q.squares().size() || !raw.objective) { + if (raw.termination==Termination::Optimal) result.termination=Termination::NumericalFailure; + result.message="No independently validated original QP candidate"; return; + } + const int sign=q.linear_part().sense==ObjectiveSense::Minimize?1:-1; + for (std::size_t i=0;i(t.coefficient)*raw.values[t.variable.id]); + const auto mismatch=difference.value(); + if (!std::isfinite(raw.residual_values[i]) || !std::isfinite(mismatch) || + std::abs(mismatch)>o.solve.feasibility_tolerance) { + result.termination=Termination::NumericalFailure; result.message="Lifted residual disagrees with original square"; return; + } + } + // Vendor objective is normalized. Its agreement cannot establish optimality. + const long double mismatch=static_cast(*raw.objective)-sign*(*out.checks.original_objective); + if (std::abs(mismatch)>o.optimality_tolerance || !std::isfinite(mismatch)) { + result.termination=Termination::NumericalFailure; result.message="Vendor objective disagrees with original quadratic objective"; return; + } + check_kkt(q,raw,o,out.checks,budget); + std::vector duals(q.rows().size(),0); + if (raw.dual_valid && raw.row_duals.size()==duals.size()) { + bool finite=true; + for (const auto& row:q.rows()) if(row.active) finite &= std::isfinite(raw.row_duals[row.constraint.id]); + if (finite) duals=raw.row_duals; + } + const auto bound=Detail::quadratic_bound(q,raw.values,out.checks.square_values,duals,&budget); + out.checks.normalized_lower_bound=bound.normalized_lower; + out.checks.gap_upper_bound=bound.gap_upper; + out.checks.bound_valid=bound.normalized_lower.has_value(); + event("after_validation"); checkpoint(budget); + result.values=raw.values; result.objective=out.checks.original_objective; + result.solution_validated=true; + if (bound.normalized_lower) { + result.best_bound=sign*(*bound.normalized_lower); + try { result.update_gaps(q.linear_part().sense); } + catch (const ModelError&) { + result.best_bound.reset(); out.checks.bound_valid=false; + result.termination=Termination::NumericalFailure; + result.message="Verified feasible-set bound contradicts numerical primal objective"; return; + } + } + if (raw.termination==Termination::Optimal && + (!out.checks.kkt_valid || !out.checks.bound_valid || !bound.gap_upper || + *bound.gap_upper<0 || *bound.gap_upper>o.optimality_tolerance)) { + result.termination=Termination::NumericalFailure; + result.message="Original KKT or offset-independent verified gap did not close"; + } +} +#ifdef GECODE_OPTIMIZE_WITH_HIGHS +void ok(HighsStatus s, const char* what) { + if (s!=HighsStatus::kOk) throw Unsupported(std::string("HiGHS rejected/modified QP input: ")+what); +} +void range(double x, double maximum, bool matrix=false) { + if (std::isfinite(x) && (std::abs(x)>=maximum || (matrix && x!=0 && std::abs(x)<=1e-12))) + throw Unsupported("QP data exceeds supported HiGHS coefficient/bound range; rescale explicitly"); +} +Termination termination(HighsModelStatus s) { + switch(s) { + case HighsModelStatus::kOptimal: case HighsModelStatus::kModelEmpty:return Termination::Optimal; + case HighsModelStatus::kInfeasible:return Termination::Infeasible; + case HighsModelStatus::kUnbounded:return Termination::Unbounded; + case HighsModelStatus::kUnboundedOrInfeasible:return Termination::InfeasibleOrUnbounded; + case HighsModelStatus::kTimeLimit:return Termination::TimeLimit; + case HighsModelStatus::kIterationLimit:return Termination::IterationLimit; + case HighsModelStatus::kMemoryLimit:return Termination::MemoryLimit; + case HighsModelStatus::kInterrupt: case HighsModelStatus::kHighsInterrupt:return Termination::Cancelled; + default:return Termination::BackendError; + } +} +QuadraticRaw backend(const QuadraticSnapshot& q, const QuadraticOptions& o, const SolveBudget& budget) { + QuadraticRaw raw; raw.model_id=q.id(); raw.revision=q.revision(); + for(const auto& v:q.variables()) raw.active_variables.push_back(v.active); + const auto max=static_cast(std::numeric_limits::max()); + if (q.squares().size()>o.max_auxiliary_variables || q.squares().size()>max || + o.iteration_limit>static_cast(std::numeric_limits::max())) + throw Unsupported("QP auxiliary/iteration capacity exceeds supported range"); + HighsModel model; + auto& lp=model.lp_; + lp.sense_=ObjSense::kMinimize; + const int sign=q.linear_part().sense==ObjectiveSense::Minimize?1:-1; + lp.offset_=sign*q.linear_part().offset; range(lp.offset_,1e20); + std::vector columns(q.variables().size(),-1); + std::vector slots, rows; + for(const auto& v:q.variables()) if(v.active) { + checkpoint(budget); + if(slots.size()+q.squares().size()>=max) throw Unsupported("QP dimension exceeds HiGHS range"); + range(v.lower,1e20);range(v.upper,1e20); + columns[v.variable.id]=static_cast(slots.size());slots.push_back(v.variable.id); + lp.col_lower_.push_back(v.lower);lp.col_upper_.push_back(v.upper);lp.col_cost_.push_back(0); + } + for(const auto& t:q.linear_part().terms) { range(t.coefficient,1e20);lp.col_cost_[columns[t.variable.id]]=sign*t.coefficient; } + const auto originals=slots.size(); + for(std::size_t k=0;k=o.max_lifted_nonzeros || a.value_.size()>=max) throw Unsupported("QP nonzero capacity exceeded"); + range(value,1e15,true);a.index_.push_back(column);a.value_.push_back(value); + }; + for(const auto& row:q.rows()) if(row.active) { + checkpoint(budget); + range(row.lower,1e20);range(row.upper,1e20); + if(rows.size()+q.squares().size()>=max) throw Unsupported("QP row capacity exceeded"); + rows.push_back(row.constraint.id);lp.row_lower_.push_back(row.lower);lp.row_upper_.push_back(row.upper); + for(const auto& t:row.terms) add_entry(columns[t.variable.id],t.coefficient); + a.start_.push_back(static_cast(a.value_.size())); + } + for(std::size_t k=0;k(originals+k),1);a.start_.push_back(static_cast(a.value_.size())); + } + lp.num_col_=static_cast(lp.col_cost_.size());lp.num_row_=static_cast(lp.row_lower_.size()); + a.num_col_=lp.num_col_;a.num_row_=lp.num_row_; + auto& h=model.hessian_; + h.dim_=lp.num_col_;h.format_=HessianFormat::kTriangular;h.start_.assign(1,0); + for(HighsInt j=0;j(j)::quiet_NaN()); + raw.value_valid=true;raw.dual_valid=true;raw.row_duals.assign(q.rows().size(),0); + raw.column_duals.assign(q.variables().size(),0);raw.objective=lp.offset_; + raw.termination=Termination::Optimal; + for(const auto& row:q.rows()) if(row.active && (row.lower>0 || row.upper<0)) { raw.termination=Termination::Infeasible;raw.values.clear();raw.value_valid=false; } + return raw; + } + Highs highs; + ok(highs.setOptionValue("output_flag",false),"output"); + ok(highs.setOptionValue("threads",1),"threads"); + ok(highs.setOptionValue("random_seed",0),"seed"); + ok(highs.setOptionValue("solver","qpasm"),"solver"); + ok(highs.setOptionValue("small_matrix_value",1e-12),"matrix threshold"); + double regularization=0; +#ifdef GECODE_QUADRATIC_TEST_HOOKS + regularization=Detail::quadratic_test_regularization; +#endif + raw.regularization=regularization; + ok(highs.setOptionValue("qp_regularization_value",regularization),"regularization"); + ok(highs.setOptionValue("qp_iteration_limit",static_cast(o.iteration_limit)),"iterations"); + ok(highs.setOptionValue("primal_feasibility_tolerance",o.solve.feasibility_tolerance),"primal tolerance"); + ok(highs.setOptionValue("dual_feasibility_tolerance",std::max(1e-10,o.stationarity_tolerance)),"dual tolerance"); + ok(highs.passModel(model),"upload"); + const auto& loaded=highs.getModel(); + const auto& p=loaded.lp_; + if(p.num_col_!=lp.num_col_ || p.num_row_!=lp.num_row_ || p.offset_!=lp.offset_ || + p.sense_!=lp.sense_ || p.col_cost_!=lp.col_cost_ || p.col_lower_!=lp.col_lower_ || p.col_upper_!=lp.col_upper_ || + p.row_lower_!=lp.row_lower_ || p.row_upper_!=lp.row_upper_ || p.a_matrix_.value_.size()!=a.value_.size()) + throw Unsupported("HiGHS changed lifted QP model on upload"); + for(HighsInt i=0;i::quiet_NaN()); + if(solution.col_value.size()==lp.col_cost_.size()) { + for(std::size_t j=0;j(originals),solution.col_value.end()); + } else { raw.values.clear();raw.value_valid=false; } + if(solution.col_dual.size()==lp.col_cost_.size() && solution.row_dual.size()==lp.row_lower_.size()) { + raw.column_duals.assign(q.variables().size(),0);raw.row_duals.assign(q.rows().size(),0); + for(std::size_t j=0;j(std::max(HighsInt{0},info.qp_iteration_count)); + } + double dual=0; + if(solution.dual_valid && highs.getDualObjectiveValue(dual)==HighsStatus::kOk) raw.dual_estimate=dual; + return raw; +} +#endif +QuadraticResult run(const QuadraticSnapshot& q,const QuadraticOptions& o,const SolveBudget& budget) { + QuadraticResult out; + auto& r=out.result;r.model_id=q.id();r.revision=q.revision();r.backend="HiGHS QP";r.guarantee=o.solve.guarantee; + for(const auto& v:q.variables()) r.active_variables.push_back(v.active); + try { + o.validate();support(o);checkpoint(budget); + validate_structure(Detail::QuadraticAccess::core(q)); + event("after_preflight");checkpoint(budget); + QuadraticRaw raw; + bool supplied=false; +#ifdef GECODE_QUADRATIC_TEST_HOOKS + supplied=Detail::quadratic_test_oracle(q,o,raw); +#endif + if(!supplied) { +#ifdef GECODE_OPTIMIZE_WITH_HIGHS + r.backend_version=Highs{}.version();raw=backend(q,o,budget); +#else + throw Unsupported("HiGHS quadratic backend is not available in this build"); +#endif + } + checkpoint(budget);accept(q,o,raw,out,budget);checkpoint(budget); + } catch(const Stopped& s) { + // Candidates checked/published after a deadline are excluded completely. + r.termination=s.reason;r.solution_validated=false;r.values.clear();r.objective.reset();r.best_bound.reset(); + r.absolute_gap.reset();r.relative_gap.reset();out.checks={};r.message="QP shared budget exhausted before publication"; + } catch(const Unsupported& e) {r.termination=Termination::Unsupported;r.message=e.what();} + catch(const ModelError& e) {r.termination=Termination::InvalidModel;r.message=e.what();} + catch(const std::bad_alloc&) {r.termination=Termination::MemoryLimit;r.message="QP allocation failed";} + catch(const std::exception& e) {r.termination=Termination::BackendError;r.message=e.what();} + if (auto reason=budget.stop_reason(); reason && r.termination!=Termination::Unsupported && r.termination!=Termination::InvalidModel) { + r.termination=*reason;r.solution_validated=false;r.values.clear();r.objective.reset();r.best_bound.reset(); + r.absolute_gap.reset();r.relative_gap.reset();out.checks={}; + } + r.elapsed_seconds=budget.elapsed_seconds();return out; +} +QuadraticResult failure(ModelId id,Revision rev,const QuadraticOptions& o,Termination t,const char* msg) { + QuadraticResult out;out.result.model_id=id;out.result.revision=rev;out.result.guarantee=o.solve.guarantee; + out.result.termination=t;out.result.message=msg;return out; +} +} +QuadraticResult solve_quadratic(const QuadraticSnapshot& q,const QuadraticOptions& o) { + try {o.validate();SolveBudget b(o.solve);return run(q,o,b);} + catch(const ModelError& e) {return failure(q.id(),q.revision(),o,Termination::InvalidModel,e.what());} + catch(const std::bad_alloc&) {return failure(q.id(),q.revision(),o,Termination::MemoryLimit,"QP allocation failed");} +} +QuadraticResult solve_quadratic(const QuadraticModel& q,const QuadraticOptions& o) { + try {o.validate();support(o);SolveBudget b(o.solve);checkpoint(b);return run(q.snapshot(),o,b);} + catch(const Unsupported& e) {return failure(q.id(),q.revision(),o,Termination::Unsupported,e.what());} + catch(const Stopped& s) {return failure(q.id(),q.revision(),o,s.reason,"QP budget exhausted before snapshot");} + catch(const ModelError& e) {return failure(q.id(),q.revision(),o,Termination::InvalidModel,e.what());} + catch(const std::bad_alloc&) {return failure(q.id(),q.revision(),o,Termination::MemoryLimit,"QP allocation failed");} +} +BackendCapabilities quadratic_capabilities() { + BackendCapabilities c;c.name="HiGHS QP"; +#ifdef GECODE_OPTIMIZE_WITH_HIGHS + c.available=Detail::quadratic_arithmetic_supported();c.quadratic_programming=c.available;c.version=Highs{}.version(); +#endif + c.limitations={"Explicit finite-box continuous quadratic wrapper only", "Positive affine squares: convex min or concave max", "Numerical guarantee; checked original KKT and outward finite-box bound", "Requires IEEE binary64 nearest rounding without fast-math or flush-to-zero", "No arbitrary Hessian, MIQP, QCP, starts, session, native or Exact support", "Cooperative time limit; active-set cancellation checked before/after backend"}; + return c; +} +}} diff --git a/gecode/optimize/relaxation.cpp b/gecode/optimize/relaxation.cpp new file mode 100644 index 0000000000..337e3d1c1d --- /dev/null +++ b/gecode/optimize/relaxation.cpp @@ -0,0 +1,392 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { +namespace { +constexpr double infinity = std::numeric_limits::infinity(); +class UnsupportedRepair : public std::runtime_error { using std::runtime_error::runtime_error; }; +class InvalidRepair : public std::runtime_error { using std::runtime_error::runtime_error; }; + +class Sum { + long double sum_ = 0.0L, correction_ = 0.0L; +public: + void add(long double value) { + const long double next = sum_ + value; + if (!std::isfinite(value) || !std::isfinite(next)) + throw UnsupportedRepair("Repair arithmetic exceeds supported precision"); + correction_ += std::fabs(sum_) >= std::fabs(value) + ? (sum_ - next) + value : (value - next) + sum_; + sum_ = next; + } + long double value() const { + const auto result = sum_ + correction_; + if (!std::isfinite(result)) throw UnsupportedRepair("Repair accumulation exceeds supported precision"); + return result; + } +}; + +double finite(long double value) { + if (!std::isfinite(value) || std::fabs(value) > std::numeric_limits::max()) + throw UnsupportedRepair("Repair value cannot be represented as a finite double"); + return static_cast(value); +} + +long double evaluate(const std::vector& terms, const std::vector& values, + double offset = 0.0) { + Sum sum; + sum.add(offset); + for (const auto& term : terms) sum.add(static_cast(term.coefficient) * values.at(term.variable.id)); + return sum.value(); +} + +void check_penalty(RelaxationSide side, double penalty) { + if (side != RelaxationSide::Lower && side != RelaxationSide::Upper) + throw ModelError("Unknown relaxation side"); + if (!std::isfinite(penalty) || penalty <= 0) + throw ModelError("Relaxation penalties must be positive and finite"); +} + +bool indicator_variable(const ModelSnapshot& model, Variable variable) { + for (const auto& indicator : model.indicators) if (indicator.active) { + if (indicator.activator == variable || + (indicator.inactive_gate && *indicator.inactive_gate == variable)) return true; + for (const auto& term : indicator.terms) if (term.variable == variable) return true; + } + return false; +} + +struct Construction { + ModelSnapshot model; + std::vector mapping; + std::vector items; + ObjectiveData penalty; +}; + +Construction construct(const ModelSnapshot& source, const RelaxationOptions& options) { + Construction built; + std::set> seen; + for (const auto& selected : options.rows) { + check_penalty(selected.side, selected.penalty); + if (selected.row.model_id != source.model_id || selected.row.id >= source.rows.size() || + !source.rows[selected.row.id].active) throw ModelError("Relaxed row is foreign, absent, or deleted"); + if (!seen.emplace(false, selected.row.id, selected.side).second) + throw ModelError("A row side was selected more than once"); + const auto& row = source.rows[selected.row.id]; + if (row.indicator_origin) throw UnsupportedRepair("Generated indicator rows cannot be relaxed; original indicators remain hard"); + RelaxationItem item; + item.source_row = selected.row; item.side = selected.side; item.penalty = selected.penalty; + item.original_bound = selected.side == RelaxationSide::Lower ? row.lower : row.upper; + item.name = row.name.empty() ? "row " + std::to_string(selected.row.id) : row.name; + if (!std::isfinite(item.original_bound)) throw ModelError("Selected row side has no finite bound"); + built.items.push_back(std::move(item)); + } + for (const auto& selected : options.bounds) { + check_penalty(selected.side, selected.penalty); + if (selected.variable.model_id != source.model_id || selected.variable.id >= source.variables.size() || + !source.variables[selected.variable.id].active) throw ModelError("Relaxed variable is foreign, absent, or deleted"); + if (!seen.emplace(true, selected.variable.id, selected.side).second) + throw ModelError("A variable bound was selected more than once"); + const auto& variable = source.variables[selected.variable.id]; + if (variable.type == VariableType::SemiContinuous || variable.type == VariableType::SemiInteger) + throw UnsupportedRepair("Selected semi-variable bounds need a typed disjunctive relaxation; their zero domain remains hard"); + if (indicator_variable(source, selected.variable)) + throw UnsupportedRepair("Indicator-participating variable bounds cannot be relaxed without rebuilding the guarded formulation"); + RelaxationItem item; + item.source_variable = selected.variable; item.side = selected.side; item.penalty = selected.penalty; + item.original_bound = selected.side == RelaxationSide::Lower ? variable.lower : variable.upper; + item.name = variable.name.empty() ? "variable " + std::to_string(selected.variable.id) : variable.name; + if (!std::isfinite(item.original_bound)) throw ModelError("Selected variable side has no finite bound"); + built.items.push_back(std::move(item)); + } + std::sort(built.items.begin(), built.items.end(), [](const RelaxationItem& a, const RelaxationItem& b) { + return std::make_tuple(a.source_variable.has_value(), a.source_row ? a.source_row->id : a.source_variable->id, a.side) < + std::make_tuple(b.source_variable.has_value(), b.source_row ? b.source_row->id : b.source_variable->id, b.side); + }); + Model identity; + // Public snapshots can carry an arbitrary otherwise well-formed owner. + if (identity.id() == source.model_id) identity = Model{}; + built.model = source; + auto& model = built.model; + model.model_id = identity.id(); + model.revision = 1; + // Active globals were rejected before construction. Inactive records have no + // semantic effect and no live constraints refer to their global handles. + model.globals.clear(); + auto variable = [&](Variable& handle) { handle.model_id = model.model_id; }; + auto row = [&](Constraint& handle) { handle.model_id = model.model_id; }; + auto indicator = [&](Indicator& handle) { handle.model_id = model.model_id; }; + auto terms = [&](std::vector& expression) { for (auto& term : expression) variable(term.variable); }; + for (auto& data : model.variables) { + variable(data.variable); + if (data.indicator_origin) indicator(*data.indicator_origin); + built.mapping.push_back(data.variable); + } + for (auto& data : model.rows) { + row(data.constraint); terms(data.terms); + if (data.indicator_origin) indicator(*data.indicator_origin); + } + terms(model.objective.terms); + for (auto& data : model.indicators) { + indicator(data.indicator); variable(data.activator); terms(data.terms); + if (data.inactive_gate) variable(*data.inactive_gate); + for (auto& handle : data.generated_rows) row(handle); + for (auto& domain : data.domains) variable(domain.variable); + } + for (auto& item : built.items) { + if (model.variables.size() >= std::numeric_limits::max() || + model.rows.size() >= std::numeric_limits::max()) + throw UnsupportedRepair("Private repair model exceeds handle capacity"); + item.slack = {model.model_id, static_cast(model.variables.size())}; + item.penalty_row = {model.model_id, static_cast(model.rows.size())}; + VariableData slack; + slack.variable = item.slack; slack.lower = 0; slack.upper = infinity; + slack.name = "__repair_slack_" + std::to_string(item.slack.id); + model.variables.push_back(std::move(slack)); + RowData penalty_row; + penalty_row.constraint = item.penalty_row; + penalty_row.name = "__repair_side_" + std::to_string(item.penalty_row.id); + if (item.source_row) { + auto& original_row = model.rows[item.source_row->id]; + penalty_row.terms = original_row.terms; + if (item.side == RelaxationSide::Lower) original_row.lower = -infinity; + else original_row.upper = infinity; + } else { + auto& original_variable = model.variables[item.source_variable->id]; + penalty_row.terms.push_back({original_variable.variable, 1.0}); + if (item.side == RelaxationSide::Lower) + original_variable.lower = original_variable.type == VariableType::Binary ? 0.0 : -infinity; + else original_variable.upper = original_variable.type == VariableType::Binary ? 1.0 : infinity; + } + penalty_row.terms.push_back({item.slack, item.side == RelaxationSide::Lower ? 1.0 : -1.0}); + if (item.side == RelaxationSide::Lower) penalty_row.lower = item.original_bound; + else penalty_row.upper = item.original_bound; + model.rows.push_back(std::move(penalty_row)); + built.penalty.terms.push_back({item.slack, item.penalty}); + } + validate_structure(model); + return built; +} + +long double residuals(const ModelSnapshot& source, const std::vector& values, + std::vector& items) { + Sum total; + for (auto& item : items) { + const long double activity = item.source_row ? evaluate(source.rows[item.source_row->id].terms, values) + : values.at(item.source_variable->id); + // Include the bound before reducing the compensated sum: activity itself + // can round to the bound and erase a small but material original residual. + Sum residual; + const long double sign = item.side == RelaxationSide::Lower ? -1.0L : 1.0L; + residual.add(-sign * item.original_bound); + if (item.source_row) { + for (const auto& term : source.rows[item.source_row->id].terms) + residual.add(sign * term.coefficient * values.at(term.variable.id)); + } else { + residual.add(sign * activity); + } + const long double violation = std::max(0.0L, residual.value()); + const long double weighted = static_cast(item.penalty) * violation; + item.activity = finite(activity); item.violation = finite(violation); + item.weighted_violation = finite(weighted); item.slack_value = values.at(item.slack.id); + total.add(weighted); + } + return total.value(); +} + +long double allowance(long double a, long double b, double feasibility, long double multiplier = 1) { + const auto roundoff = 64.0L * std::numeric_limits::epsilon() * + std::max({1.0L, std::fabs(a), std::fabs(b)}); + return std::max(static_cast(feasibility) * multiplier, roundoff); +} + +void verify_layout(const SolveResult& result, const ModelSnapshot& model) { + if (result.model_id != model.model_id || result.revision != model.revision || + result.values.size() != model.variables.size() || result.active_variables.size() != model.variables.size()) + throw InvalidRepair("Repair candidate has inconsistent model identity or slot layout"); + for (std::size_t i = 0; i < model.variables.size(); ++i) + if (result.active_variables[i] != model.variables[i].active) + throw InvalidRepair("Repair candidate active mask does not match its private model"); +} + +double verify_optimum(const SolveResult& result, const ObjectiveData& objective, + const ModelSnapshot& model, double feasibility) { + verify_layout(result, model); + if (!result.has_solution() || result.termination != Termination::Optimal || + result.guarantee != Guarantee::Numerical || !result.best_bound || !std::isfinite(*result.best_bound)) + throw InvalidRepair("Completed repair stage lacks a validated candidate and finite numerical bound"); + const double actual = finite(evaluate(objective.terms, result.values, objective.offset)); + if (std::fabs(static_cast(actual) - *result.objective) > + allowance(actual, *result.objective, feasibility)) + throw InvalidRepair("Repair stage objective disagrees with independently recomputed activity"); + auto checked = result; + checked.objective = actual; + try { checked.update_gaps(objective.sense); } + catch (const ModelError&) { throw InvalidRepair("Repair stage global bound has inconsistent ordering"); } + if (!checked.absolute_gap || !std::isfinite(*checked.absolute_gap) || + *checked.absolute_gap > allowance(actual, *checked.best_bound, feasibility)) + throw InvalidRepair("Repair stage has not closed its independently recomputed numerical gap"); + return actual; +} + +RelaxationResult run(const ModelSnapshot& source, const RelaxationOptions& options, SolveBudget& budget) { + RelaxationResult output; + output.source_model_id = source.model_id; output.source_revision = source.revision; + auto finish = [&]() { + if (budget.expired()) { + output.termination = budget.stop_reason().value_or(Termination::Unknown); + output.message = "Shared repair budget stopped; no newly checked repair is promoted"; + output.repair_validated = output.minimum_violation_established = output.original_objective_optimized = false; + output.minimum_weighted_violation.reset(); + } + output.elapsed_seconds = budget.elapsed_seconds(); + return std::move(output); + }; + try { + validate_structure(source); + for (std::size_t i = 0; i < source.globals.size(); ++i) { + if (source.globals[i].global.model_id != source.model_id || source.globals[i].global.id != i) + throw ModelError("Global metadata identity does not match its source model"); + if (source.globals[i].active) throw UnsupportedRepair("Feasibility relaxation of models with active globals is not supported"); + } + if (options.solve.guarantee != Guarantee::Numerical) + throw UnsupportedRepair("Weighted feasibility relaxation provides Numerical evidence only"); + if (!options.solve.primal_start.empty()) + throw UnsupportedRepair("Repair primal starts require explicit private slack mapping and are not yet supported"); + if (options.optimize_original_objective && options.solve.node_limit) + throw UnsupportedRepair("Lexicographic repair node budgets need cumulative consumed-node reporting"); + if (budget.expired()) return finish(); + auto built = construct(source, options); + const auto penalty = std::move(built.penalty); + output.private_model = std::move(built.model); + output.private_variables = std::move(built.mapping); + output.items = std::move(built.items); + std::vector objectives; + objectives.push_back({penalty, 0.0, 0.0, "minimum weighted violation"}); + if (options.optimize_original_objective) + objectives.push_back({output.private_model->objective, 0.0, 0.0, "original objective among minimum-violation repairs"}); + auto solve_options = options.solve; + // Source-model starts lack the private owner/slacks and may be infeasible. + solve_options.primal_start.clear(); + solve_options.cancellation = budget.cancellation(); + solve_options.time_limit_seconds = budget.remaining_seconds(); + output.workflow = solve_lexicographic(*output.private_model, objectives, solve_options); + output.termination = output.workflow.termination; output.message = output.workflow.message; + if (budget.expired()) return finish(); + for (const auto& stage : output.workflow.stages) + if (stage.result.has_solution()) verify_layout(stage.result, *output.private_model); + if (output.workflow.completed_stages >= 1) { + const auto& stage = output.workflow.stages.at(0); + if (!stage.completed || !validate(*output.private_model, stage.result.values, + options.solve.feasibility_tolerance, options.solve.integrality_tolerance).valid) + throw InvalidRepair("Minimum-violation stage failed independent private-model validation"); + const double optimum = verify_optimum(stage.result, penalty, *output.private_model, options.solve.feasibility_tolerance); + if (optimum < 0) throw InvalidRepair("Nonnegative weighted violation has a negative reported optimum"); + auto first_items = output.items; + const auto first_residual = residuals(source, stage.result.values, first_items); + Sum weights; for (const auto& item : output.items) weights.add(item.penalty); + const auto multiplier = std::max(1.0L, weights.value()); + if (std::fabs(first_residual - optimum) > allowance(first_residual, optimum, + options.solve.feasibility_tolerance, multiplier)) + throw InvalidRepair("Minimum penalty does not agree with original-unit violations"); + output.minimum_weighted_violation = optimum; + output.minimum_violation_established = true; + if (options.optimize_original_objective) { + const auto exact_activity = evaluate(penalty.terms, stage.result.values); + double lock = finite(exact_activity); + if (static_cast(lock) > exact_activity) lock = std::nextafter(lock, -infinity); + RowData row; + row.constraint = {output.private_model->model_id, static_cast(output.private_model->rows.size())}; + row.terms = penalty.terms; row.upper = lock; row.name = "__repair_minimum_violation"; + output.violation_lock = row.constraint; + output.private_model->rows.push_back(std::move(row)); + } + } + if (output.workflow.has_solution()) { + const auto& result = output.workflow.final_solution; + verify_layout(result, *output.private_model); + const auto checked = validate(*output.private_model, result.values, + options.solve.feasibility_tolerance, options.solve.integrality_tolerance); + if (!checked.valid || result.model_id != output.private_model->model_id || + result.revision != output.private_model->revision) + throw InvalidRepair("Candidate failed independent validation of hard constraints, penalty rows, or the minimum lock"); + auto items = output.items; + const auto weighted = residuals(source, result.values, items); + if (output.minimum_violation_established) { + Sum weights; for (const auto& item : items) weights.add(item.penalty); + if (std::fabs(weighted - *output.minimum_weighted_violation) > allowance(weighted, + *output.minimum_weighted_violation, options.solve.feasibility_tolerance, std::max(1.0L, weights.value()))) + throw InvalidRepair("Final repair does not retain the established minimum violation"); + } + std::vector original(result.values.begin(), result.values.begin() + source.variables.size()); + auto original_check = validate(source, original, options.solve.feasibility_tolerance, + options.solve.integrality_tolerance); + for (const auto& item : items) { + auto& maximum = item.source_row ? original_check.max_row_violation : original_check.max_bound_violation; + maximum = std::max(maximum, *item.violation); + if (*item.violation > options.solve.feasibility_tolerance) { + original_check.valid = false; + if (original_check.message.empty()) original_check.message = "Selected original sides remain violated"; + } + } + output.weighted_violation = finite(weighted); + output.original_objective = original_check.objective; + output.original_values = std::move(original); + output.original_validation = std::move(original_check); + output.items = std::move(items); + output.repair_validated = true; + } + if (options.optimize_original_objective && output.workflow.completed_numerically() && + output.workflow.completed_stages == 2) { + if (!output.minimum_violation_established || !output.has_repair()) + throw InvalidRepair("Original-objective completion lacks a validated minimum-violation repair"); + verify_optimum(output.workflow.stages.at(1).result, output.private_model->objective, + *output.private_model, options.solve.feasibility_tolerance); + output.original_objective_optimized = true; + } + if (output.termination == Termination::Optimal && output.minimum_violation_established) + output.message = output.original_objective_optimized + ? "Minimum weighted violation and the original objective within that repair region were established numerically" + : "Minimum weighted violation established numerically; original feasibility is reported separately"; + } catch (const UnsupportedRepair& error) { + output.termination = Termination::Unsupported; output.message = error.what(); + } catch (const InvalidRepair& error) { + output.termination = Termination::NumericalFailure; output.message = error.what(); + output.minimum_violation_established = output.original_objective_optimized = false; + output.minimum_weighted_violation.reset(); + } catch (const ModelError& error) { + output.termination = Termination::InvalidModel; output.message = error.what(); + } catch (const std::bad_alloc&) { + output.termination = Termination::MemoryLimit; output.message = "Repair allocation failed"; + } catch (const std::exception& error) { + output.termination = Termination::BackendError; output.message = error.what(); + } + return finish(); +} + +RelaxationResult failure(ModelId id, Revision revision, Termination reason, const std::string& message) { + RelaxationResult result; + result.source_model_id = id; result.source_revision = revision; + result.termination = reason; result.message = message; + return result; +} +} + +RelaxationResult relax_feasibility(const ModelSnapshot& model, const RelaxationOptions& options) { + try { SolveBudget budget(options.solve); return run(model, options, budget); } + catch (const ModelError& error) { return failure(model.model_id, model.revision, Termination::InvalidModel, error.what()); } + catch (const std::bad_alloc&) { return failure(model.model_id, model.revision, Termination::MemoryLimit, "Repair allocation failed"); } +} +RelaxationResult relax_feasibility(const Model& model, const RelaxationOptions& options) { + try { SolveBudget budget(options.solve); return run(model.snapshot(), options, budget); } + catch (const ModelError& error) { return failure(model.id(), model.revision(), Termination::InvalidModel, error.what()); } + catch (const std::bad_alloc&) { return failure(model.id(), model.revision(), Termination::MemoryLimit, "Repair allocation failed"); } +} + +}} diff --git a/gecode/optimize/relaxation.hpp b/gecode/optimize/relaxation.hpp new file mode 100644 index 0000000000..48e431a860 --- /dev/null +++ b/gecode/optimize/relaxation.hpp @@ -0,0 +1,95 @@ +/* Explicit weighted L1 repairs on a distinct private model. */ +#ifndef GECODE_OPTIMIZE_RELAXATION_HPP +#define GECODE_OPTIMIZE_RELAXATION_HPP + +#include +#include + +namespace Gecode { namespace Optimize { + +enum class RelaxationSide { Lower, Upper }; + +struct RowRelaxation { + Constraint row; + RelaxationSide side = RelaxationSide::Lower; + double penalty = 1.0; +}; +struct BoundRelaxation { + Variable variable; + RelaxationSide side = RelaxationSide::Lower; + double penalty = 1.0; +}; +struct RelaxationOptions { + SolveOptions solve; + std::vector rows; + std::vector bounds; + bool optimize_original_objective = false; +}; + +struct RelaxationItem { + // Exactly one source handle is set. Private slack/row handles have a distinct + // model owner and cannot be passed back to the original model as its handles. + std::optional source_row; + std::optional source_variable; + RelaxationSide side = RelaxationSide::Lower; + std::string name; + double original_bound = 0.0; + double penalty = 1.0; + Variable slack; + Constraint penalty_row; + // Absent until a repaired candidate passes independent checks. Violations + // are recomputed in original units, rather than inferred from slack values. + std::optional activity; + std::optional violation; + std::optional weighted_violation; + std::optional slack_value; +}; + +struct RelaxationResult { + ModelId source_model_id = 0; + Revision source_revision = 0; + Termination termination = Termination::Unknown; + Guarantee guarantee = Guarantee::Numerical; + std::string message; + double elapsed_seconds = 0.0; + std::optional private_model; + // One private handle per original variable slot, including tombstones. + std::vector private_variables; + std::vector items; + // Present when phase two was requested and a minimum-violation lock was + // established. The returned private model includes this additional hard row. + std::optional violation_lock; + // All workflow results belong to private_model, never the original model. + LexicographicResult workflow; + bool repair_validated = false; + bool minimum_violation_established = false; + // Optimal only among repairs with minimum weighted violation, not an + // unconstrained optimum of the original objective or original feasible set. + bool original_objective_optimized = false; + std::optional minimum_weighted_violation; + std::optional weighted_violation; + std::optional original_objective; + std::vector original_values; + ValidationReport original_validation; + + bool has_repair() const noexcept { return repair_validated && workflow.has_solution(); } +}; + +/** + * Minimize the sum of positive finite penalties times selected side violations. + * Unselected sides, integrality and intrinsic binary/semi domains remain hard. + * Binary bound selection relaxes a narrower stored bound only as far as [0,1]. + * Selected semi bounds, generated indicator rows, indicator-participating + * variable bounds, active globals and primal starts are explicitly Unsupported. + * Optional phase two optimizes the original objective with zero requested + * degradation of minimum weighted violation. All completion is numerical and + * tolerance-qualified; no original feasible SolveResult is manufactured. + * The original model is not mutated. See RELAXATION.md for support/limits. + */ +RelaxationResult relax_feasibility(const ModelSnapshot& model, + const RelaxationOptions& options = {}); +RelaxationResult relax_feasibility(const Model& model, + const RelaxationOptions& options = {}); + +}} +#endif diff --git a/gecode/optimize/result.cpp b/gecode/optimize/result.cpp new file mode 100644 index 0000000000..f1a7e447fe --- /dev/null +++ b/gecode/optimize/result.cpp @@ -0,0 +1,221 @@ +#include "result.hpp" + +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { + +const char* to_string(Termination termination) noexcept { + switch (termination) { + case Termination::Unknown: return "unknown"; + case Termination::Optimal: return "optimal"; + case Termination::Infeasible: return "infeasible"; + case Termination::Unbounded: return "unbounded"; + case Termination::InfeasibleOrUnbounded: return "infeasible_or_unbounded"; + case Termination::TimeLimit: return "time_limit"; + case Termination::NodeLimit: return "node_limit"; + case Termination::MemoryLimit: return "memory_limit"; + case Termination::IterationLimit: return "iteration_limit"; + case Termination::SolutionLimit: return "solution_limit"; + case Termination::ObjectiveLimit: return "objective_limit"; + case Termination::Cancelled: return "cancelled"; + case Termination::NumericalFailure: return "numerical_failure"; + case Termination::Unsupported: return "unsupported"; + case Termination::InvalidModel: return "invalid_model"; + case Termination::BackendError: return "backend_error"; + } + return "unknown"; +} + +void CancellationToken::cancel() noexcept { + cancelled_.store(true, std::memory_order_release); +} + +bool CancellationToken::cancelled() const noexcept { + return cancelled_.load(std::memory_order_acquire); +} + +namespace { +void require_nonnegative_finite(double value, const char* name) { + if (!std::isfinite(value) || value < 0.0) + throw ModelError(std::string(name) + " must be nonnegative and finite"); +} + +double nonnegative_double(long double value) noexcept { + if (value > static_cast(std::numeric_limits::max())) + return std::numeric_limits::infinity(); + return static_cast(value); +} +} + +void SolveOptions::validate() const { + switch (backend) { + case Backend::Auto: + case Backend::Highs: + case Backend::Native: + break; + default: + throw ModelError("invalid backend option"); + } + switch (guarantee) { + case Guarantee::Numerical: + case Guarantee::Exact: + case Guarantee::Certified: + break; + default: + throw ModelError("invalid guarantee option"); + } + if (std::isnan(time_limit_seconds) || time_limit_seconds < 0.0) + throw ModelError("time limit must be nonnegative or positive infinity"); + if (threads < 1) + throw ModelError("threads must be positive"); + if (random_seed < 0) + throw ModelError("random seed must be nonnegative"); + require_nonnegative_finite(relative_gap, "relative gap"); + require_nonnegative_finite(absolute_gap, "absolute gap"); + if (!std::isfinite(feasibility_tolerance) || feasibility_tolerance <= 0.0) + throw ModelError("feasibility tolerance must be positive and finite"); + if (!std::isfinite(integrality_tolerance) || integrality_tolerance <= 0.0 || + integrality_tolerance >= 0.5) + throw ModelError("integrality tolerance must be finite and between 0 and 0.5"); + for (const auto& entry : primal_start) + if (!std::isfinite(entry.value)) + throw ModelError("primal start values must be finite"); +} + +struct SolveBudget::State { + using Clock = std::chrono::steady_clock; + const Clock::time_point start; + const double time_limit; + const std::optional node_limit; + const std::shared_ptr cancellation; + std::atomic nodes{0}; + + State(const SolveOptions& options, Clock::time_point started) + : start(started), time_limit(options.time_limit_seconds), node_limit(options.node_limit), + cancellation(options.cancellation ? options.cancellation + : std::make_shared()) {} +}; + +SolveBudget::SolveBudget(const SolveOptions& options) { + const auto started = State::Clock::now(); + options.validate(); + state_ = std::make_shared(options, started); +} + +SolveBudget SolveBudget::slice(double seconds, std::uint64_t node_allowance) const { + if (std::isnan(seconds) || seconds < 0.0) + throw ModelError("budget slice time must be nonnegative or positive infinity"); + auto result=*this; + result.local_deadline_=std::min(local_deadline_,elapsed_seconds()+seconds); + const auto used=nodes(), maximum=std::numeric_limits::max(); + const auto limit=node_allowance>maximum-used ? maximum:used+node_allowance; + result.local_node_limit_=local_node_limit_ ? std::min(*local_node_limit_,limit):limit; + return result; +} + +double SolveBudget::elapsed_seconds() const noexcept { + return std::max(0.0, std::chrono::duration( + State::Clock::now() - state_->start).count()); +} + +double SolveBudget::remaining_seconds() const noexcept { + return std::max(0.0, std::min(state_->time_limit,local_deadline_) - elapsed_seconds()); +} + +bool SolveBudget::cancelled() const noexcept { + return state_->cancellation->cancelled(); +} + +bool SolveBudget::time_limit_reached() const noexcept { + return elapsed_seconds() >= std::min(state_->time_limit,local_deadline_); +} + +bool SolveBudget::node_limit_reached() const noexcept { + const auto used=nodes(); + return (state_->node_limit && used >= *state_->node_limit) || + (local_node_limit_ && used >= *local_node_limit_); +} + +bool SolveBudget::expired() const noexcept { + return stop_reason().has_value(); +} + +std::optional SolveBudget::stop_reason() const noexcept { + if (cancelled()) return Termination::Cancelled; + if (time_limit_reached()) return Termination::TimeLimit; + if (node_limit_reached()) return Termination::NodeLimit; + return std::nullopt; +} + +void SolveBudget::add_nodes(std::uint64_t count) noexcept { + auto old = state_->nodes.load(std::memory_order_relaxed); + const auto maximum = std::numeric_limits::max(); + for (;;) { + const auto next = count > maximum - old ? maximum : old + count; + if (state_->nodes.compare_exchange_weak(old, next, std::memory_order_relaxed)) + return; + } +} + +std::uint64_t SolveBudget::nodes() const noexcept { + return state_->nodes.load(std::memory_order_relaxed); +} + +std::shared_ptr SolveBudget::cancellation() const noexcept { + return state_->cancellation; +} + +bool SolveResult::has_solution() const noexcept { + if (!solution_validated || !objective || !std::isfinite(*objective) || + values.size() != active_variables.size()) + return false; + for (std::size_t i = 0; i < values.size(); ++i) + if (active_variables[i] && !std::isfinite(values[i])) + return false; + return true; +} + +double SolveResult::value(Variable variable) const { + if (variable.model_id != model_id) + throw ModelError("variable belongs to a different model than this result"); + if (variable.id >= active_variables.size() || !active_variables[variable.id]) + throw ModelError("variable is absent or deleted in this result snapshot"); + if (!has_solution()) + throw ModelError("result has no validated solution"); + return values[variable.id]; +} + +void SolveResult::update_gaps(ObjectiveSense sense) { + absolute_gap.reset(); + relative_gap.reset(); + if (sense != ObjectiveSense::Minimize && sense != ObjectiveSense::Maximize) + throw ModelError("invalid objective sense for gap computation"); + if (objective && !std::isfinite(*objective)) + throw ModelError("objective must be finite for gap computation"); + if (best_bound && std::isnan(*best_bound)) + throw ModelError("best bound must not be NaN"); + if (!objective || !best_bound) + return; + + if ((sense == ObjectiveSense::Minimize && *best_bound > *objective) || + (sense == ObjectiveSense::Maximize && *best_bound < *objective)) + throw ModelError("global best bound is inconsistent with the objective"); + if (!std::isfinite(*best_bound)) + return; + + const long double primal = *objective; + const long double dual = *best_bound; + const long double gap = sense == ObjectiveSense::Minimize + ? primal - dual : dual - primal; + absolute_gap = nonnegative_double(gap); + const long double scale = std::max({1.0L, std::fabs(primal), std::fabs(dual)}); + // Some platforms implement long double as double. Dividing before the + // subtraction avoids an overflowing absolute gap hiding a finite ratio. + relative_gap = nonnegative_double(std::isfinite(gap) + ? gap / scale : std::fabs(primal / scale - dual / scale)); +} + +}} diff --git a/gecode/optimize/result.hpp b/gecode/optimize/result.hpp new file mode 100644 index 0000000000..23963830ff --- /dev/null +++ b/gecode/optimize/result.hpp @@ -0,0 +1,168 @@ +/* Results and shared solve budgets for the additive optimization API. */ +#ifndef GECODE_OPTIMIZE_RESULT_HPP +#define GECODE_OPTIMIZE_RESULT_HPP + +#include "types.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { + +/** The reason a solve stopped, independent of whether it found a solution. */ +enum class Termination { + Unknown, + Optimal, + Infeasible, + Unbounded, + InfeasibleOrUnbounded, + TimeLimit, + NodeLimit, + MemoryLimit, + IterationLimit, + SolutionLimit, + ObjectiveLimit, + Cancelled, + NumericalFailure, + Unsupported, + InvalidModel, + BackendError +}; + +const char* to_string(Termination termination) noexcept; + +/** Monotonic cancellation: a cancelled token cannot be reset or reused. */ +class CancellationToken { +public: + void cancel() noexcept; + bool cancelled() const noexcept; + +private: + std::atomic cancelled_{false}; +}; + +/** One original-variable value in a possibly partial primal start. */ +struct StartValue { + Variable variable; + double value = 0.0; +}; + +struct SolveOptions { + Backend backend = Backend::Auto; + Guarantee guarantee = Guarantee::Numerical; + double time_limit_seconds = std::numeric_limits::infinity(); + int threads = 1; + int random_seed = 0; + double relative_gap = 1e-4; + double absolute_gap = 1e-6; + double feasibility_tolerance = 1e-7; + double integrality_tolerance = 1e-6; + std::shared_ptr cancellation; + std::optional node_limit; + /** + * Optional sparse primal start. Values must be finite, unique, and satisfy + * their original variable domains. A complete start must satisfy every + * original constraint, including retained indicators and globals. + * A partial start is a hint, not a validated incumbent or a fixed assignment. + * HiGHS accepts partial hints. Native requires a complete exact assignment + * after any deterministic completion of live indicator inactivity gates; + * unresolved partial starts are Unsupported. See solve_native(). + */ + std::vector primal_start; + + /** + * Validate values, throwing ModelError for malformed options. + * Exact and Certified requests are valid options; a backend that cannot + * deliver the requested guarantee must return Termination::Unsupported. + * Gap tolerances may be zero. Feasibility/integrality tolerances must be + * positive and finite; integrality tolerance must also be less than 0.5. + */ + void validate() const; +}; + +/** + * Copies share a monotonic clock, cancellation token and cumulative node count. + * All stages of one solve should use this same budget rather than restarting + * the deadline. Callers explicitly add newly consumed nodes (not a cumulative + * backend total on every poll). Concurrent node additions saturate at UINT64_MAX. + */ +class SolveBudget { +public: + explicit SolveBudget(const SolveOptions& options); + + /** A locally capped view sharing the original clock, nodes and cancellation. + * Exhausting a slice does not cancel its parent. Nested slices cannot relax + * any parent cap. All nodes consumed through a slice count globally. + */ + SolveBudget slice(double seconds, std::uint64_t node_allowance) const; + + double elapsed_seconds() const noexcept; + double remaining_seconds() const noexcept; + bool cancelled() const noexcept; + bool time_limit_reached() const noexcept; + bool node_limit_reached() const noexcept; + bool expired() const noexcept; + /** Cancellation takes precedence over time, then node limits. */ + std::optional stop_reason() const noexcept; + + void add_nodes(std::uint64_t count = 1) noexcept; + std::uint64_t nodes() const noexcept; + std::shared_ptr cancellation() const noexcept; + +private: + struct State; + std::shared_ptr state_; + double local_deadline_ = std::numeric_limits::infinity(); + std::optional local_node_limit_; +}; + +/** + * An owning historical result. Model edits do not invalidate this snapshot. + * Values and active_variables use original variable slots, including tombstones. + * Only an independent original-model validator may set solution_validated. + */ +struct SolveResult { + ModelId model_id = 0; + Revision revision = 0; + std::string backend; + std::string backend_version; + Termination termination = Termination::Unknown; + std::string message; + std::optional objective; + std::optional best_bound; + std::optional absolute_gap; + std::optional relative_gap; + std::vector values; + std::vector active_variables; + bool solution_validated = false; + Guarantee guarantee = Guarantee::Numerical; + double elapsed_seconds = 0.0; + std::optional native_backend_gap; + /** Backend accepted the hint; this does not claim it became an incumbent. */ + bool start_submitted = false; + + /** Requires a finite validated objective and every active slot to be finite. */ + bool has_solution() const noexcept; + /** Reject absent solutions, foreign handles, invalid slots and tombstones. */ + double value(Variable variable) const; + + /** + * Recompute gaps from the original-sense objective and global best_bound. + * Missing values or a valid infinite bound leave both gaps unavailable. + * NaNs, non-finite objectives and inconsistent bound ordering throw ModelError; + * previous gaps are cleared before validation, never silently clamped to zero. + * The common normalized relative gap is abs(objective-best_bound) divided by + * max(1, abs(objective), abs(best_bound)); it is finite for finite inputs, + * including zero objectives. The absolute gap can overflow double to positive + * infinity. native_backend_gap preserves the vendor convention separately. + */ + void update_gaps(ObjectiveSense sense); +}; + +}} +#endif diff --git a/gecode/optimize/scenarios.cpp b/gecode/optimize/scenarios.cpp new file mode 100644 index 0000000000..0e90be4d98 --- /dev/null +++ b/gecode/optimize/scenarios.cpp @@ -0,0 +1,436 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { +#ifdef GECODE_OPTIMIZE_TEST_SCENARIOS +namespace Detail { +SolveResult scenario_test_solve(const ModelSnapshot&, const SolveOptions&, SolveSession*); +void scenario_test_checkpoint(const char*, std::size_t); +} +#endif +namespace { +struct Stopped { Termination reason; const char* message; }; +struct Unsupported : std::runtime_error { using std::runtime_error::runtime_error; }; +struct EvidenceError : std::runtime_error { using std::runtime_error::runtime_error; }; +// SolveBudget owns the shared cancellation/node state. This enclosing clock +// also includes option validation and its construction in the total allowance. +class BatchBudget { + SolveBudget shared_; + std::chrono::steady_clock::time_point started_; + double seconds_; +public: + BatchBudget(const SolveOptions& options,std::chrono::steady_clock::time_point started) + : shared_(options),started_(started),seconds_(options.time_limit_seconds) {} + double remaining_seconds() const noexcept { + const double elapsed=std::chrono::duration(std::chrono::steady_clock::now()-started_).count(); + return std::max(0.0,seconds_-elapsed); + } + std::optional stop_reason() const noexcept { + if(shared_.cancelled())return Termination::Cancelled; + if(remaining_seconds()==0)return Termination::TimeLimit; + if(shared_.node_limit_reached())return Termination::NodeLimit; + return {}; + } + std::shared_ptr cancellation() const noexcept{return shared_.cancellation();} +}; +void checkpoint(const BatchBudget& budget) { + if (const auto reason=budget.stop_reason()) throw Stopped{*reason,"Whole scenario batch budget stopped"}; +} +struct Meter { + const BatchBudget& budget; + std::size_t limit, used=0; + void tick(std::size_t count=1) { + checkpoint(budget); + if(count>limit-used)throw Stopped{Termination::IterationLimit,"Scenario coordinator element-visit cap reached"}; + used+=count; + } +}; +void tick(Meter* meter,std::size_t count=1){if(meter)meter->tick(count);} +void event(const char* point,std::size_t index) { +#ifdef GECODE_OPTIMIZE_TEST_SCENARIOS + Detail::scenario_test_checkpoint(point,index); +#else + (void)point;(void)index; +#endif +} +void storage(std::size_t count,std::size_t limit,const char* message) { + if(count>limit)throw Stopped{Termination::MemoryLimit,message}; +} +void product(std::size_t a,std::size_t b,std::size_t limit,const char* message) { + if(a && b>limit/a)throw Stopped{Termination::MemoryLimit,message}; +} +void visit(const ModelSnapshot& model,Meter* meter) { + tick(meter,model.variables.size()); + for(const auto& v:model.variables)tick(meter,v.name.size()); + tick(meter,model.rows.size()); + for(const auto& row:model.rows){tick(meter,row.terms.size());tick(meter,row.name.size());} + tick(meter,model.objective.terms.size()); +} +std::size_t variable(const ModelSnapshot& model,Variable handle) { + if(handle.model_id!=model.model_id || handle.id>=model.variables.size() || + !model.variables[static_cast(handle.id)].active) + throw ModelError("Scenario variable is foreign, missing or deleted"); + return static_cast(handle.id); +} +std::size_t row(const ModelSnapshot& model,Constraint handle) { + if(handle.model_id!=model.model_id || handle.id>=model.rows.size() || + !model.rows[static_cast(handle.id)].active) + throw ModelError("Scenario row is foreign, missing or deleted"); + return static_cast(handle.id); +} +void bounds(double lower,double upper) { + const double inf=std::numeric_limits::infinity(); + if(std::isnan(lower)||std::isnan(upper)||lower==inf||upper==-inf||lower>upper) + throw ModelError("Scenario has invalid or reversed bounds"); +} +void definition(const ModelSnapshot& base,const ScenarioDefinition& def,Meter& meter) { + if(def.objective_offset && !std::isfinite(*def.objective_offset)) + throw ModelError("Scenario objective offset must be finite"); + meter.tick(base.variables.size()); + std::vector seen(base.variables.size(),false); + for(const auto& term:def.objective_coefficients) { + meter.tick();const auto slot=variable(base,term.variable); + if(seen[slot])throw ModelError("Duplicate scenario objective coefficient override"); + seen[slot]=true; + if(!std::isfinite(term.coefficient))throw ModelError("Scenario objective coefficient must be finite"); + } + meter.tick(base.variables.size());std::fill(seen.begin(),seen.end(),false); + for(const auto& change:def.variable_bounds) { + meter.tick();const auto slot=variable(base,change.variable); + if(seen[slot])throw ModelError("Duplicate scenario variable-bound override"); + seen[slot]=true; + if(!change.lower&&!change.upper)throw ModelError("Scenario variable override has no bound side"); + const auto& v=base.variables[slot]; + const double lo=change.lower.value_or(v.lower),hi=change.upper.value_or(v.upper); + bounds(lo,hi); + if(v.type==VariableType::Binary&&(lo<0||hi>1))throw ModelError("Scenario binary bounds must remain in [0,1]"); + } + meter.tick(base.rows.size());seen.assign(base.rows.size(),false); + for(const auto& change:def.row_bounds) { + meter.tick();const auto slot=row(base,change.row); + if(seen[slot])throw ModelError("Duplicate scenario row-side override"); + seen[slot]=true; + if(!change.lower&&!change.upper)throw ModelError("Scenario row override has no bound side"); + const auto& r=base.rows[slot]; + bounds(change.lower.value_or(r.lower),change.upper.value_or(r.upper)); + } +} +ModelSnapshot materialize(const ModelSnapshot& base,const ScenarioDefinition& def, + ModelId owner,Revision revision,Meter* meter) { + visit(base,meter); // precharge the owned source copy + ModelSnapshot out=base;out.model_id=owner;out.revision=revision; + for(auto& v:out.variables){tick(meter);v.variable.model_id=owner;} + for(auto& r:out.rows){tick(meter);r.constraint.model_id=owner; + for(auto& t:r.terms){tick(meter);t.variable.model_id=owner;}} + tick(meter,base.variables.size());std::vector coefficients(base.variables.size(),0); + for(const auto& t:base.objective.terms){tick(meter);coefficients[t.variable.id]=t.coefficient;} + for(const auto& t:def.objective_coefficients){tick(meter);coefficients[t.variable.id]=t.coefficient;} + out.objective.terms.clear(); + for(std::size_t i=0;istatic_cast(exact_limit)) + throw EvidenceError("Exact scenario witness/data is not a supported exact integer"); + return static_cast(value); +} +std::int64_t add(std::int64_t a,std::int64_t b) { + constexpr auto hi=std::numeric_limits::max(),lo=std::numeric_limits::min(); + if((b>0&&a>hi-b)||(b<0&&a::max(),lo=std::numeric_limits::min(); + if((a>0&&((b>0&&a>hi/b)||(b<0&&b0&&a& values,Meter& meter) { + for(const auto& v:model.variables){meter.tick();if(!v.active)continue; + if(v.type!=VariableType::Integer&&v.type!=VariableType::Binary) + throw EvidenceError("Exact scenario result has a nondiscrete original variable"); + const auto x=integer(values[v.variable.id]); + if(xinteger(v.upper))throw EvidenceError("Exact scenario variable violation");} + const auto activity=[&](const std::vector& terms,std::int64_t offset) { + auto sum=offset;for(const auto& t:terms){meter.tick();sum=add(sum,multiply(integer(t.coefficient),integer(values[t.variable.id])));}return sum; + }; + for(const auto& r:model.rows){meter.tick();if(!r.active)continue; + const auto sum=activity(r.terms,0); + if((std::isfinite(r.lower)&&suminteger(r.upper))) + throw EvidenceError("Exact scenario row violation");} + const auto value=activity(model.objective.terms,integer(model.objective.offset)); + if(value < -exact_limit||value>exact_limit)throw EvidenceError("Exact scenario objective is not an exact double integer"); + return static_cast(value); +} +ScenarioCheck check(const ModelSnapshot& model,SolveResult& result,const SolveOptions& options,Meter& meter) { + ScenarioCheck out; + known_status(result.termination); + if(result.model_id!=model.model_id||result.revision!=model.revision) + throw EvidenceError("Scenario result owner or revision differs from the materialized scenario"); + if(result.guarantee!=options.guarantee)throw EvidenceError("Scenario result guarantee differs from request"); + if((result.objective&&!std::isfinite(*result.objective)) || (result.best_bound&&std::isnan(*result.best_bound))) + throw EvidenceError("Scenario result contains malformed objective or bound"); + if(result.native_backend_gap&&(!std::isfinite(*result.native_backend_gap)||*result.native_backend_gap<0)) + throw EvidenceError("Scenario result contains a malformed vendor gap"); + const bool supplied_candidate=!result.values.empty() || result.objective || result.solution_validated; + const bool values_present=supplied_candidate || + (model.variables.empty()&&definitive(result.termination)); + if(values_present || !result.active_variables.empty()) { + if(result.active_variables.size()!=model.variables.size())throw EvidenceError("Scenario active-mask dimension mismatch"); + for(std::size_t i=0;i0); + bounded=bounded&&std::isfinite(lower?v.lower:v.upper); + } + if(bounded)throw EvidenceError("Original variable bounds contradict objective unboundedness"); + } + try{result.update_gaps(model.objective.sense);}catch(const ModelError& e){throw EvidenceError(e.what());} + return out; +} +} + +void ScenarioBatchOptions::validate() const { + solve.validate(); + if(reuse!=ScenarioReuse::Automatic&&reuse!=ScenarioReuse::Cold)throw ModelError("Invalid scenario reuse mode"); +} +ModelId ScenarioBatch::id() const noexcept{return owner_;} +const ModelSnapshot& ScenarioBatch::base() const noexcept{return base_;} +std::size_t ScenarioBatch::size() const noexcept{return definitions_.size();} +ScenarioId ScenarioBatch::scenario(std::size_t index) const { + if(index>=size())throw ModelError("Scenario index is outside its batch");return {owner_,index}; +} +const ScenarioDefinition& ScenarioBatch::definition(ScenarioId id) const { + if(id.batch_id!=owner_||id.index>=size())throw ModelError("Scenario ID is foreign or out of range"); + return definitions_[static_cast(id.index)]; +} +ModelSnapshot ScenarioBatch::materialize(ScenarioId id) const { + return Optimize::materialize(base_,definition(id),owner_,id.index+1,nullptr); +} +Variable ScenarioBatch::map(Variable original) const{return {owner_,variable(base_,original)};} +Constraint ScenarioBatch::map(Constraint original) const{return {owner_,row(base_,original)};} +bool ScenarioBatchResult::all_resolved() const noexcept { + if(!batch||completion!=ScenarioBatchCompletion::Complete||outcomes.size()!=batch->size()||resolved!=outcomes.size())return false; + for(const auto& outcome:outcomes)if(!outcome.result||!definitive(outcome.result->termination))return false; + return true; +} +double ScenarioBatchResult::value(ScenarioId id,Variable original) const { + if(!batch)throw ModelError("Scenario batch was not admitted"); + batch->definition(id); + if(id.index>=outcomes.size())throw ModelError("Scenario outcome is missing"); + const auto& outcome=outcomes[id.index]; + if(!outcome.result||outcome.scenario.batch_id!=id.batch_id||outcome.scenario.index!=id.index|| + outcome.result->model_id!=batch->id()||outcome.result->revision!=id.index+1) + throw ModelError("Scenario result identity is absent or inconsistent"); + return outcome.result->value(batch->map(original)); +} +namespace Detail { +struct ScenarioBatchAccess { + static std::shared_ptr create(const ModelSnapshot& base, + const std::vector& definitions,const ScenarioBatchOptions& options, + Meter& meter,ScenarioBatchResult& output) { + storage(definitions.size(),options.max_scenarios,"Scenario count cap reached"); + if(definitions.size()>=std::numeric_limits::max()) + throw Unsupported("Too many scenario revisions"); + product(definitions.size(),base.variables.size(),options.max_saved_value_slots,"Scenario saved-value slot cap reached"); + if(!base.indicators.empty()||!base.globals.empty()) + throw Unsupported("Scenario batches currently reject all indicator/global metadata, including inactive history"); + visit(base,&meter);validate_structure(base);meter.tick(0); + // Admission is independent of backend availability and batch size. These + // routes cannot meet this guarantee, even if no scenario is requested. + if(options.solve.guarantee==Guarantee::Certified || + (options.solve.guarantee==Guarantee::Exact&&options.solve.backend!=Backend::Native)) + throw Unsupported("Scenario batches require Numerical, or explicit Native with Exact"); + for(const auto& v:base.variables){meter.tick();if(v.active&&v.type!=VariableType::Continuous&&v.type!=VariableType::Integer&&v.type!=VariableType::Binary) + throw Unsupported("Scenario batches currently support Continuous, Integer and Binary variables only");} + if(!options.solve.primal_start.empty())throw Unsupported("Scenario batches do not yet admit common primal starts"); + if(definitions.size()>1&&options.solve.node_limit&&*options.solve.node_limit>0) + throw Unsupported("Multi-scenario positive node limits require consumed-node accounting"); + std::size_t count=0; + for(std::size_t i=0;i(new ScenarioBatch); + Model owner;batch->owner_=owner.id(); + // Public snapshots can contain a caller-selected positive owner number. + // Keep original/private identity distinct even if it predicts our next ID. + if(batch->owner_==base.model_id){Model distinct;batch->owner_=distinct.id();} + batch->base_=base;batch->definitions_=definitions; + for(std::size_t i=0;iowner_,i+1,&meter);(void)tested;} + meter.tick(0); + } + output.offending_scenario.reset();return batch; + } +}; +} +namespace { +template +ScenarioBatchResult run(Snapshot snapshot,ModelId owner,Revision revision, + const std::vector& definitions,const ScenarioBatchOptions& options) { + const auto started=std::chrono::steady_clock::now(); + ScenarioBatchResult out;out.model_id=owner;out.revision=revision; + std::optional budget; + std::optional meter; + std::optional current; + auto fail=[&](Termination reason,const char* message) noexcept { + out.completion=out.batch?ScenarioBatchCompletion::Interrupted:ScenarioBatchCompletion::Rejected; + out.stop_reason=reason; + try {out.message=message;}catch(...){out.message.clear();} + if(current && out.outcomes[*current].result) { + out.outcomes[*current].check.reset(); + try {clear(*out.outcomes[*current].result,reason,message);} + catch(...){out.outcomes[*current].result->message.clear();} + } + }; + try { + options.validate();budget.emplace(options.solve,started);meter.emplace(Meter{*budget,options.max_work,0}); + checkpoint(*budget); + const auto& original=snapshot();checkpoint(*budget); + auto batch=Detail::ScenarioBatchAccess::create(original,definitions,options,*meter,out); + meter->tick(definitions.size());std::vector outcomes(definitions.size()); + for(std::size_t i=0;iscenario(i); + checkpoint(*budget);out.batch=std::move(batch);out.outcomes=std::move(outcomes); + out.completion=ScenarioBatchCompletion::Interrupted; + { + std::optional session; + if(options.reuse==ScenarioReuse::Automatic && options.solve.backend!=Backend::Native && !out.outcomes.empty())session.emplace(); + for(std::size_t i=0;ibase(),out.batch->definition(outcome.scenario),out.batch->id(),i+1,&*meter); + auto adjusted=options.solve;adjusted.time_limit_seconds=budget->remaining_seconds();adjusted.cancellation=budget->cancellation(); + const auto before=session?session->statistics():SessionStatistics{}; + event("before_solve",i);checkpoint(*budget);outcome.state=ScenarioRunState::Attempted;++out.attempted; + outcome.result.emplace();outcome.result->model_id=scenario.model_id; + outcome.result->revision=scenario.revision;outcome.result->guarantee=options.solve.guarantee; +#ifdef GECODE_OPTIMIZE_TEST_SCENARIOS + auto result=Detail::scenario_test_solve(scenario,adjusted,session?&*session:nullptr); +#else + auto result=session?session->solve(scenario,adjusted):solve(scenario,adjusted); +#endif + const auto after=session?session->statistics():SessionStatistics{}; + outcome.reuse_delta=difference(after,before);out.reuse_statistics=after; + event("after_solve",i);checkpoint(*budget); + auto checked=check(scenario,result,options.solve,*meter); + event("after_check",i);checkpoint(*budget); + // All allocating checked fields are staged before publication. + outcome.result=std::move(result);outcome.check=std::move(checked); + } + event("after_cleanup",i);checkpoint(*budget); + outcome.elapsed_seconds=std::chrono::duration(std::chrono::steady_clock::now()-stage_started).count(); + if(!definitive(outcome.result->termination)) { + out.stop_reason=outcome.result->termination;out.message="Scenario did not reach a definitive outcome"; + current.reset();break; + } + ++out.resolved;current.reset();out.offending_scenario.reset(); + } + } + event("batch_cleanup",out.outcomes.size());checkpoint(*budget); + if(out.resolved==out.outcomes.size()) {out.completion=ScenarioBatchCompletion::Complete;out.stop_reason.reset();} + } catch(const Stopped& e) {fail(e.reason,e.message);} + catch(const Unsupported& e){fail(Termination::Unsupported,e.what());} + catch(const ModelError& e){fail(Termination::InvalidModel,e.what());} + catch(const EvidenceError& e){fail(Termination::NumericalFailure,e.what());} + catch(const std::bad_alloc&){fail(Termination::MemoryLimit,"Scenario allocation failed");} + catch(const std::exception& e){fail(Termination::BackendError,e.what());} + // The Model overload's temporary snapshot has now been released as well. + // Earlier stage witnesses remain timely historical results if cleanup used + // the remaining allowance; only batch completion is downgraded here. + if(budget&&out.completion==ScenarioBatchCompletion::Complete) + if(const auto reason=budget->stop_reason())fail(*reason,"Whole batch budget stopped during final source cleanup"); + if(meter)out.work=meter->used; + out.elapsed_seconds=std::chrono::duration(std::chrono::steady_clock::now()-started).count(); + return out; +} +} +ScenarioBatchResult solve_scenarios(const ModelSnapshot& model,const std::vector& definitions,const ScenarioBatchOptions& options) { + return run([&]() -> const ModelSnapshot& {return model;},model.model_id,model.revision,definitions,options); +} +ScenarioBatchResult solve_scenarios(const Model& model,const std::vector& definitions,const ScenarioBatchOptions& options) { + return run([&]{return model.snapshot();},model.id(),model.revision(),definitions,options); +} +}} diff --git a/gecode/optimize/scenarios.hpp b/gecode/optimize/scenarios.hpp new file mode 100644 index 0000000000..06aac4055a --- /dev/null +++ b/gecode/optimize/scenarios.hpp @@ -0,0 +1,108 @@ +/* Owning serial scenario batches; no shared search-tree semantics. */ +#ifndef GECODE_OPTIMIZE_SCENARIOS_HPP +#define GECODE_OPTIMIZE_SCENARIOS_HPP +#include +#include + +namespace Gecode { namespace Optimize { +struct ScenarioVariableBounds { + Variable variable; + std::optional lower, upper; +}; +struct ScenarioRowBounds { + Constraint row; + std::optional lower, upper; +}; +struct ScenarioDefinition { + std::string name; + /** Absolute coefficients; zero removes a term. Missing entries inherit base. */ + std::vector objective_coefficients; + std::optional objective_offset; + std::vector variable_bounds; + std::vector row_bounds; +}; +struct ScenarioId { + ModelId batch_id = 0; + std::uint64_t index = 0; +}; +enum class ScenarioReuse { Automatic, Cold }; +struct ScenarioBatchOptions { + SolveOptions solve; + ScenarioReuse reuse = ScenarioReuse::Automatic; + std::size_t max_scenarios = 1000; + std::size_t max_patch_entries = 1000000; + std::size_t max_saved_value_slots = 10000000; + /** Metered coordinator element visits, excluding backend internals. */ + std::size_t max_work = 100000000; + void validate() const; +}; +enum class ScenarioRunState { NotStarted, Attempted }; +enum class ScenarioBatchCompletion { Rejected, Interrupted, Complete }; +namespace Detail { struct ScenarioBatchAccess; } +class ScenarioBatch { +public: + ScenarioBatch(const ScenarioBatch&) = delete; + ScenarioBatch& operator=(const ScenarioBatch&) = delete; + ModelId id() const noexcept; + const ModelSnapshot& base() const noexcept; + std::size_t size() const noexcept; + ScenarioId scenario(std::size_t index) const; + const ScenarioDefinition& definition(ScenarioId) const; + /** Owning private snapshot. Same private owner, distinct scenario revision. */ + ModelSnapshot materialize(ScenarioId) const; + Variable map(Variable original) const; + Constraint map(Constraint original) const; +private: + friend struct Detail::ScenarioBatchAccess; + ScenarioBatch() = default; + ModelId owner_ = 0; + ModelSnapshot base_; + std::vector definitions_; +}; +struct ScenarioCheck { + bool identity_valid = false; + /** validation/objective/exact flags are meaningful only when true. */ + bool candidate_examined = false; + bool objective_matches = false; + bool exact_witness_validated = false; + ValidationReport validation; +}; +struct ScenarioOutcome { + ScenarioId scenario; + ScenarioRunState state = ScenarioRunState::NotStarted; + std::optional result; + std::optional check; + SessionStatistics reuse_delta; + double elapsed_seconds = 0; +}; +struct ScenarioBatchResult { + ModelId model_id = 0; + Revision revision = 0; + std::shared_ptr batch; + ScenarioBatchCompletion completion = ScenarioBatchCompletion::Rejected; + std::optional stop_reason; + std::optional offending_scenario; + std::string message; + std::vector outcomes; + SessionStatistics reuse_statistics; + std::size_t attempted = 0, resolved = 0, work = 0; + double elapsed_seconds = 0; + bool all_resolved() const noexcept; + double value(ScenarioId, Variable original) const; +}; +/** + * Ordinary Continuous/Integer/Binary linear models only. All patches are + * admitted before any solve; nonempty indicator/global metadata and primal + * starts are Unsupported. Auto/HiGHS use private session reuse or Cold solves; + * explicit Native uses its ordinary one-shot route and exact admission. + * One whole-batch time/cancel allowance. Positive node limits with >1 scenario + * are Unsupported until consumed-node accounting exists. Zero stops before any + * solve; one scenario preserves the ordinary backend node-limit contract. + * No caller model/session is changed. Materializations and results own history. + */ +ScenarioBatchResult solve_scenarios(const ModelSnapshot&, + const std::vector&, const ScenarioBatchOptions& = {}); +ScenarioBatchResult solve_scenarios(const Model&, + const std::vector&, const ScenarioBatchOptions& = {}); +}} +#endif diff --git a/gecode/optimize/session.hpp b/gecode/optimize/session.hpp new file mode 100644 index 0000000000..f506bef992 --- /dev/null +++ b/gecode/optimize/session.hpp @@ -0,0 +1,66 @@ +/* Persistent numerical reoptimization with explicit state reuse. */ +#ifndef GECODE_OPTIMIZE_SESSION_HPP +#define GECODE_OPTIMIZE_SESSION_HPP + +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { + +struct SessionStatistics { + std::uint64_t solve_calls = 0; + std::uint64_t model_loads = 0; + std::uint64_t incremental_updates = 0; + std::uint64_t unchanged_models = 0; + /** Runs entered with a retained valid LP basis; no speedup is implied. */ + std::uint64_t basis_warm_starts = 0; + /** Previously validated MIP witnesses, rechecked and submitted as hints. */ + std::uint64_t incumbent_starts = 0; +}; + +/** + * Owns one HiGHS instance. Bounds, costs, row sides, sense and offset edits + * reuse its model; matrix/type/slot/owner changes reload it. Every snapshot is + * validated and compared by content, including when its revision is unchanged. + * LP bases can survive compatible edits. MIP trees/cuts are not retained; + * previous MIP solutions are reused only after original-model revalidation. + * Explicit primal starts take precedence. Each call has a fresh solve budget. + * + * Auto selects HiGHS; Native sessions are currently Unsupported. Separate + * sessions may be used independently. Do not call one session concurrently. + * Moving transfers all state; solving a moved-from session returns InvalidModel. + * reset() discards model, basis, witnesses and counters (and revives a moved + * session). Results remain owning historical snapshots after edits or reset. + */ +class SolveSession { +public: + SolveSession(); + ~SolveSession(); + SolveSession(SolveSession&&) noexcept; + SolveSession& operator=(SolveSession&&) noexcept; + SolveSession(const SolveSession&) = delete; + SolveSession& operator=(const SolveSession&) = delete; + + SolveResult solve(const ModelSnapshot& model, const SolveOptions& options = {}); + SolveResult solve(const Model& model, const SolveOptions& options = {}); + /** Owning continuous-LP observations; same session reuse/lifetime contract. */ + LpObservedResult solve_lp_observed(const ModelSnapshot& model, + const LpObservationOptions& options = {}); + LpObservedResult solve_lp_observed(const Model& model, + const LpObservationOptions& options = {}); + LpBasisSolveResult solve_lp_with_basis(const ModelSnapshot& model, + const LpBasisSolveOptions& options); + LpBasisSolveResult solve_lp_with_basis(const Model& model, + const LpBasisSolveOptions& options); + SessionStatistics statistics() const noexcept; + void reset(); + +private: + struct Impl; + std::unique_ptr impl_; +}; + +}} +#endif diff --git a/gecode/optimize/solve.cpp b/gecode/optimize/solve.cpp new file mode 100644 index 0000000000..10fd39cde7 --- /dev/null +++ b/gecode/optimize/solve.cpp @@ -0,0 +1,969 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifdef GECODE_OPTIMIZE_WITH_HIGHS +#include +#include +#include +#endif + +namespace Gecode { namespace Optimize { +#ifdef GECODE_OPTIMIZE_TEST_LP_BASIS_FAILURE +namespace Detail { bool lp_basis_test_cancel() noexcept; } +#endif +namespace { + +class Unsupported : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; +class BasisStopped : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +#ifdef GECODE_OPTIMIZE_WITH_HIGHS + +void require_ok(HighsStatus status, const char* operation) { + if (status != HighsStatus::kOk) + throw ModelError(std::string("HiGHS rejected or modified input during ") + operation); +} + +void numerical_range(double value, double maximum, const char* kind) { + if (std::isfinite(value) && std::abs(value) >= maximum) + throw Unsupported(std::string(kind) + " exceeds supported HiGHS numerical range"); +} + +HighsVarType variable_type(VariableType type) { + switch (type) { + case VariableType::Continuous: return HighsVarType::kContinuous; + case VariableType::Integer: + case VariableType::Binary: return HighsVarType::kInteger; + case VariableType::SemiContinuous: return HighsVarType::kSemiContinuous; + case VariableType::SemiInteger: return HighsVarType::kSemiInteger; + } + throw ModelError("Invalid variable type"); +} + +HighsBasisStatus basis_status(LpBasisStatus value) { + switch(value) { + case LpBasisStatus::Lower: return HighsBasisStatus::kLower; + case LpBasisStatus::Basic: return HighsBasisStatus::kBasic; + case LpBasisStatus::Upper: return HighsBasisStatus::kUpper; + case LpBasisStatus::Zero: return HighsBasisStatus::kZero; + case LpBasisStatus::NonbasicUnspecified: return HighsBasisStatus::kNonbasic; + } + throw ModelError("Unknown original basis status"); +} +LpBasisStatus basis_status(HighsBasisStatus value) { + switch(value) { + case HighsBasisStatus::kLower: return LpBasisStatus::Lower; + case HighsBasisStatus::kBasic: return LpBasisStatus::Basic; + case HighsBasisStatus::kUpper: return LpBasisStatus::Upper; + case HighsBasisStatus::kZero: return LpBasisStatus::Zero; + case HighsBasisStatus::kNonbasic: return LpBasisStatus::NonbasicUnspecified; + } + throw std::runtime_error("Unknown returned backend basis status"); +} + +struct Compiled { + HighsLp lp; + std::vector slots; + std::vector row_slots; + // Unlike row_slots (all active-row topology), this maps emitted backend rows. + std::vector backend_row_slots; + std::vector original_types; + bool discrete = false; + bool contradictory_constant = false; +}; + +Compiled compile(const ModelSnapshot& model, double feasibility_tolerance) { + validate_structure(model); + Compiled compiled; + auto& lp = compiled.lp; + std::vector columns(model.variables.size(), -1); + const auto limit = static_cast(std::numeric_limits::max()); + for (const auto& v : model.variables) { + if (!v.active) continue; + if (compiled.slots.size() >= limit) throw Unsupported("Too many columns for HiGHS"); + numerical_range(v.lower, 1e20, "Variable bound"); + numerical_range(v.upper, 1e20, "Variable bound"); + if ((v.type == VariableType::SemiContinuous || v.type == VariableType::SemiInteger) + && (!std::isfinite(v.upper) || v.upper > 1e5)) + throw Unsupported("Semi variables require a finite upper bound <= 100000 in this adapter"); + columns[v.variable.id] = static_cast(compiled.slots.size()); + compiled.slots.push_back(static_cast(v.variable.id)); + compiled.original_types.push_back(v.type); + lp.col_lower_.push_back(v.lower); + lp.col_upper_.push_back(v.upper); + lp.col_cost_.push_back(0.0); + lp.col_names_.push_back(v.name.empty() ? "x" + std::to_string(v.variable.id) : v.name); + lp.integrality_.push_back(variable_type(v.type)); + compiled.discrete |= v.type != VariableType::Continuous; + } + lp.num_col_ = static_cast(compiled.slots.size()); + lp.sense_ = model.objective.sense == ObjectiveSense::Minimize + ? ObjSense::kMinimize : ObjSense::kMaximize; + numerical_range(model.objective.offset, 1e20, "Objective offset"); + lp.offset_ = model.objective.offset; + for (const auto& term : model.objective.terms) { + numerical_range(term.coefficient, 1e20, "Objective coefficient"); + lp.col_cost_[columns[term.variable.id]] = term.coefficient; + } + auto& matrix = lp.a_matrix_; + matrix.format_ = MatrixFormat::kRowwise; + matrix.start_.assign(1, 0); + for (const auto& row : model.rows) { + if (!row.active) continue; + compiled.row_slots.push_back(static_cast(row.constraint.id)); + if (lp.row_lower_.size() >= limit) throw Unsupported("Too many rows for HiGHS"); + numerical_range(row.lower, 1e20, "Row bound"); + numerical_range(row.upper, 1e20, "Row bound"); + if (row.terms.empty()) { + // HiGHS MIP presolve requires a nonempty matrix when rows exist. Constant + // rows need no backend column: decide them using the original checker's + // absolute feasibility convention and retain them in the original model. + compiled.contradictory_constant |= row.lower > feasibility_tolerance || + row.upper < -feasibility_tolerance; + continue; + } + compiled.backend_row_slots.push_back(static_cast(row.constraint.id)); + lp.row_lower_.push_back(row.lower); + lp.row_upper_.push_back(row.upper); + lp.row_names_.push_back(row.name.empty() ? "r" + std::to_string(row.constraint.id) : row.name); + for (const auto& term : row.terms) { + if (matrix.value_.size() >= limit) throw Unsupported("Too many nonzeros for HiGHS"); + // HiGHS discards coefficients at/below this minimum threshold. Refuse + // such input rather than silently changing the original problem. + if (std::abs(term.coefficient) <= 1e-12) + throw Unsupported("Nonzero matrix coefficients <= 1e-12 require rescaling"); + numerical_range(term.coefficient, 1e15, "Matrix coefficient"); + matrix.index_.push_back(columns[term.variable.id]); + matrix.value_.push_back(term.coefficient); + } + matrix.start_.push_back(static_cast(matrix.value_.size())); + } + lp.num_row_ = static_cast(lp.row_lower_.size()); + matrix.num_col_ = lp.num_col_; + matrix.num_row_ = lp.num_row_; + if (!compiled.discrete) lp.integrality_.clear(); + return compiled; +} + +HighsBasis prepare_basis(const LpBasis& source,const Compiled& model) { + HighsBasis out; + // The pinned alien zero-row path indexes internal status vectors before its + // general size checks. A fully checked basis with zero basic entities and + // zero rows has no singularity issue and uses the non-alien consistency path. + out.alien=!model.backend_row_slots.empty();out.valid=false;out.useful=true; + out.col_status.reserve(model.slots.size());out.row_status.reserve(model.backend_row_slots.size()); + for(auto slot:model.slots)out.col_status.push_back(basis_status(*source.columns()[slot])); + for(auto slot:model.backend_row_slots)out.row_status.push_back(basis_status(*source.rows()[slot])); + return out; +} + +void submit_basis(Highs& highs,const HighsBasis& requested,const Compiled& compiled, + const ModelSnapshot& model,const SolveBudget& budget,LpBasisSubmission& report) { + report.backend_attempted=true; + try { + auto status=highs.setBasis(requested,"Gecode original LP basis start"); +#ifdef GECODE_OPTIMIZE_TEST_LP_BASIS_FAILURE + // Separately compiled fault panel only: dirty the REAL backend first, then + // exercise rejection or cancellation cleanup. Absent in production builds. + if(status==HighsStatus::kOk) { + if(Detail::lp_basis_test_cancel())budget.cancellation()->cancel(); + else status=HighsStatus::kError; + } +#endif + if(budget.expired())throw BasisStopped("Budget expired during LP basis factorization/repair"); + if(status!=HighsStatus::kOk)throw std::runtime_error("HiGHS rejected the submitted LP basis"); + const auto& actual=highs.getBasis(); + if(!actual.valid||actual.alien||actual.col_status.size()!=compiled.slots.size()|| + actual.row_status.size()!=compiled.backend_row_slots.size()) + throw std::runtime_error("HiGHS returned an invalid or incomplete submitted basis"); + { + std::vector> rows(model.rows.size()),cols(model.variables.size()); + for(std::size_t i=0;i static_cast(std::numeric_limits::max())) + throw Unsupported("Node limit exceeds HiGHS integer range"); + require_ok(highs.setOptionValue("mip_max_nodes", static_cast(*options.node_limit)), "node limit setup"); + } else { + require_ok(highs.setOptionValue("mip_max_nodes", kHighsIInf), "node limit reset"); + } +} + +HighsSolution prepare_start(const ModelSnapshot& model, + const Compiled& compiled, const SolveOptions& options) { + std::vector column(model.variables.size(), -1); + for (std::size_t i=0; i(i); + std::vector values(model.variables.size(), std::numeric_limits::quiet_NaN()); + std::vector indices; + std::vector entries; + for (const auto& entry : options.primal_start) { + const auto handle = entry.variable; + if (handle.model_id != model.model_id || handle.id >= model.variables.size() + || !model.variables[handle.id].active) + throw ModelError("Primal start contains a foreign or deleted variable"); + if (!std::isnan(values[handle.id])) + throw ModelError("Primal start contains a duplicate variable"); + const auto& v = model.variables[handle.id]; + const long double value = entry.value; + const bool semi = v.type == VariableType::SemiContinuous || v.type == VariableType::SemiInteger; + const bool zero = semi && std::abs(value) <= options.feasibility_tolerance; + if (!zero && (value < static_cast(v.lower)-options.feasibility_tolerance + || value > static_cast(v.upper)+options.feasibility_tolerance)) + throw ModelError("Primal start violates a variable bound"); + if (v.type != VariableType::Continuous && v.type != VariableType::SemiContinuous + && std::abs(value-std::round(value)) > options.integrality_tolerance) + throw ModelError("Primal start violates integrality"); + values[handle.id] = entry.value; + indices.push_back(column[handle.id]); + entries.push_back(entry.value); + } + if (entries.size() == compiled.slots.size()) { + const auto checked = validate(model, values, options.feasibility_tolerance, + options.integrality_tolerance); + if (!checked.valid) throw ModelError("Complete primal start is infeasible: " + checked.message); + } + // HiGHS's sparse overload checks only the nonzero interval of semi domains, + // incorrectly rejecting their valid zero alternative. Its dense overload + // uses kHighsUndefined for unprovided values and handles the MIP hint normally. + HighsSolution start; + start.col_value.assign(compiled.slots.size(), kHighsUndefined); + for (std::size_t i=0; i original_values(const ModelSnapshot& model, + const std::vector& slots, + const std::vector& columns) { + if (columns.size() != slots.size()) throw ModelError("HiGHS returned wrong solution dimension"); + std::vector values(model.variables.size(), std::numeric_limits::quiet_NaN()); + for (std::size_t i = 0; i < slots.size(); ++i) values[slots[i]] = columns[i]; + return values; +} + +#endif + +struct SessionState { + SessionStatistics statistics; +#ifdef GECODE_OPTIMIZE_WITH_HIGHS + std::unique_ptr highs; + std::optional compiled; + ModelId model_id = 0; + std::optional previous; + void invalidate() noexcept { + previous.reset(); + compiled.reset(); + highs.reset(); + model_id = 0; + } +#endif +}; + +#ifdef GECODE_OPTIMIZE_WITH_HIGHS +bool compatible(const Compiled& a, const Compiled& b) { + return a.slots == b.slots && a.row_slots == b.row_slots && + a.backend_row_slots == b.backend_row_slots && + a.original_types == b.original_types && + a.lp.integrality_ == b.lp.integrality_ && + a.lp.a_matrix_.start_ == b.lp.a_matrix_.start_ && + a.lp.a_matrix_.index_ == b.lp.a_matrix_.index_ && + a.lp.a_matrix_.value_ == b.lp.a_matrix_.value_; +} + +bool prepare(SessionState& state, Compiled&& compiled, ModelId owner) { + auto& highs = *state.highs; + const bool reuse = state.compiled && state.model_id == owner && + compatible(*state.compiled, compiled); + if (!reuse) { + require_ok(highs.passModel(compiled.lp), "model load"); + state.previous.reset(); + ++state.statistics.model_loads; + } else { + const auto& old = state.compiled->lp; + const auto& next = compiled.lp; + bool changed = false; + if (old.col_cost_ != next.col_cost_) { + require_ok(highs.changeColsCost(0, next.num_col_-1, next.col_cost_.data()), "cost update"); + changed = true; + } + if (old.col_lower_ != next.col_lower_ || old.col_upper_ != next.col_upper_) { + require_ok(highs.changeColsBounds(0, next.num_col_-1, next.col_lower_.data(), + next.col_upper_.data()), "column bound update"); + changed = true; + } + if (old.row_lower_ != next.row_lower_ || old.row_upper_ != next.row_upper_) { + require_ok(highs.changeRowsBounds(0, next.num_row_-1, next.row_lower_.data(), + next.row_upper_.data()), "row bound update"); + changed = true; + } + if (old.sense_ != next.sense_) { + require_ok(highs.changeObjectiveSense(next.sense_), "sense update"); + changed = true; + } + if (old.offset_ != next.offset_) { + require_ok(highs.changeObjectiveOffset(next.offset_), "offset update"); + changed = true; + } + if (changed) ++state.statistics.incremental_updates; + else ++state.statistics.unchanged_models; + } + state.compiled = std::move(compiled); + state.model_id = owner; + return reuse; +} + +struct CallbackScope { + SessionState& state; + ~CallbackScope() noexcept { + // A persistent backend must never retain references to a completed call's + // budget or local vectors, including exception and early-return paths. + try { state.highs->setCallback(HighsCallbackFunctionType{}, nullptr); } + catch (...) { state.invalidate(); } + } +}; +#endif +} // namespace + +#ifdef GECODE_OPTIMIZE_WITH_HIGHS +Detail::LpSensitivityCompiled Detail::compile_lp_sensitivity( + const ModelSnapshot& model,double primal_tolerance) { + try { + auto compiled=compile(model,primal_tolerance); + if(compiled.row_slots.size()!=compiled.backend_row_slots.size()) + throw LpSensitivityBackendError(LpSensitivityReason::NoBasis, + "LP sensitivity cannot map elided constant rows"); + return {std::move(compiled.lp),std::move(compiled.slots), + std::move(compiled.backend_row_slots)}; + } catch(const Unsupported& error) { + throw LpSensitivityBackendError(LpSensitivityReason::Unsupported,error.what()); + } +} +#endif + +struct SolveSession::Impl : SessionState {}; + +BackendCapabilities capabilities(Backend backend) { + if (backend == Backend::Native) return native_capabilities(); + BackendCapabilities c; + if (backend != Backend::Auto && backend != Backend::Highs) { + c.limitations.push_back("Unknown backend"); + return c; + } + c.name = "HiGHS"; +#ifdef GECODE_OPTIMIZE_WITH_HIGHS + c.available = c.linear_programming = c.mixed_integer_linear = true; + c.version = Highs().version(); +#else + c.limitations.push_back("Built without HiGHS"); +#endif + c.limitations.push_back("Numerical linear LP/MILP only; no exact/certified or nonlinear solve"); + c.limitations.push_back("No action callbacks; sessions retain LP bases and revalidated MIP hints only"); + c.limitations.push_back("One HiGHS worker per solve in this initial adapter"); + c.limitations.push_back("Finite bounds/costs below 1e20; nonzero matrix magnitudes in (1e-12,1e15)"); + c.limitations.push_back("Time/cancellation is cooperative and may overrun during backend calls"); + return c; +} + +static SolveResult solve_highs(const ModelSnapshot& model, const SolveOptions& options, + SessionState* persistent = nullptr, + const SolveBudget* shared_budget = nullptr, + Detail::LpBackendObservations* observations = nullptr, + const LpBasis* basis_start = nullptr, + LpBasisSubmission* basis_report = nullptr) { +#ifndef GECODE_OPTIMIZE_WITH_HIGHS + (void)observations; + (void)basis_start;(void)basis_report; +#endif + const auto started = std::chrono::steady_clock::now(); + SolveResult result; + result.model_id = model.model_id; + result.revision = model.revision; + result.backend = "HiGHS"; + // Start the budget before structural validation and conversion. + std::optional budget_storage; + try { + if (shared_budget) budget_storage.emplace(*shared_budget); + else budget_storage.emplace(options); + } + catch (const ModelError& e) { + result.termination = Termination::InvalidModel; + result.message = e.what(); + result.elapsed_seconds = std::chrono::duration(std::chrono::steady_clock::now()-started).count(); + return result; + } + catch (const std::bad_alloc&) { + result.termination = Termination::MemoryLimit; + result.message = "Budget allocation failed"; + result.elapsed_seconds = std::chrono::duration(std::chrono::steady_clock::now()-started).count(); + return result; + } + auto& budget = *budget_storage; + SessionState local; + auto& state = persistent ? *persistent : local; +#ifndef GECODE_OPTIMIZE_WITH_HIGHS + (void)state; +#endif + try { + validate_structure(model); + if (std::any_of(model.globals.begin(),model.globals.end(),[](const auto& record){return record.active;})) + throw Unsupported("HiGHS linear adapter cannot preserve native global constraints; choose Native"); + if (options.guarantee != Guarantee::Numerical) + throw Unsupported("HiGHS adapter cannot satisfy an Exact or Certified guarantee"); + result.active_variables.reserve(model.variables.size()); + for (const auto& variable : model.variables) result.active_variables.push_back(variable.active); + if (budget.expired()) { + result.termination = budget.stop_reason().value_or(Termination::Unknown); + result.elapsed_seconds = budget.elapsed_seconds(); + return result; + } +#ifdef GECODE_OPTIMIZE_WITH_HIGHS + // Complete source admission before mutating a retained backend instance. + auto conversion = compile(model, options.feasibility_tolerance); + std::optional explicit_start; + if (!options.primal_start.empty()) explicit_start = prepare_start(model, conversion, options); + std::optional external_basis; + if (basis_start) external_basis=prepare_basis(*basis_start,conversion); + if (budget.expired()) { + result.termination = budget.stop_reason().value_or(Termination::Unknown); + result.elapsed_seconds = budget.elapsed_seconds(); + return result; + } + if (!state.highs) state.highs = std::make_unique(); + auto& highs = *state.highs; + result.backend_version = highs.version(); + configure(highs, options); + if (budget.expired()) { + result.termination = budget.stop_reason().value_or(Termination::Unknown); + result.elapsed_seconds = budget.elapsed_seconds(); + return result; + } + if (conversion.contradictory_constant) { + state.invalidate(); + result.termination = Termination::Infeasible; + result.message = "An original constant row violates the feasibility tolerance"; + result.elapsed_seconds = budget.elapsed_seconds(); + return result; + } + if (conversion.slots.empty()) { + if (!options.primal_start.empty()) + throw ModelError("A model without active variables cannot accept a primal start"); + // HiGHS's empty-model status does not check constant rows for us. + auto values = original_values(model, conversion.slots, {}); + auto checked = validate(model, values, options.feasibility_tolerance, options.integrality_tolerance); + state.invalidate(); + result.termination = checked.valid ? Termination::Optimal : Termination::Infeasible; + if (budget.expired()) { + result.termination = budget.stop_reason().value_or(Termination::Unknown); + result.elapsed_seconds = budget.elapsed_seconds(); + return result; + } + if (checked.valid) { + result.values = std::move(values); + result.objective = checked.objective; + result.best_bound = checked.objective; + result.solution_validated = true; + result.update_gaps(model.objective.sense); + } + result.elapsed_seconds = budget.elapsed_seconds(); + return result; + } + const bool reused = prepare(state, std::move(conversion), model.model_id); + const auto& compiled = *state.compiled; + if (compiled.discrete) + require_ok(highs.clearSolver(), "MIP search state reset"); + if (explicit_start) { + require_ok(highs.setSolution(*explicit_start), "primal start submission"); + result.start_submitted = true; + } else if (compiled.discrete && state.previous && state.previous->has_solution()) { + const auto checked = validate(model, state.previous->values, + options.feasibility_tolerance, options.integrality_tolerance); + if (checked.valid) { + HighsSolution hint; + for (const auto slot : compiled.slots) hint.col_value.push_back(state.previous->values[slot]); + require_ok(highs.setSolution(hint), "session incumbent submission"); + result.start_submitted = true; + ++state.statistics.incumbent_starts; + } + } + if (budget.expired()) { + result.termination = budget.stop_reason().value_or(Termination::Unknown); + result.elapsed_seconds = budget.elapsed_seconds(); + return result; + } + std::vector timely_columns; + std::optional timely_bound; + bool callback_failed = false; + auto callback = [&](int type, const std::string&, const HighsCallbackOutput* out, + HighsCallbackInput* in, void*) { + const bool can_interrupt = type == kCallbackSimplexInterrupt || + type == kCallbackIpmInterrupt || type == kCallbackMipInterrupt; + try { + // Improving-solution callbacks are observations only. HiGHS asserts + // that they never request actions, including user_interrupt. + if (budget.expired() || callback_failed) { + if (in && can_interrupt) in->user_interrupt = true; + return; + } + if (!out) return; + if (type == kCallbackMipImprovingSolution && out->mip_solution.size() == compiled.slots.size()) + timely_columns = out->mip_solution; + if (compiled.discrete && (type == kCallbackMipInterrupt || type == kCallbackMipImprovingSolution) + && std::isfinite(out->mip_dual_bound)) timely_bound = out->mip_dual_bound; + } catch (...) { + callback_failed = true; + if (in && can_interrupt) in->user_interrupt = true; + } + }; + CallbackScope callback_scope{state}; + require_ok(highs.setCallback(callback, nullptr), "callback setup"); + for (auto type : {kCallbackSimplexInterrupt, kCallbackIpmInterrupt, + kCallbackMipInterrupt, kCallbackMipImprovingSolution}) + require_ok(highs.startCallback(type), "callback activation"); + highs.zeroAllClocks(); + require_ok(highs.setOptionValue("time_limit", budget.remaining_seconds()), "time limit setup"); + if (external_basis) { + if (budget.expired()) throw BasisStopped("Budget expired before LP basis submission"); + submit_basis(highs,*external_basis,compiled,model,budget,*basis_report); + if (budget.expired()) { + basis_report->state=LpBasisSubmissionState::Interrupted; + basis_report->statuses_changed.reset(); + throw BasisStopped("Budget expired before LP basis submission returned"); + } + highs.zeroAllClocks(); + require_ok(highs.setOptionValue("time_limit",budget.remaining_seconds()),"post-basis time limit setup"); + } + if (!external_basis && reused && !compiled.discrete && highs.getBasis().valid) + ++state.statistics.basis_warm_starts; + const auto run_status = highs.run(); + const bool returned_in_budget = !budget.expired(); + result.termination = status(highs.getModelStatus()); + result.message = highs.modelStatusToString(highs.getModelStatus()); + if (!returned_in_budget) result.termination = budget.stop_reason().value_or(Termination::Unknown); + const auto& info = highs.getInfo(); + if (result.termination == Termination::SolutionLimit && options.node_limit && info.valid + && info.mip_node_count >= 0 && static_cast(info.mip_node_count) >= *options.node_limit) + result.termination = Termination::NodeLimit; + if (run_status == HighsStatus::kError || callback_failed) { + result.termination = Termination::BackendError; + result.message = callback_failed ? "Callback storage failed" : "HiGHS solve failed"; + } + const auto& candidate = highs.getSolution(); + if (observations) { + // Passive copies only. In particular, do not call ray/ranging/tableau + // methods here: those can solve or mutate the backend. + auto& raw=*observations; + raw.attempted=true;raw.timely=returned_in_budget; + raw.model_id=model.model_id;raw.revision=model.revision; + raw.info_valid=info.valid;raw.value_valid=candidate.value_valid; + raw.primal_feasible=info.valid && info.primal_solution_status == kSolutionStatusFeasible; + raw.dual_valid=candidate.dual_valid; + raw.dual_feasible=info.valid && info.dual_solution_status == kSolutionStatusFeasible; + try { + if (returned_in_budget) { + raw.column_slots=compiled.slots;raw.row_slots=compiled.backend_row_slots; + double tolerance=0; + if (highs.getOptionValue("primal_feasibility_tolerance",tolerance) == HighsStatus::kOk) + raw.primal_tolerance=tolerance; + if (highs.getOptionValue("dual_feasibility_tolerance",tolerance) == HighsStatus::kOk) + raw.dual_tolerance=tolerance; + if (raw.requested_duals && raw.dual_valid) { + raw.column_duals=candidate.col_dual;raw.row_duals=candidate.row_dual; + } + if (raw.requested_basis) { + const auto& basis=highs.getBasis(); + raw.basis_valid=basis.valid; + raw.info_basis_valid=info.valid && info.basis_validity == kBasisValidityValid; + auto convert_basis=[](HighsBasisStatus value) { + switch (value) { + case HighsBasisStatus::kLower: return LpBasisStatus::Lower; + case HighsBasisStatus::kBasic: return LpBasisStatus::Basic; + case HighsBasisStatus::kUpper: return LpBasisStatus::Upper; + case HighsBasisStatus::kZero: return LpBasisStatus::Zero; + case HighsBasisStatus::kNonbasic: return LpBasisStatus::NonbasicUnspecified; + } + throw ModelError("Unknown HiGHS basis status"); + }; + if (basis.valid) { + raw.column_basis.reserve(basis.col_status.size()); + raw.row_basis.reserve(basis.row_status.size()); + for (auto value : basis.col_status) raw.column_basis.push_back(convert_basis(value)); + for (auto value : basis.row_status) raw.row_basis.push_back(convert_basis(value)); + } + } + raw.complete=true; + } + } catch (const std::bad_alloc&) { raw.failure=Detail::LpCaptureFailure::Allocation; } + catch (...) { raw.failure=Detail::LpCaptureFailure::InvalidData; } + // An observation copy cannot turn a late primal into a timely incumbent. + if (budget.expired()) raw.timely=false; + } + std::vector columns; + if (returned_in_budget && candidate.value_valid && info.valid + && info.primal_solution_status == kSolutionStatusFeasible) + columns = candidate.col_value; + else if (!timely_columns.empty()) columns = std::move(timely_columns); + if (!columns.empty()) { + auto values = original_values(model, compiled.slots, columns); + const auto checked = validate(model, values, options.feasibility_tolerance, options.integrality_tolerance); + if (checked.valid) { + result.values = std::move(values); + result.objective = checked.objective; + result.solution_validated = true; + } else { + result.termination = Termination::NumericalFailure; + result.message = "Original-model validation rejected HiGHS candidate: " + checked.message; + } + } + if (returned_in_budget && info.valid && compiled.discrete) { + if (std::isfinite(info.mip_dual_bound)) result.best_bound = info.mip_dual_bound; + if (std::isfinite(info.mip_gap)) result.native_backend_gap = info.mip_gap; + } else if (returned_in_budget && result.termination == Termination::Optimal && info.valid + && std::isfinite(info.objective_function_value)) { + result.best_bound = info.objective_function_value; + } else if (timely_bound) result.best_bound = timely_bound; + if (result.termination == Termination::Optimal && !result.has_solution()) { + result.termination = Termination::NumericalFailure; + result.message = "HiGHS reported optimal without a validated original-model solution"; + } + try { result.update_gaps(model.objective.sense); } + catch (const ModelError&) { + // Preserve the observed numerical discrepancy, never manufacture zero. + result.best_bound.reset(); + result.absolute_gap.reset(); + result.relative_gap.reset(); + result.message += "; bound inconsistent with independently recomputed objective"; + } + state.previous.reset(); + if (persistent && compiled.discrete && result.has_solution()) { + // A cache is optional: allocation pressure must not erase a valid result. + try { state.previous.emplace(result); } + catch (const std::bad_alloc&) { state.previous.reset(); } + } +#else + throw Unsupported("This build has no HiGHS backend; configure GECODE_OPTIMIZE_WITH_HIGHS=ON"); +#endif + } catch (const BasisStopped& e) { +#ifdef GECODE_OPTIMIZE_WITH_HIGHS + state.invalidate(); +#endif + result.termination=budget.stop_reason().value_or(Termination::Unknown); + result.message=e.what(); + } catch (const Unsupported& e) { +#ifdef GECODE_OPTIMIZE_WITH_HIGHS + state.invalidate(); +#endif + result.termination = Termination::Unsupported; + result.message = e.what(); + } catch (const ModelError& e) { +#ifdef GECODE_OPTIMIZE_WITH_HIGHS + state.invalidate(); +#endif + result.termination = Termination::InvalidModel; + result.message = e.what(); + } catch (const std::bad_alloc&) { +#ifdef GECODE_OPTIMIZE_WITH_HIGHS + state.invalidate(); +#endif + result.termination = Termination::MemoryLimit; + result.message = "Allocation failed"; + } catch (const std::exception& e) { +#ifdef GECODE_OPTIMIZE_WITH_HIGHS + state.invalidate(); +#endif + result.termination = Termination::BackendError; + result.message = e.what(); + } + result.elapsed_seconds = budget.elapsed_seconds(); +#ifdef GECODE_OPTIMIZE_WITH_HIGHS + if (result.termination == Termination::BackendError || result.termination == Termination::NumericalFailure) + state.invalidate(); +#endif + if (result.termination == Termination::Optimal && budget.expired()) + result.termination = budget.stop_reason().value_or(Termination::Unknown); + return result; +} + +template +static LpObservedResult observed_solve(Snapshot&& snapshot, ModelId owner, + Revision revision, + const LpObservationOptions& options, + SessionState* persistent = nullptr, + bool session_live = true, + const LpBasisSolveOptions* basis_options = nullptr, + LpBasisSubmission* basis_report = nullptr) { +#ifndef GECODE_OPTIMIZE_WITH_HIGHS + (void)persistent; +#endif + const auto started=std::chrono::steady_clock::now(); + LpObservedResult out; + out.result.model_id=owner;out.result.revision=revision;out.result.backend="HiGHS"; + std::shared_ptr observations; + auto failure=[&](Termination termination,LpObservationReason reason,const char* message) noexcept { + out.result.termination=termination; + try { + out.result.message=message; + if (observations) Detail::LpObservationAccess::unavailable(*observations,reason,message); + } catch (...) { + observations.reset();out.observations.reset();out.result.message.clear(); + } + }; + try { + // The same shared clock covers cold snapshot ownership, backend work and + // independent observation checks. Never restart it for collection. + SolveBudget budget(options.solve); + options.checks.validate(); + if (basis_options) basis_options->validate(); + out.result.guarantee=options.solve.guarantee; + observations=Detail::LpObservationAccess::create(snapshot(),options); + out.observations=observations; + const auto& original=observations->source(); + out.result.active_variables.reserve(original.variables.size()); + for (const auto& variable : original.variables) out.result.active_variables.push_back(variable.active); + const auto unsupported=Detail::LpObservationAccess::unsupported(original,options.solve); + if (!session_live) { + failure(Termination::InvalidModel,LpObservationReason::InvalidModel,"Moved-from solve session"); + } else if (!unsupported.empty()) { + // Admission must precede any persistent backend/statistics mutation. + failure(Termination::Unsupported,LpObservationReason::Unsupported,unsupported.c_str()); + } else { + if (basis_options && !budget.expired()) { + try { Detail::LpBasisAccess::compatible(*basis_options->basis,original,budget); } + catch (const ModelError&) { if (!budget.expired()) throw; } + } +#ifndef GECODE_OPTIMIZE_WITH_HIGHS + failure(Termination::Unsupported,LpObservationReason::Unsupported,"This build has no HiGHS LP observation backend"); +#else + if (budget.expired()) { + failure(budget.stop_reason().value_or(Termination::Unknown),LpObservationReason::Interrupted, + "Budget expired before LP observation solve"); + } else { + { + Detail::LpBackendObservations raw; + raw.requested_duals=options.duals;raw.requested_basis=options.basis; + if (persistent) ++persistent->statistics.solve_calls; + out.result=solve_highs(original,options.solve,persistent,&budget,&raw, + basis_options ? basis_options->basis.get() : nullptr,basis_report); + if (out.result.termination == Termination::Unsupported) + Detail::LpObservationAccess::unavailable(*observations,LpObservationReason::Unsupported,out.result.message); + else if (out.result.termination == Termination::InvalidModel) + Detail::LpObservationAccess::unavailable(*observations,LpObservationReason::InvalidModel,out.result.message); + else + Detail::LpObservationAccess::finish(*observations,out.result,raw,budget); + } + // Raw vector release is part of the same cooperative call budget. + Detail::LpObservationAccess::final_budget(out,observations,budget); + } +#endif + } + } catch (const ModelError& error) { + failure(Termination::InvalidModel,LpObservationReason::InvalidModel,error.what()); + } catch (const std::bad_alloc&) { + failure(Termination::MemoryLimit,LpObservationReason::AllocationFailure,"LP observation allocation failed"); + } catch (const std::exception& error) { + failure(Termination::BackendError,LpObservationReason::InvalidBackendData,error.what()); + } + out.result.elapsed_seconds=std::chrono::duration(std::chrono::steady_clock::now()-started).count(); + return out; +} + +LpObservedResult solve_lp_observed(const ModelSnapshot& model,const LpObservationOptions& options) { + return observed_solve([&] {return model;},model.model_id,model.revision,options); +} +LpObservedResult solve_lp_observed(const Model& model,const LpObservationOptions& options) { + return observed_solve([&] {return model.snapshot();},model.id(),model.revision(),options); +} + +template +static LpBasisSolveResult basis_solve(Snapshot&& snapshot,ModelId owner,Revision revision, + const LpBasisSolveOptions& options,SessionState* session=nullptr,bool live=true) { + LpBasisSolveResult out;out.requested_basis=options.basis; + out.observed=observed_solve(std::forward(snapshot),owner,revision, + options.observations,session,live,&options,&out.submission); + return out; +} +LpBasisSolveResult solve_lp_with_basis(const ModelSnapshot& model,const LpBasisSolveOptions& options) { + return basis_solve([&]{return model;},model.model_id,model.revision,options); +} +LpBasisSolveResult solve_lp_with_basis(const Model& model,const LpBasisSolveOptions& options) { + return basis_solve([&]{return model.snapshot();},model.id(),model.revision(),options); +} + +SolveResult solve(const ModelSnapshot& model, const SolveOptions& options) { + if (options.backend == Backend::Native || (options.backend == Backend::Auto && + std::any_of(model.globals.begin(),model.globals.end(),[](const auto& record){return record.active;}))) + return solve_native_auto(model, options); + return solve_highs(model, options); +} + +SolveResult solve(const Model& model, const SolveOptions& options) { + if (options.backend == Backend::Native) return solve_native_auto(model, options); + const auto start = std::chrono::steady_clock::now(); + SolveResult result; + result.model_id = model.id(); + result.revision = model.revision(); + result.backend = "HiGHS"; + try { + auto snapshot = model.snapshot(); + auto adjusted = options; + const double copy_seconds = std::chrono::duration(std::chrono::steady_clock::now()-start).count(); + if (std::isfinite(adjusted.time_limit_seconds) && adjusted.time_limit_seconds >= 0) + adjusted.time_limit_seconds = std::max(0.0, adjusted.time_limit_seconds-copy_seconds); + result = solve(snapshot, adjusted); + } catch (const ModelError& error) { + result.termination = Termination::InvalidModel; + result.message = error.what(); + } catch (const std::bad_alloc&) { + result.termination = Termination::MemoryLimit; + result.message = "Snapshot/options allocation failed"; + } + result.elapsed_seconds = std::chrono::duration(std::chrono::steady_clock::now()-start).count(); + return result; +} + +SolveSession::SolveSession() : impl_(std::make_unique()) {} +SolveSession::~SolveSession() = default; +SolveSession::SolveSession(SolveSession&&) noexcept = default; +SolveSession& SolveSession::operator=(SolveSession&&) noexcept = default; + +SessionStatistics SolveSession::statistics() const noexcept { + return impl_ ? impl_->statistics : SessionStatistics{}; +} + +void SolveSession::reset() { impl_ = std::make_unique(); } + +SolveResult SolveSession::solve(const ModelSnapshot& model, const SolveOptions& options) { + if (!impl_ || options.backend == Backend::Native) { + SolveResult result; + result.model_id = model.model_id; + result.revision = model.revision; + result.backend = "HiGHS"; + result.termination = impl_ ? Termination::Unsupported : Termination::InvalidModel; + result.message = impl_ ? "Persistent Native sessions are not implemented" : "Moved-from solve session"; + return result; + } + ++impl_->statistics.solve_calls; + return solve_highs(model, options, impl_.get()); +} + +SolveResult SolveSession::solve(const Model& model, const SolveOptions& options) { + const auto started = std::chrono::steady_clock::now(); + SolveResult result; + result.model_id = model.id(); + result.revision = model.revision(); + result.backend = "HiGHS"; + try { + auto snapshot = model.snapshot(); + auto adjusted = options; + const double elapsed = std::chrono::duration(std::chrono::steady_clock::now()-started).count(); + if (std::isfinite(adjusted.time_limit_seconds) && adjusted.time_limit_seconds >= 0) + adjusted.time_limit_seconds = std::max(0.0, adjusted.time_limit_seconds-elapsed); + result = solve(snapshot, adjusted); + } catch (const ModelError& error) { + result.termination = Termination::InvalidModel; + result.message = error.what(); + } catch (const std::bad_alloc&) { + result.termination = Termination::MemoryLimit; + result.message = "Snapshot/options allocation failed"; + } + result.elapsed_seconds = std::chrono::duration(std::chrono::steady_clock::now()-started).count(); + return result; +} + +LpObservedResult SolveSession::solve_lp_observed(const ModelSnapshot& model, + const LpObservationOptions& options) { + return observed_solve([&] {return model;},model.model_id,model.revision,options, + impl_.get(),static_cast(impl_)); +} +LpObservedResult SolveSession::solve_lp_observed(const Model& model, + const LpObservationOptions& options) { + return observed_solve([&] {return model.snapshot();},model.id(),model.revision(),options, + impl_.get(),static_cast(impl_)); +} +LpBasisSolveResult SolveSession::solve_lp_with_basis(const ModelSnapshot& model, + const LpBasisSolveOptions& options) { + return basis_solve([&]{return model;},model.model_id,model.revision,options, + impl_.get(),static_cast(impl_)); +} +LpBasisSolveResult SolveSession::solve_lp_with_basis(const Model& model, + const LpBasisSolveOptions& options) { + return basis_solve([&]{return model.snapshot();},model.id(),model.revision(),options, + impl_.get(),static_cast(impl_)); +} + +}} diff --git a/gecode/optimize/solve.hpp b/gecode/optimize/solve.hpp new file mode 100644 index 0000000000..7fa788181b --- /dev/null +++ b/gecode/optimize/solve.hpp @@ -0,0 +1,61 @@ +/* Numerical LP/MILP backend for the additive optimization API. */ +#ifndef GECODE_OPTIMIZE_SOLVE_HPP +#define GECODE_OPTIMIZE_SOLVE_HPP + +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { + +struct BackendCapabilities { + std::string name; + std::string version; + bool available = false; + bool linear_programming = false; + bool mixed_integer_linear = false; + bool exact_solving = false; + bool proof_certificates = false; + bool lazy_constraints = false; + bool quadratic_programming = false; + std::vector limitations; +}; + +/** Query one backend without inspecting a model. Auto reports HiGHS here; + * solve() separately routes models with active native globals to Native. + * Other explicit operations have their own capability queries and contracts. + */ +BackendCapabilities capabilities(Backend backend = Backend::Auto); + +/** + * Solve an owning original-model snapshot. HiGHS is explicitly a numerical + * backend, not the legacy certified Gecode binary-LP propagation engine. + * Auto selects Native for models with active native globals, HiGHS otherwise. + * HiGHS cannot meet Exact/Certified requests. + * Explicit Native selects the bounded exact-integer Gecode bridge with + * conservative structural algorithm selection; see solve_native_auto() in + * native.hpp. The explicit solve_native() entry point retains ordinary BAB. + * Returned incumbents pass the + * independent original-model numerical checker. Time/cancel limits are + * cooperative; elapsed_seconds includes compilation and checking. A late + * candidate is excluded unless captured by an internal HiGHS observation + * callback before the deadline. This does not expose a user callback API. + */ +SolveResult solve(const ModelSnapshot& model, const SolveOptions& options = {}); +SolveResult solve(const Model& model, const SolveOptions& options = {}); + +/** + * Backend-independent, strict numerical LP/free-MPS I/O. Unsupported dialects, + * quadratic models, and exporting active original indicators/globals are rejected. + * Writes preserve double values and atomically replace only after a checked + * semantic round trip. See docs/optimize-io.md for the supported syntax. + */ +Model read_model(const std::string& filename); +void write_model(const ModelSnapshot& model, const std::string& filename); +inline void write_model(const Model& model, const std::string& filename) { + write_model(model.snapshot(), filename); +} + +}} +#endif diff --git a/gecode/optimize/types.hpp b/gecode/optimize/types.hpp new file mode 100644 index 0000000000..9b62278ce9 --- /dev/null +++ b/gecode/optimize/types.hpp @@ -0,0 +1,36 @@ +/* Shared types for the additive optimization API. */ +#ifndef GECODE_OPTIMIZE_TYPES_HPP +#define GECODE_OPTIMIZE_TYPES_HPP + +#include +#include +#include + +namespace Gecode { namespace Optimize { + +using ModelId = std::uint64_t; +using Revision = std::uint64_t; +enum class VariableType { Continuous, Integer, Binary, SemiContinuous, SemiInteger }; +enum class ObjectiveSense { Minimize, Maximize }; +enum class Guarantee { Numerical, Exact, Certified }; +enum class Backend { Auto, Highs, Native }; + +struct Variable { + ModelId model_id = 0; + std::uint64_t id = 0; + bool operator==(const Variable& other) const noexcept { + return model_id == other.model_id && id == other.id; + } + bool operator!=(const Variable& other) const noexcept { return !(*this == other); } +}; +struct Constraint { + ModelId model_id = 0; + std::uint64_t id = 0; +}; +class ModelError : public std::invalid_argument { +public: + explicit ModelError(const std::string& message) : std::invalid_argument(message) {} +}; + +}} +#endif diff --git a/gecode/optimize/validate.cpp b/gecode/optimize/validate.cpp new file mode 100644 index 0000000000..ee83d74f36 --- /dev/null +++ b/gecode/optimize/validate.cpp @@ -0,0 +1,292 @@ +/* Independent original-model checks; no numerical backend is consulted. */ +#include +#include +#include + +#include +#include +#include + +namespace Gecode { namespace Optimize { +namespace { + +using Wide = long double; + +std::string slot(const char* kind, std::size_t index) { + return std::string(kind) + " slot " + std::to_string(index); +} + +void check_range(double lower, double upper, const std::string& location) { + if (std::isnan(lower) || std::isnan(upper)) + throw ModelError(location + ": NaN bound"); + if (lower == std::numeric_limits::infinity() || + upper == -std::numeric_limits::infinity()) + throw ModelError(location + ": invalid infinity in bound"); + if (lower > upper) + throw ModelError(location + ": lower bound exceeds upper bound"); +} + +void check_type(const VariableData& variable, const std::string& location) { + switch (variable.type) { + case VariableType::Continuous: + case VariableType::Integer: + break; + case VariableType::Binary: + if (variable.lower < 0.0 || variable.upper > 1.0) + throw ModelError(location + ": binary bounds must lie in [0,1]"); + break; + case VariableType::SemiContinuous: + case VariableType::SemiInteger: + if (!std::isfinite(variable.lower) || variable.lower <= 0.0) + throw ModelError(location + ": semi-variable lower bound must be finite and positive"); + break; + default: + throw ModelError(location + ": unknown variable type"); + } +} + +void check_terms(const ModelSnapshot& model, const std::vector& terms, + const std::string& location, bool active) { + std::uint64_t previous = 0; + bool first = true; + for (const auto& term : terms) { + if (term.variable.model_id != model.model_id) + throw ModelError(location + ": variable belongs to another model"); + if (term.variable.id >= model.variables.size()) + throw ModelError(location + ": dangling variable reference"); + if (active && !model.variables[static_cast(term.variable.id)].active) + throw ModelError(location + ": deleted variable reference"); + if (!std::isfinite(term.coefficient)) + throw ModelError(location + ": non-finite coefficient"); + if (term.coefficient == 0.0) + throw ModelError(location + ": zero coefficient is not canonical"); + if (!first && term.variable.id <= previous) + throw ModelError(location + ": terms must have ascending unique variable IDs"); + previous = term.variable.id; + first = false; + } +} + +// Compensated summation also helps on platforms where long double has the +// same precision as double. Overflow still invalidates the numerical check. +class WideSum { + Wide sum_ = 0.0L; + Wide correction_ = 0.0L; +public: + bool add(Wide value) { + if (!std::isfinite(value)) + return false; + const Wide next = sum_ + value; + if (!std::isfinite(next)) + return false; + const Wide correction = std::fabs(sum_) >= std::fabs(value) + ? (sum_ - next) + value : (value - next) + sum_; + const Wide next_correction = correction_ + correction; + if (!std::isfinite(next_correction)) + return false; + sum_ = next; + correction_ = next_correction; + return true; + } + bool value(Wide& result) const { + result = sum_ + correction_; + return std::isfinite(result); + } +}; + +bool evaluate(const std::vector& terms, double offset, + const std::vector& values, Wide& result) { + WideSum sum; + if (!sum.add(static_cast(offset))) + return false; + for (const auto& term : terms) { + const Wide product = static_cast(term.coefficient) * + static_cast(values[static_cast(term.variable.id)]); + if (!sum.add(product)) + return false; + } + return sum.value(result); +} + +Wide range_violation(Wide value, double lower, double upper) { + Wide violation = 0.0L; + if (std::isfinite(lower)) + violation = std::max(violation, static_cast(lower) - value); + if (std::isfinite(upper)) + violation = std::max(violation, value - static_cast(upper)); + return violation; +} + +// Include each finite bound in the compensated sum. Collapsing a large row +// activity first can erase a small residual before subtracting its bound (for +// example 1 + 1e16 <= 1e16 on platforms with double-width long double). +bool linear_violation(const std::vector& terms, double lower, double upper, + const std::vector& values, Wide& violation) { + Wide activity = 0.0L; + if (!evaluate(terms, 0.0, values, activity)) + return false; + violation = 0.0L; + Wide residual = 0.0L; + if (std::isfinite(lower)) { + if (!evaluate(terms, -lower, values, residual)) + return false; + violation = std::max(violation, -residual); + } + if (std::isfinite(upper)) { + if (!evaluate(terms, -upper, values, residual)) + return false; + violation = std::max(violation, residual); + } + return true; +} + +void record_violation(Wide violation, double& maximum) { + const double reported = violation > static_cast(std::numeric_limits::max()) + ? std::numeric_limits::infinity() : static_cast(violation); + maximum = std::max(maximum, reported); +} + +void record_failure(ValidationReport& report, const std::string& message) { + if (report.message.empty()) + report.message = message; +} + +} // namespace + +void validate_structure(const ModelSnapshot& model) { + if (model.model_id == 0) + throw ModelError("model ID must be nonzero"); + for (std::size_t i = 0; i < model.variables.size(); ++i) { + const auto& variable = model.variables[i]; + const auto location = slot("variable", i); + if (variable.variable.model_id != model.model_id) + throw ModelError(location + ": owner does not match model"); + if (variable.variable.id != i) + throw ModelError(location + ": ID does not match stable slot"); + check_range(variable.lower, variable.upper, location); + check_type(variable, location); + } + for (std::size_t i = 0; i < model.rows.size(); ++i) { + const auto& row = model.rows[i]; + const auto location = slot("row", i); + if (row.constraint.model_id != model.model_id) + throw ModelError(location + ": owner does not match model"); + if (row.constraint.id != i) + throw ModelError(location + ": ID does not match stable slot"); + check_range(row.lower, row.upper, location); + // Inactive rows can legitimately retain terms referencing tombstones. + check_terms(model, row.terms, location, row.active); + } + if (!std::isfinite(model.objective.offset)) + throw ModelError("objective: non-finite offset"); + switch (model.objective.sense) { + case ObjectiveSense::Minimize: + case ObjectiveSense::Maximize: + break; + default: + throw ModelError("objective: unknown sense"); + } + check_terms(model, model.objective.terms, "objective", true); + Detail::validate_indicators(model); + Detail::validate_globals(model); +} + +ValidationReport validate(const ModelSnapshot& model, + const std::vector& slot_values, + double feasibility_tolerance, + double integrality_tolerance) { + ValidationReport report; + try { + validate_structure(model); + report.model_valid = true; + } catch (const ModelError& error) { + report.message = std::string("invalid model: ") + error.what(); + return report; + } + if (!std::isfinite(feasibility_tolerance) || feasibility_tolerance < 0.0 || + !std::isfinite(integrality_tolerance) || integrality_tolerance < 0.0) { + report.message = "validation tolerances must be finite and nonnegative"; + return report; + } + if (slot_values.size() != model.variables.size()) { + report.message = "assignment size does not match original variable slots"; + return report; + } + const Wide feasibility = static_cast(feasibility_tolerance); + const Wide integrality = static_cast(integrality_tolerance); + for (std::size_t i = 0; i < model.variables.size(); ++i) { + const auto& variable = model.variables[i]; + if (!variable.active) + continue; + if (!std::isfinite(slot_values[i])) { + report.message = slot("variable", i) + ": non-finite assignment"; + return report; + } + const Wide value = static_cast(slot_values[i]); + Wide bound_violation = range_violation(value, variable.lower, variable.upper); + if (variable.type == VariableType::SemiContinuous || + variable.type == VariableType::SemiInteger) + bound_violation = std::min(bound_violation, std::fabs(value)); + record_violation(bound_violation, report.max_bound_violation); + if (!std::isfinite(bound_violation) || bound_violation > feasibility) + record_failure(report, slot("variable", i) + ": bound violation"); + if (variable.type == VariableType::Integer || + variable.type == VariableType::Binary || + variable.type == VariableType::SemiInteger) { + const Wide violation = std::fabs(value - std::round(value)); + record_violation(violation, report.max_integrality_violation); + if (!std::isfinite(violation) || violation > integrality) + record_failure(report, slot("variable", i) + ": integrality violation"); + } + } + for (std::size_t i = 0; i < model.rows.size(); ++i) { + const auto& row = model.rows[i]; + if (!row.active) + continue; + Wide violation = 0.0L; + if (!linear_violation(row.terms, row.lower, row.upper, slot_values, violation)) { + report.message = slot("row", i) + ": non-finite accumulated activity"; + return report; + } + record_violation(violation, report.max_row_violation); + if (!std::isfinite(violation) || violation > feasibility) + record_failure(report, slot("row", i) + ": row violation"); + } + for (std::size_t i = 0; i < model.indicators.size(); ++i) { + const auto& indicator = model.indicators[i]; + if (!indicator.active || + std::round(slot_values[static_cast(indicator.activator.id)]) != + (indicator.active_value ? 1.0 : 0.0)) + continue; + Wide violation = 0.0L; + if (!linear_violation(indicator.terms, indicator.lower, indicator.upper, slot_values, violation)) { + report.message = slot("indicator", i) + ": non-finite original activity"; + return report; + } + record_violation(violation, report.max_indicator_violation); + if (!std::isfinite(violation) || violation > feasibility) + record_failure(report, slot("indicator", i) + ": original logical constraint violated"); + } + for (std::size_t i=0;i static_cast(std::numeric_limits::max())) { + report.message = "objective: non-finite or unrepresentable accumulated value"; + return report; + } + report.objective = static_cast(objective); + report.valid = report.message.empty(); + if (report.valid) + report.message = "original model satisfied within numerical tolerances"; + return report; +} + +}} diff --git a/gecode/optimize/validate.hpp b/gecode/optimize/validate.hpp new file mode 100644 index 0000000000..ee6dbe5975 --- /dev/null +++ b/gecode/optimize/validate.hpp @@ -0,0 +1,59 @@ +/* Independent validation of numerical linear optimization models. */ +#ifndef GECODE_OPTIMIZE_VALIDATE_HPP +#define GECODE_OPTIMIZE_VALIDATE_HPP + +#include + +#include +#include +#include + +namespace Gecode { namespace Optimize { + +struct ValidationReport { + bool valid = false; + // True iff the snapshot itself passed structural validation. This does + // not establish that its domains contain a feasible solution. + bool model_valid = false; + std::string message; + std::optional objective; + double max_bound_violation = 0.0; + double max_row_violation = 0.0; + double max_integrality_violation = 0.0; + double max_indicator_violation = 0.0; + std::size_t violated_globals = 0; +}; + +/** Check a complete original snapshot before a backend or workflow uses it. + * Throws ModelError for malformed IDs, domains, expressions or references. + * Expression terms must have ascending unique slot IDs and finite nonzero + * coefficients, matching the canonical form produced by Model. + * Semi-variable domains are {0} union [lower, upper], with finite lower > 0. + * A mathematically infeasible integer interval is not a structural error. + * Active original indicators require complete lowerings, safe M values, + * intact generated rows/gates and valid captured-domain dependencies. + * Typed global payloads and their referenced variable domains are checked too; + * backend-specific numerical/native admission remains a separate operation. + */ +void validate_structure(const ModelSnapshot& model); + +/** Independently evaluate original domains, rows, indicators, globals and cost. + * Values use original slot indexing: exactly one entry per variable slot; + * unreferenced deleted slots may contain NaN and are ignored. Tolerances must + * be finite and nonnegative, in absolute original-model units. + * Linear arithmetic uses compensated long double accumulation. This is + * numerical validation, not a proof of optimality or infeasibility. + * Original indicators are checked after rounding their binary activator; + * passing their big-M rows alone never establishes logical feasibility. + * Globals check tolerance-qualified integer values against original payloads. + * Malformed models, options and assignments return an invalid report rather + * than throwing ModelError. Allocation failures may still propagate. + */ +ValidationReport validate(const ModelSnapshot& model, + const std::vector& slot_values, + double feasibility_tolerance = 1e-7, + double integrality_tolerance = 1e-6); + +}} + +#endif diff --git a/gecode/optimize/workflow.cpp b/gecode/optimize/workflow.cpp new file mode 100644 index 0000000000..6287cf713b --- /dev/null +++ b/gecode/optimize/workflow.cpp @@ -0,0 +1,337 @@ +#include +#include + +#include +#include +#include +#include +#include + +namespace Gecode { namespace Optimize { +namespace { +class UnsupportedWorkflow : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +struct ObjectiveValue { + long double linear; + long double total; +}; + +class ObjectiveSum { + long double sum_ = 0.0L; + long double correction_ = 0.0L; +public: + void add(long double value) { + const long double next = sum_ + value; + if (!std::isfinite(value) || !std::isfinite(next)) + throw UnsupportedWorkflow("Objective accumulation exceeds supported precision"); + const long double correction = std::fabs(sum_) >= std::fabs(value) + ? (sum_ - next) + value : (value - next) + sum_; + const long double corrected = correction_ + correction; + if (!std::isfinite(corrected)) + throw UnsupportedWorkflow("Objective accumulation exceeds supported precision"); + sum_ = next; + correction_ = corrected; + } + long double value() const { + const long double result = sum_ + correction_; + if (!std::isfinite(result)) + throw UnsupportedWorkflow("Objective accumulation exceeds supported precision"); + return result; + } +}; + +ObjectiveValue evaluate(const ObjectiveData& objective, + const std::vector& values) { + ObjectiveSum linear, total; + // Include the offset before collapsing the compensated terms. A large + // linear subtotal can otherwise lose a small residual before cancellation + // against the offset, even when the final objective is representable. + total.add(static_cast(objective.offset)); + for (const auto& term : objective.terms) { + const long double term_value = static_cast(term.coefficient) * + values[term.variable.id]; + linear.add(term_value); + total.add(term_value); + } + return {linear.value(), total.value()}; +} + +double finite_double(long double value, const char* what) { + if (!std::isfinite(value) || + std::fabs(value) > static_cast(std::numeric_limits::max())) + throw UnsupportedWorkflow(std::string(what) + " exceeds finite double range"); + return static_cast(value); +} + +// Round toward a tighter feasible region so conversion never grants additional +// degradation. Candidate validation detects an unrepresentable restrictive lock. +double row_bound(long double value, ObjectiveSense sense) { + double converted = finite_double(value, "Objective lock"); + if (sense == ObjectiveSense::Minimize && static_cast(converted) > value) + converted = std::nextafter(converted, -std::numeric_limits::infinity()); + if (sense == ObjectiveSense::Maximize && static_cast(converted) < value) + converted = std::nextafter(converted, std::numeric_limits::infinity()); + if (!std::isfinite(converted)) + throw UnsupportedWorkflow("Objective lock cannot be represented conservatively"); + return converted; +} + +struct Retention { + ObjectiveData objective; + long double bound; +}; + +bool retained(const std::vector& locks, + const std::vector& values, double tolerance) { + for (const auto& lock : locks) { + const long double actual = evaluate(lock.objective, values).total; + const long double violation = lock.objective.sense == ObjectiveSense::Minimize + ? actual - lock.bound : lock.bound - actual; + if (violation > static_cast(tolerance)) return false; + } + return true; +} + +bool bound_agrees(const SolveResult& result, double actual, + ObjectiveSense sense, double tolerance) { + if (!result.objective || !result.best_bound || !std::isfinite(*result.best_bound)) + return false; + const double scale = std::max({1.0, std::fabs(actual), + std::fabs(*result.objective), std::fabs(*result.best_bound)}); + const double allowance = std::max(tolerance, + 64.0 * std::numeric_limits::epsilon() * scale); + if (std::fabs(static_cast(actual) - *result.objective) > allowance) + return false; + // Recompute from independent activity and the actual bound. A backend's + // cached zero gap must not establish completion after its values change. + auto checked = result; + checked.objective = actual; + try { checked.update_gaps(sense); } + catch (const ModelError&) { return false; } + return checked.absolute_gap && std::isfinite(*checked.absolute_gap) && + *checked.absolute_gap <= allowance; +} + +void clear_late_solution(SolveResult& result, Termination reason) { + result.termination = reason; + result.message = "Stage returned after the shared workflow budget stopped"; + result.solution_validated = false; + result.values.clear(); + result.objective.reset(); + result.best_bound.reset(); + result.absolute_gap.reset(); + result.relative_gap.reset(); + result.native_backend_gap.reset(); +} + +LexicographicResult run(const ModelSnapshot& original, + const std::vector& objectives, + const SolveOptions& options, SolveBudget& budget) { + LexicographicResult output; + output.model_id = original.model_id; + output.revision = original.revision; + auto finish = [&]() { + output.elapsed_seconds = budget.elapsed_seconds(); + if (output.termination == Termination::Optimal && budget.expired()) { + output.termination = budget.stop_reason().value_or(Termination::Unknown); + output.message = "Shared workflow budget stopped before completion"; + } + return std::move(output); + }; + try { + validate_structure(original); + if (objectives.empty()) throw ModelError("At least one lexicographic objective is required"); + if (options.guarantee != Guarantee::Numerical) + throw UnsupportedWorkflow("Lexicographic workflow supports Numerical guarantees only"); + if (objectives.size() > 1 && options.node_limit) + throw UnsupportedWorkflow("Multi-stage node budgets need consumed-node reporting"); + if (budget.expired()) { + output.termination = budget.stop_reason().value_or(Termination::Unknown); + return finish(); + } + ModelSnapshot working = original; + for (const auto& objective : objectives) { + if (!std::isfinite(objective.absolute_degradation) || objective.absolute_degradation < 0.0 || + !std::isfinite(objective.relative_degradation) || objective.relative_degradation < 0.0) + throw ModelError("Objective degradation must be nonnegative and finite"); + working.objective = objective.objective; + validate_structure(working); // reject later malformed objectives before solving + if (budget.expired()) { + output.termination = budget.stop_reason().value_or(Termination::Unknown); + return finish(); + } + } + std::vector locks; + for (std::size_t i = 0; i < objectives.size(); ++i) { + if (budget.expired()) { + output.termination = budget.stop_reason().value_or(Termination::Unknown); + return finish(); + } + working.objective = objectives[i].objective; + auto stage_options = options; + stage_options.cancellation = budget.cancellation(); + stage_options.time_limit_seconds = budget.remaining_seconds(); + stage_options.relative_gap = 0.0; + stage_options.absolute_gap = 0.0; + if (i != 0) stage_options.primal_start.clear(); + LexicographicStage stage; + stage.index = i; + stage.name = objectives[i].name; + stage.result = solve(working, stage_options); + if (budget.expired()) { + const auto reason = budget.stop_reason().value_or(Termination::Unknown); + clear_late_solution(stage.result, reason); + output.stages.push_back(std::move(stage)); + output.termination = reason; + output.message = "Shared workflow budget stopped during stage " + std::to_string(i); + return finish(); + } + output.stages.push_back(std::move(stage)); + auto& current = output.stages.back(); + auto& result = current.result; + std::optional stage_objective; + + // Even an incomplete stage can supply an independently checked incumbent. + // Never accept a candidate merely because a previous phase accepted it. + if (result.has_solution()) { + const auto original_check = validate(original, result.values, + options.feasibility_tolerance, options.integrality_tolerance); + const auto lock_check = validate(working, result.values, + options.feasibility_tolerance, options.integrality_tolerance); + if (!original_check.valid || !lock_check.valid || + !retained(locks, result.values, options.feasibility_tolerance)) { + output.termination = Termination::NumericalFailure; + output.message = "Independent original-model or objective-lock validation failed"; + return finish(); + } + std::vector vector_values; + vector_values.reserve(objectives.size()); + for (const auto& objective : objectives) + vector_values.push_back(finite_double(evaluate(objective.objective, result.values).total, + "Reported objective")); + stage_objective = vector_values[i]; + if (budget.expired()) { + output.termination = budget.stop_reason().value_or(Termination::Unknown); + clear_late_solution(result, output.termination); + output.message = "Shared workflow budget stopped during candidate validation"; + return finish(); + } + output.final_solution = result; + output.final_solution.model_id = original.model_id; + output.final_solution.revision = original.revision; + output.final_solution.objective = original_check.objective; + output.final_solution.best_bound.reset(); + output.final_solution.absolute_gap.reset(); + output.final_solution.relative_gap.reset(); + output.final_solution.native_backend_gap.reset(); + output.final_solution.termination = Termination::Unknown; + output.final_solution.message = "Validated original-model feasible solution; see workflow stages"; + output.objective_values = std::move(vector_values); + } + + if (result.termination != Termination::Optimal) { + output.termination = result.termination; + output.message = "Lexicographic stage " + std::to_string(i) + " stopped: " + result.message; + if (result.termination == Termination::Infeasible && output.has_solution()) { + output.termination = Termination::NumericalFailure; + output.message = "Stage reported infeasible despite a validated retained solution"; + } + return finish(); + } + if (!result.has_solution() || result.guarantee != Guarantee::Numerical || + !stage_objective || !bound_agrees(result, *stage_objective, + working.objective.sense, options.feasibility_tolerance)) { + output.termination = Termination::NumericalFailure; + output.message = "Stage optimum lacks a validated solution and agreeing numerical global bound"; + return finish(); + } + + if (i + 1 < objectives.size()) { + const auto value = evaluate(working.objective, result.values); + const long double degradation = objectives[i].absolute_degradation + + static_cast(objectives[i].relative_degradation) * std::fabs(value.total); + if (!std::isfinite(degradation)) + throw UnsupportedWorkflow("Objective degradation exceeds supported precision"); + const long double change = working.objective.sense == ObjectiveSense::Minimize + ? degradation : -degradation; + const long double threshold = value.total + change; + // Direct linear evaluation avoids cancelling a large objective offset + // against a rounded reported objective value when forming the row. + const double bound = row_bound(value.linear + change, working.objective.sense); + current.retention_bound = finite_double(threshold, "Objective retention threshold"); + RowData row; + row.constraint = {working.model_id, static_cast(working.rows.size())}; + row.terms = working.objective.terms; + row.name = "__lexicographic_lock_" + std::to_string(i); + if (working.objective.sense == ObjectiveSense::Minimize) row.upper = bound; + else row.lower = bound; + working.rows.push_back(std::move(row)); + locks.push_back({working.objective, threshold}); + const auto retained_check = validate(working, result.values, + options.feasibility_tolerance, options.integrality_tolerance); + if (!retained_check.valid || !retained(locks, result.values, options.feasibility_tolerance)) + throw UnsupportedWorkflow("Objective retention lock loses its incumbent at available precision"); + } + if (budget.expired()) { + output.termination = budget.stop_reason().value_or(Termination::Unknown); + output.message = "Shared workflow budget stopped before stage promotion"; + return finish(); + } + current.completed = true; + ++output.completed_stages; + } + output.termination = Termination::Optimal; + output.message = "All ordered stages completed numerically with requested degradation and feasibility tolerances"; + } catch (const UnsupportedWorkflow& e) { + output.termination = Termination::Unsupported; + output.message = e.what(); + } catch (const ModelError& e) { + output.termination = Termination::InvalidModel; + output.message = e.what(); + } catch (const std::bad_alloc&) { + output.termination = Termination::MemoryLimit; + output.message = "Workflow allocation failed"; + } catch (const std::exception& e) { + output.termination = Termination::BackendError; + output.message = e.what(); + } + return finish(); +} + +LexicographicResult invalid_options(const ModelId id, const Revision revision, + const std::exception& error) { + LexicographicResult result; + result.model_id = id; + result.revision = revision; + result.termination = Termination::InvalidModel; + result.message = error.what(); + return result; +} +} // namespace + +LexicographicResult solve_lexicographic( + const ModelSnapshot& model, const std::vector& objectives, + const SolveOptions& options) { + try { + SolveBudget budget(options); + return run(model, objectives, options, budget); + } catch (const ModelError& e) { + return invalid_options(model.model_id, model.revision, e); + } +} + +LexicographicResult solve_lexicographic( + const Model& model, const std::vector& objectives, + const SolveOptions& options) { + try { + SolveBudget budget(options); // includes owning snapshot copy in the deadline + return run(model.snapshot(), objectives, options, budget); + } catch (const ModelError& e) { + return invalid_options(model.id(), model.revision(), e); + } +} + +}} diff --git a/gecode/optimize/workflow.hpp b/gecode/optimize/workflow.hpp new file mode 100644 index 0000000000..fbe5c86d30 --- /dev/null +++ b/gecode/optimize/workflow.hpp @@ -0,0 +1,74 @@ +/* Ordered linear objective workflows for the numerical optimization API. */ +#ifndef GECODE_OPTIMIZE_WORKFLOW_HPP +#define GECODE_OPTIMIZE_WORKFLOW_HPP + +#include + +namespace Gecode { namespace Optimize { + +/** The input vector establishes priority: first objective has highest priority. */ +struct LexicographicObjective { + ObjectiveData objective; + double absolute_degradation = 0.0; + double relative_degradation = 0.0; + std::string name; +}; + +struct LexicographicStage { + std::size_t index = 0; + std::string name; + SolveResult result; + bool completed = false; + // Threshold in this objective's original units, including its offset. + // A minimization stage supplies an upper threshold; maximization a lower. + std::optional retention_bound; +}; + +struct LexicographicResult { + ModelId model_id = 0; + Revision revision = 0; + Termination termination = Termination::Unknown; + Guarantee guarantee = Guarantee::Numerical; + std::string message; + std::vector stages; + std::size_t completed_stages = 0; + double elapsed_seconds = 0.0; + // Feasible historical solution of the ORIGINAL model. Its scalar objective + // evaluates the original model objective, which this workflow need not have + // optimized; its termination remains Unknown and it has no objective bound. + SolveResult final_solution; + // All requested objectives at final_solution, in the original input order. + std::vector objective_values; + + bool has_solution() const noexcept { return final_solution.has_solution(); } + bool completed_numerically() const noexcept { + return termination == Termination::Optimal && !stages.empty() && + completed_stages == stages.size(); + } +}; + +/** + * Optimize ordered linear objectives on private snapshot copies. Degradation + * after stage i is absolute_degradation + relative_degradation * abs(value_i). + * Both native MIP gap targets are forced to zero. Completion is numerical and + * tolerance-qualified, never an exact certificate of lexicographic optimality. + * All candidates are independently checked against original constraints, + * accumulated lock rows and original objective retention thresholds. + * + * A shared outer clock/token covers copying, validation and every solve. + * Time/cancellation remains cooperative; a stage returned after the outer + * deadline is not promoted. Node-limited multi-stage requests are Unsupported. + * Terms must use the canonical ascending unique slots of ModelSnapshot. + */ +LexicographicResult solve_lexicographic( + const ModelSnapshot& model, + const std::vector& objectives, + const SolveOptions& options = {}); + +LexicographicResult solve_lexicographic( + const Model& model, + const std::vector& objectives, + const SolveOptions& options = {}); + +}} +#endif diff --git a/gecode/search.hh b/gecode/search.hh index 38668804ad..d935a308f6 100755 --- a/gecode/search.hh +++ b/gecode/search.hh @@ -117,6 +117,8 @@ namespace Gecode { namespace Search { const unsigned int c_d = 8; /// Create a clone during recomputation if distance is greater than \a a_d (adaptive distance) const unsigned int a_d = 2; + /// Additional propagation-count checkpoint threshold (zero disables) + const unsigned long int c_p = 0; /// Minimal number of open nodes for stealing const unsigned int steal_limit = 3; @@ -758,6 +760,17 @@ namespace Gecode { namespace Search { unsigned int c_d; /// Create a clone during recomputation if distance is greater than \a a_d (adaptive distance) unsigned int a_d; + /** + * \brief Additional propagation-count checkpoint threshold + * + * Sequential DFS and BAB also clone a branching space when its + * most recent status call performs at least \a c_p propagator + * executions. This can avoid replaying expensive propagation, + * at the cost of retaining more spaces. Zero disables this policy. + * The existing commit and adaptive distances still apply. + * Parallel engines and LDS currently ignore this option. + */ + unsigned long int c_p; /// Discrepancy limit (for LDS) unsigned int d_l; /// Number of assets (engines) in a portfolio diff --git a/gecode/search/options.hpp b/gecode/search/options.hpp index b85d969008..c08de84887 100644 --- a/gecode/search/options.hpp +++ b/gecode/search/options.hpp @@ -37,7 +37,7 @@ namespace Gecode { namespace Search { Options::Options(void) : clone(Config::clone), threads(Config::threads), - c_d(Config::c_d), a_d(Config::a_d), + c_d(Config::c_d), a_d(Config::a_d), c_p(Config::c_p), d_l(Config::d_l), assets(0), slice(Config::slice), nogoods_limit(0), stop(nullptr), cutoff(nullptr), tracer(nullptr) {} diff --git a/gecode/search/seq/bab.hpp b/gecode/search/seq/bab.hpp index 3138fd371e..b860d0e609 100644 --- a/gecode/search/seq/bab.hpp +++ b/gecode/search/seq/bab.hpp @@ -97,6 +97,7 @@ namespace Gecode { namespace Search { namespace Seq { ei.init(tracer.wid(), top.nid(), top.truealt(), *cur, *top.choice()); } unsigned int nid = tracer.nid(); + const unsigned long int propagate_before = propagate; switch (cur->status(*this)) { case SS_FAILED: if (tracer) { @@ -128,7 +129,9 @@ namespace Gecode { namespace Search { namespace Seq { case SS_BRANCH: { Space* c; - if ((d == 0) || (d >= opt.c_d)) { + if ((d == 0) || (d >= opt.c_d) || + ((opt.c_p != 0) && + (propagate - propagate_before >= opt.c_p))) { c = cur->clone(); d = 1; } else { diff --git a/gecode/search/seq/dfs.hpp b/gecode/search/seq/dfs.hpp index e0bc36cf6f..4eec80b055 100644 --- a/gecode/search/seq/dfs.hpp +++ b/gecode/search/seq/dfs.hpp @@ -106,6 +106,7 @@ namespace Gecode { namespace Search { namespace Seq { ei.init(tracer.wid(), top.nid(), top.truealt(), *cur, *top.choice()); } unsigned int nid = tracer.nid(); + const unsigned long int propagate_before = propagate; switch (cur->status(*this)) { case SS_FAILED: if (tracer) { @@ -135,7 +136,9 @@ namespace Gecode { namespace Search { namespace Seq { case SS_BRANCH: { Space* c; - if ((d == 0) || (d >= opt.c_d)) { + if ((d == 0) || (d >= opt.c_d) || + ((opt.c_p != 0) && + (propagate - propagate_before >= opt.c_p))) { c = cur->clone(); d = 1; } else { diff --git a/python/gecode_optimize/__init__.py b/python/gecode_optimize/__init__.py new file mode 100644 index 0000000000..9f37e7cfde --- /dev/null +++ b/python/gecode_optimize/__init__.py @@ -0,0 +1,66 @@ +"""Minimal owning ctypes binding for Gecode's version 1 optimization ABI. + +Set GECODE_OPTIMIZE_LIBRARY to the shared C ABI library, or pass Library(path). +No native dependency is downloaded, installed, or loaded until explicitly used. +""" +from .binding import (ApiError, Backend, Cancellation, Guarantee, GlobalConstraint, Indicator, Library, Model, + Options, Result, Row, Session, Termination, Variable, + VariableType, load_library) +from .binding import (PoolAttempt, PoolCompletion, PoolEntry, PoolOptions, PoolResult, + RelaxationSelection, RelaxationSide, RepairItem, RepairOptions, + RepairResult, RepairStage, RepairVariable, Validation) + +__all__ = ["ApiError", "Backend", "Cancellation", "Guarantee", "GlobalConstraint", "Indicator", "Library", "Model", + "Options", "Result", "Row", "Session", "Termination", "Variable", + "VariableType", "load_library"] +__all__ += ["PoolAttempt", "PoolCompletion", "PoolEntry", "PoolOptions", "PoolResult", + "RelaxationSelection", "RelaxationSide", "RepairItem", "RepairOptions", + "RepairResult", "RepairStage", "RepairVariable", "Validation"] + +from .binding import VariableSpec, RowSpec, SparseRowBatch +__all__ += ["VariableSpec", "RowSpec", "SparseRowBatch"] + +from .binding import QuadraticModel, QuadraticOptions, QuadraticResult, QuadraticChecks, WeightedSquare +__all__ += ["QuadraticModel", "QuadraticOptions", "QuadraticResult", "QuadraticChecks", "WeightedSquare"] + +from .binding import (LpObservationOptions, LpObservationState, LpObservationReason, + LpBasisStatus, LpDualSource, LpObservationCapabilities, + LpObservationGroup, LpObservationMetadata, LpKktReport, + LpRowObservation, LpColumnObservation, LpObservations, LpObservedResult) +__all__ += ["LpObservationOptions", "LpObservationState", "LpObservationReason", + "LpBasisStatus", "LpDualSource", "LpObservationCapabilities", + "LpObservationGroup", "LpObservationMetadata", "LpKktReport", + "LpRowObservation", "LpColumnObservation", "LpObservations", "LpObservedResult"] + +from .binding import LpBasis, LpBasisInfo, LpBasisOrigin, LpBasisSubmissionState, LpBasisSubmission, LpBasisSolveResult +__all__ += ["LpBasis", "LpBasisInfo", "LpBasisOrigin", "LpBasisSubmissionState", "LpBasisSubmission", "LpBasisSolveResult"] + +from .binding import RegularTransition +__all__ += ["RegularTransition"] + +from .binding import (ScenarioReuse, ScenarioRunState, ScenarioBatchCompletion, ScenarioId, + ScenarioVariableBounds, ScenarioRowBounds, ScenarioDefinition, ScenarioOptions, + ScenarioStatistics, ScenarioBatchInfo, ScenarioResultInfo, ScenarioCheck, + ScenarioOutcome, ScenarioBatchResult) +__all__ += ["ScenarioReuse", "ScenarioRunState", "ScenarioBatchCompletion", "ScenarioId", + "ScenarioVariableBounds", "ScenarioRowBounds", "ScenarioDefinition", "ScenarioOptions", + "ScenarioStatistics", "ScenarioBatchInfo", "ScenarioResultInfo", "ScenarioCheck", + "ScenarioOutcome", "ScenarioBatchResult"] + +from .binding import (LpEvidenceRequest, LpEvidenceState, LpEvidenceReason, LpEvidenceCompletion, + LpEvidencePhase, LpEvidenceSide, LpEvidenceColumnKind, LpEvidenceOptions, + LpEvidenceInfo, LpEvidenceGroup, LpEvidenceMetadata, LpPrimalEvidence, + LpFarkasEvidence, LpEvidenceSlot, LpEvidenceDiagnostics, LpEvidenceRawResult, + LpEvidenceStageInfo, LpEvidenceColumn, LpEvidenceRawValue, LpEvidenceResult, LpEvidenceStage) +__all__ += ["LpEvidenceRequest", "LpEvidenceState", "LpEvidenceReason", "LpEvidenceCompletion", + "LpEvidencePhase", "LpEvidenceSide", "LpEvidenceColumnKind", "LpEvidenceOptions", + "LpEvidenceInfo", "LpEvidenceGroup", "LpEvidenceMetadata", "LpPrimalEvidence", + "LpFarkasEvidence", "LpEvidenceSlot", "LpEvidenceDiagnostics", "LpEvidenceRawResult", + "LpEvidenceStageInfo", "LpEvidenceColumn", "LpEvidenceRawValue", "LpEvidenceResult", "LpEvidenceStage"] + +from .binding import (LpSensitivityState, LpSensitivityReason, LpSensitivityCompletion, LpRangeEndKind, + LpSensitivitySide, LpObjectiveParameter, LpEqualityRhsParameter, LpSensitivityTolerances, + LpSensitivityLimits, LpSensitivityOptions, LpSensitivityInfo, LpSensitivityWork, + LpSensitivityGroup, LpRangeEnd, LpSensitivityLimiter, LpIntervalCheckReport, + LpParameterInterval, LpSensitivityEntry, LpSensitivityReferenceChecks, LpSensitivityResult) +__all__ += ['LpSensitivityState', 'LpSensitivityReason', 'LpSensitivityCompletion', 'LpRangeEndKind', 'LpSensitivitySide', 'LpObjectiveParameter', 'LpEqualityRhsParameter', 'LpSensitivityTolerances', 'LpSensitivityLimits', 'LpSensitivityOptions', 'LpSensitivityInfo', 'LpSensitivityWork', 'LpSensitivityGroup', 'LpRangeEnd', 'LpSensitivityLimiter', 'LpIntervalCheckReport', 'LpParameterInterval', 'LpSensitivityEntry', 'LpSensitivityReferenceChecks', 'LpSensitivityResult'] diff --git a/python/gecode_optimize/_runtime.py b/python/gecode_optimize/_runtime.py new file mode 100644 index 0000000000..7abb777862 --- /dev/null +++ b/python/gecode_optimize/_runtime.py @@ -0,0 +1,20 @@ +"""Locate an optional bundled binary without searching the working directory.""" +from pathlib import Path +import sys + + +def bundled_library_path(): + directory = Path(__file__).resolve().parent / "_native" + if not directory.exists(): + return None + names = {"darwin": "libgecodeoptimize_c.dylib", "win32": "gecodeoptimize_c.dll", + "linux": "libgecodeoptimize_c.so"} + if sys.platform not in names: + raise RuntimeError("the installed native bundle does not support this platform") + candidate = directory / names[sys.platform] + if not directory.is_dir() or not candidate.is_file(): + raise RuntimeError("the installed gecode_optimize native bundle is incomplete") + resolved = candidate.resolve() + if resolved.parent != directory.resolve(): + raise RuntimeError("bundled solver library resolves outside its native directory") + return str(resolved) diff --git a/python/gecode_optimize/binding.py b/python/gecode_optimize/binding.py new file mode 100644 index 0000000000..fe6f1ddc1a --- /dev/null +++ b/python/gecode_optimize/binding.py @@ -0,0 +1,2298 @@ +"""Owning Python wrappers for the versioned C ABI; close handles explicitly.""" + +import ctypes as C +from ctypes.util import find_library +from ._runtime import bundled_library_path +from dataclasses import dataclass, field +from enum import IntEnum +import math +import os +import threading +import time + + +class VariableType(IntEnum): + CONTINUOUS=0; INTEGER=1; BINARY=2; SEMI_CONTINUOUS=3; SEMI_INTEGER=4 +class Backend(IntEnum): + AUTO=0; HIGHS=1; NATIVE=2 +class Guarantee(IntEnum): + NUMERICAL=0; EXACT=1; CERTIFIED=2 +class Termination(IntEnum): + UNKNOWN=0; OPTIMAL=1; INFEASIBLE=2; UNBOUNDED=3; INFEASIBLE_OR_UNBOUNDED=4 + TIME_LIMIT=5; NODE_LIMIT=6; MEMORY_LIMIT=7; ITERATION_LIMIT=8; SOLUTION_LIMIT=9 + OBJECTIVE_LIMIT=10; CANCELLED=11; NUMERICAL_FAILURE=12; UNSUPPORTED=13 + INVALID_MODEL=14; BACKEND_ERROR=15 +class PoolCompletion(IntEnum): + INCOMPLETE=0; REQUESTED_LIMIT=1; EXHAUSTED=2 +class RelaxationSide(IntEnum): + LOWER=0; UPPER=1 + + +class ApiError(RuntimeError): + """C ABI misuse/model-building error; distinct from solver Termination.""" + def __init__(self, code, message): + self.code = code + super().__init__(message) + + +U64=C.c_uint64; I64=C.c_int64; I32=C.c_int32; U32=C.c_uint32; F64=C.c_double; U8=C.c_uint8 +class _Id(C.Structure): + _fields_=[("model_id", U64), ("slot", U64), ("kind", U32), ("reserved", U32)] +class _RegularTransition(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("from_state",U64),("symbol",I64),("to_state",U64)] +class _Term(C.Structure): + _fields_=[("variable", _Id), ("coefficient", F64)] +class _Start(C.Structure): + _fields_=[("variable", _Id), ("value", F64)] +class _VariableSpec(C.Structure): + _fields_=[("struct_size",U64),("type",I32),("reserved",I32), + ("lower",F64),("upper",F64),("name",C.c_char_p)] +class _RowSpec(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("terms",C.POINTER(_Term)), + ("term_count",U64),("lower",F64),("upper",F64),("name",C.c_char_p)] +class _SparseRowBatch(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64), + ("columns",C.POINTER(_Id)),("columns_count",U64), + ("row_start",C.POINTER(U64)),("row_start_count",U64), + ("column",C.POINTER(U64)),("column_count",U64), + ("coefficient",C.POINTER(F64)),("coefficient_count",U64), + ("lower",C.POINTER(F64)),("lower_count",U64), + ("upper",C.POINTER(F64)),("upper_count",U64), + ("names",C.POINTER(C.c_char_p)),("names_count",U64)] +class _Options(C.Structure): + _fields_=[("struct_size", U64), ("backend", I32), ("guarantee", I32), ("threads", I32), ("random_seed", I32), + ("time_limit_seconds", F64), ("relative_gap", F64), ("absolute_gap", F64), + ("feasibility_tolerance", F64), ("integrality_tolerance", F64), ("node_limit", U64), + ("has_node_limit", I32), ("reserved", I32), ("cancellation", U64), + ("primal_start", C.POINTER(_Start)), ("primal_start_count", U64)] +class _Info(C.Structure): + _fields_=[("model_id", U64), ("revision", U64), ("variable_slots", U64), ("termination", I32), + ("guarantee", I32), ("has_solution", I32), ("solution_validated", I32), ("start_submitted", I32), + ("reserved", I32), ("elapsed_seconds", F64)] +class _Statistics(C.Structure): + _fields_=[(name, U64) for name in ("solve_calls", "model_loads", "incremental_updates", "unchanged_models", "basis_warm_starts", "incumbent_starts")] +class _OptionalNumber(C.Structure): + _fields_=[("present", I32), ("reserved", I32), ("value", F64)] +class _LpOptions(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("solve",_Options),("duals",I32),("basis",I32)]+[ + (name,F64) for name in ("dual_feasibility","stationarity","complementarity","objective_gap")] +class _LpCapabilities(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64)]+[(name,I32) for name in + ("available","duals","basis_export","reserved_flags")]+[("limitation_count",U64)] +class _LpInfo(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("result",_Info),("has_observations",I32),("reserved_flags",I32)]+[ + (name,U64) for name in ("model_id","revision","row_slots","column_slots")] +class _BasisInfo(C.Structure): + _fields_=[(name,U64) for name in ("struct_size","reserved","model_id","revision","row_slots","column_slots")]+[ + ("origin",I32),("reserved_flags",I32)] +class _BasisResultInfo(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("result",_Info), + ("requested_model_id",U64),("requested_revision",U64)]+[(name,I32) for name in + ("has_requested_basis","state","backend_attempted","has_statuses_changed","statuses_changed","reserved_flags")] +class _LpMetadata(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64)]+[(name,F64) for name in + ("dual_feasibility","stationarity","complementarity","objective_gap","primal_check_tolerance")]+[ + (name,_OptionalNumber) for name in ("backend_primal_tolerance","backend_dual_tolerance")] +class _LpGroup(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("state",I32),("reason",I32)] +class _LpRow(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64)]+[(name,I32) for name in + ("active","dual_source","has_basis","basis")]+[(name,_OptionalNumber) for name in + ("activity","lower_slack","upper_slack","dual")] +class _LpColumn(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64)]+[(name,I32) for name in + ("active","has_basis","basis","reserved_flags")]+[("reduced_cost",_OptionalNumber)] +class _LpChecks(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64)]+[(name,I32) for name in + ("primal_valid","dual_signs_valid","stationarity_valid","complementarity_valid","gap_valid","accepted")]+[ + (name,_OptionalNumber) for name in ("max_dual_sign_violation","max_stationarity","max_complementarity", + "dual_objective_estimate","normalized_gap")] +class _WeightedSquare(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("terms",C.POINTER(_Term)), + ("term_count",U64),("offset",F64),("weight",F64),("name",C.c_char_p)] +class _QuadraticOptions(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("solve",_Options)]+[ + (name,U64) for name in ("iteration_limit","max_auxiliary_variables","max_lifted_nonzeros")]+[ + (name,F64) for name in ("stationarity_tolerance","complementarity_tolerance","optimality_tolerance")] +class _QuadraticInfo(C.Structure): + _fields_=[("result",_Info),("qp_iterations",U64),("regularization",F64)] +class _QuadraticChecks(C.Structure): + _fields_=[(name,I32) for name in ("primal_valid","objective_valid","kkt_available","kkt_valid","bound_valid","reserved")]+[ + (name,_OptionalNumber) for name in ("max_stationarity","max_complementarity","original_objective", + "normalized_lower_bound","gap_upper_bound")]+[ + ("square_count",U64),("gradient_slots",U64)] +class _PoolOptions(C.Structure): + _fields_=[("struct_size", U64), ("solve", _Options), ("max_solutions", U64), + ("has_projection", I32), ("reserved", I32), ("projection", C.POINTER(_Id)), ("projection_count", U64)] +class _PoolInfo(C.Structure): + _fields_=[(name,U64) for name in ("model_id","revision","projection_count","entry_count","attempt_count","ranked_prefix")]+[ + (name,I32) for name in ("termination","completion","guarantee","reserved")]+[("elapsed_seconds",F64)] +class _PoolEntryInfo(C.Structure): + _fields_=[("projection_count",U64),("rank_established",I32),("reserved",I32)] +class _PoolAttemptInfo(C.Structure): + _fields_=[(name,I32) for name in ("termination","guarantee","candidate_accepted","rank_established")]+[ + ("objective",_OptionalNumber),("remaining_bound",_OptionalNumber)] +class _Selection(C.Structure): + _fields_=[("source",_Id),("side",I32),("reserved",I32),("penalty",F64)] +class _RepairOptions(C.Structure): + _fields_=[("struct_size",U64),("solve",_Options),("selections",C.POINTER(_Selection)), + ("selection_count",U64),("optimize_original_objective",I32),("reserved",I32)] +class _RepairInfo(C.Structure): + _fields_=[(name,U64) for name in ("source_model_id","source_revision","private_model_id","private_revision", + "variable_slots","item_count","stage_count","completed_stages")]+[(name,I32) for name in ( + "termination","guarantee","has_private_model","has_repair","minimum_violation_established", + "original_objective_optimized","workflow_termination","workflow_guarantee")]+[ + ("elapsed_seconds",F64),("workflow_elapsed_seconds",F64)] +class _RepairItemInfo(C.Structure): + _fields_=[("source",_Id),("slack",_Id),("penalty_row",_Id),("side",I32),("reserved",I32), + ("original_bound",F64),("penalty",F64)]+[(name,_OptionalNumber) for name in ( + "activity","violation","weighted_violation","slack_value")] +class _RepairStageInfo(C.Structure): + _fields_=[("index",U64),("completed",I32),("reserved",I32),("retention_bound",_OptionalNumber)] +class _ValidationInfo(C.Structure): + _fields_=[("valid",I32),("model_valid",I32),("violated_globals",U64)]+[(name,F64) for name in ( + "max_bound_violation","max_row_violation","max_integrality_violation","max_indicator_violation")]+[("objective",_OptionalNumber)] + + +class _ScenarioId(C.Structure): + _fields_=[("batch_id",U64),("index",U64)] +class _ScenarioBounds(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("entity",_Id),("has_lower",I32),("has_upper",I32),("lower",F64),("upper",F64)] +class _ScenarioDefinition(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("name",C.c_char_p), + ("objective_coefficients",C.POINTER(_Term)),("objective_count",U64),("objective_offset",_OptionalNumber), + ("variable_bounds",C.POINTER(_ScenarioBounds)),("variable_count",U64), + ("row_bounds",C.POINTER(_ScenarioBounds)),("row_count",U64)] +class _ScenarioOptions(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("solve",_Options),("reuse",I32),("reserved_flags",I32)]+[ + (name,U64) for name in ("max_scenarios","max_patch_entries","max_saved_value_slots","max_work")] +class _ScenarioInfo(C.Structure): + _fields_=[(name,U64) for name in ("struct_size","reserved","model_id","revision","batch_id","scenario_count","outcome_count")]+[ + (name,I32) for name in ("has_batch","completion","has_stop_reason","stop_reason","has_offending_scenario","all_resolved")]+[ + (name,U64) for name in ("offending_scenario","attempted","resolved","work")]+[("elapsed_seconds",F64),("reuse_statistics",_Statistics)] +class _ScenarioOutcome(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("scenario",_ScenarioId)]+[ + (name,I32) for name in ("state","has_result","has_check","reserved_flags")]+[ + ("result",_Info),("reuse_delta",_Statistics),("elapsed_seconds",F64)] +class _ScenarioCheck(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64)]+[(name,I32) for name in + ("has_check","identity_valid","candidate_examined","objective_matches","exact_witness_validated","reserved_flags")]+[("validation",_ValidationInfo)] +class _ScenarioDefinitionInfo(C.Structure): + _fields_=[(name,U64) for name in ("struct_size","reserved","objective_count","variable_count","row_count")]+[("objective_offset",_OptionalNumber)] + + +class _EvidenceOptions(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("solve",_Options),("request",I32),("reserved_flags",I32)]+[ + (name,F64) for name in ("recession","stationarity","minimum_improvement","minimum_contradiction")]+[ + (name,U64) for name in ("max_auxiliary_variables","max_auxiliary_rows","max_auxiliary_nonzeros","max_retained_slots","max_work","max_auxiliary_solves")] +class _EvidenceInfo(C.Structure): + _fields_=[(name,U64) for name in ("struct_size","reserved","model_id","revision")]+[ + (name,I32) for name in ("has_evidence","completion","has_stop_reason","stop_reason")]+[ + (name,U64) for name in ("row_slots","column_slots","stage_count","attempted_calls","work")]+[("elapsed_seconds",F64)] +class _EvidenceGroup(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("state",I32),("reason",I32)] +class _EvidenceMetadata(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64)]+[(name,F64) for name in + ("recession","stationarity","minimum_improvement","minimum_contradiction","primal_tolerance")] +class _EvidencePrimal(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("has_base_check",I32),("reserved_flags",I32),("base_check",_ValidationInfo)]+[ + (name,_OptionalNumber) for name in ("direction_scale","normalized_objective_slope","max_variable_recession_violation","max_row_recession_violation")] +class _EvidenceFarkas(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64)]+[(name,_OptionalNumber) for name in + ("multiplier_scale","contradiction_margin","max_stationarity")] +class _EvidenceSlot(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("source",_Id)]+[(name,I32) for name in + ("active","has_side","side","reserved_flags")]+[(name,_OptionalNumber) for name in + ("base_value","direction","multiplier","contribution","selected_bound")] +class _EvidenceRawResult(C.Structure): + _fields_=[(name,U64) for name in ("struct_size","reserved","model_id","revision","value_count","mask_count")]+[ + (name,I32) for name in ("termination_code","guarantee_code","reported_solution_validated","reported_start_submitted")]+[ + ("elapsed_seconds",F64)]+[(name,_OptionalNumber) for name in ("objective","best_bound","absolute_gap","relative_gap","native_gap")] +class _EvidenceStage(C.Structure): + _fields_=[(name,U64) for name in ("struct_size","reserved","index","private_model_id","private_revision","row_count","column_count","nonzeros")]+[ + (name,I32) for name in ("phase","attempted","has_raw_result","candidate_examined")]+[("check",_ValidationInfo),("raw_result",_EvidenceRawResult)] +class _EvidenceColumn(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("private_variable",_Id),("source",_Id)]+[ + (name,I32) for name in ("kind","has_side","side","reserved_flags")] +class _EvidenceRawValue(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("slot",U64),("reported_value",_OptionalNumber),("has_reported_mask",I32),("reported_mask",I32)] + + + +class _SensitivityRequest(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("kind",I32),("reserved_flags",I32),("entity",_Id)] +class _SensitivityChecksOptions(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64)]+[(name,F64) for name in + ("primal_feasibility","dual_feasibility","stationarity","complementarity","objective_gap","system_absolute","system_relative")] +class _SensitivityLimits(C.Structure): + _fields_=[(name,U64) for name in ("struct_size","reserved","max_rows","max_columns","max_nonzeros","max_requests", + "max_basis_solves","max_factor_entries","max_retained_slots","max_work")] +class _SensitivityOptions(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("backend",I32),("reserved_flags",I32), + ("time_limit_seconds",F64),("cancellation",U64),("checks",_SensitivityChecksOptions),("limits",_SensitivityLimits), + ("requests",C.POINTER(_SensitivityRequest)),("request_count",U64)] +class _SensitivityInfo(C.Structure): + _fields_=[(name,U64) for name in ("struct_size","reserved","model_id","revision")]+[(name,I32) for name in + ("completion","reason","has_stop_reason","stop_reason","has_sensitivity","has_basis","guarantee","reserved_flags")]+[ + (name,U64) for name in ("entry_count","factor_order_count","row_slots","column_slots")]+[("elapsed_seconds",F64)] +class _SensitivityWork(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("factor_setup_attempted",I32),("reserved_flags",I32)]+[ + (name,U64) for name in ("basis_solves","coordinator_visits","retained_slots","preparation_visits")] +class _SensitivityGroup(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("state",I32),("reason",I32)] +class _SensitivityEnd(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("kind",I32),("reserved_flags",I32),("value",_OptionalNumber)] +class _SensitivityLimiter(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("entity",_Id),("side",I32),("dual_condition",I32)] +class _SensitivityIntervalChecks(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("inequalities",U64)]+[(name,I32) for name in + ("accepted","lower_direction_checked","upper_direction_checked","reserved_flags")]+[("max_endpoint_violation",_OptionalNumber)] +class _SensitivityEntry(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("request",_SensitivityRequest),("group",_SensitivityGroup),("index",U64)]+[ + (name,I32) for name in ("requested","has_interval","has_lower_limiter","has_upper_limiter")]+[ + ("anchor",F64),("lower",_SensitivityEnd),("upper",_SensitivityEnd),("objective_slope",_OptionalNumber), + ("lower_limiter",_SensitivityLimiter),("upper_limiter",_SensitivityLimiter),("checks",_SensitivityIntervalChecks)] +class _SensitivityReferenceChecks(C.Structure): + _fields_=[("struct_size",U64),("reserved",U64),("primal",_ValidationInfo),("kkt",_LpChecks), + ("basis_point_matches",I32),("reserved_flags",I32)]+[(name,_OptionalNumber) for name in + ("max_point_difference","max_system_residual","max_scaled_system_residual")] + + +def _integer(value, bits, signed=False): + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError("integer argument must be an int, not a float/bool") + low=-(1 << (bits-1)) if signed else 0 + high=(1 << (bits-1))-1 if signed else (1 << bits)-1 + if not low <= value <= high: + raise OverflowError("integer does not fit the C ABI field") + return value + + +def _text(value): + if not isinstance(value, str): + raise TypeError("expected str") + if "\0" in value: + raise ValueError("embedded NUL would truncate the C string") + return value.encode("utf-8") + + +class Library: + """One explicitly loaded ABI instance. Keep handles within this instance.""" + def __init__(self, path): + self.path=os.fspath(path) + self._dll=C.CDLL(self.path) + self._dll.gecode_opt_v1_abi_version.argtypes=[] + self._dll.gecode_opt_v1_abi_version.restype=U32 + if self._dll.gecode_opt_v1_abi_version() != 1: + raise RuntimeError("unsupported Gecode optimization C ABI version") + self._dll.gecode_opt_v1_last_error.argtypes=[] + self._dll.gecode_opt_v1_last_error.restype=C.c_char_p + ptr=C.POINTER + signatures={ + "sensitivity_options_default": [ptr(_SensitivityOptions),U64], + "analyze_lp_sensitivity": [U64,ptr(_SensitivityOptions),ptr(U64)], + "sensitivity_info": [U64,ptr(_SensitivityInfo),U64], + "sensitivity_work": [U64,ptr(_SensitivityWork),U64], + "sensitivity_checks_options": [U64,ptr(_SensitivityChecksOptions),U64], + "sensitivity_reference_checks": [U64,ptr(_SensitivityReferenceChecks),U64], + "sensitivity_copy_source_observed": [U64,ptr(U64)], + "sensitivity_copy_basis": [U64,ptr(U64)], + "sensitivity_entry": [U64,U64,ptr(_SensitivityEntry),U64], + "sensitivity_entries": [U64,ptr(_SensitivityEntry),U64,U64,ptr(U64)], + "sensitivity_objective": [U64,_Id,ptr(_SensitivityEntry),U64], + "sensitivity_equality_rhs": [U64,_Id,ptr(_SensitivityEntry),U64], + "sensitivity_factor_order": [U64,ptr(_Id),U64,ptr(U64)], + "sensitivity_active_slots": [U64,I32,ptr(U8),U64,ptr(U64)], + "sensitivity_text": [U64,I32,U64,ptr(C.c_char),U64,ptr(U64)], + "evidence_options_default": [ptr(_EvidenceOptions),U64], + "analyze_lp_evidence": [U64,ptr(_EvidenceOptions),ptr(U64)], + "lp_evidence_info": [U64,ptr(_EvidenceInfo),U64], + "lp_evidence_group": [U64,I32,ptr(_EvidenceGroup),U64], + "lp_evidence_metadata": [U64,ptr(_EvidenceMetadata),U64], + "lp_evidence_primal": [U64,ptr(_EvidencePrimal),U64], + "lp_evidence_farkas": [U64,ptr(_EvidenceFarkas),U64], + "lp_evidence_slot": [U64,_Id,ptr(_EvidenceSlot),U64], + "lp_evidence_slots": [U64,I32,ptr(_EvidenceSlot),U64,U64,ptr(U64)], + "lp_evidence_value": [U64,_Id,I32,ptr(F64)], + "lp_evidence_multiplier": [U64,_Id,ptr(_EvidenceSlot),U64], + "lp_evidence_text": [U64,I32,ptr(C.c_char),U64,ptr(U64)], + "lp_evidence_copy_stage": [U64,U64,ptr(U64)], + "lp_evidence_stage_info": [U64,ptr(_EvidenceStage),U64], + "lp_evidence_stage_columns": [U64,ptr(_EvidenceColumn),U64,U64,ptr(U64)], + "lp_evidence_stage_raw_values": [U64,ptr(_EvidenceRawValue),U64,U64,ptr(U64)], + "lp_evidence_stage_text": [U64,I32,ptr(C.c_char),U64,ptr(U64)], + "scenario_options_default": [ptr(_ScenarioOptions),U64], + "solve_scenarios": [U64,ptr(_ScenarioDefinition),U64,U64,ptr(_ScenarioOptions),ptr(U64)], + "scenario_batch_info": [U64,ptr(_ScenarioInfo),U64], + "scenario_batch_id": [U64,U64,ptr(_ScenarioId)], + "scenario_batch_outcome": [U64,_ScenarioId,ptr(_ScenarioOutcome),U64], + "scenario_batch_check": [U64,_ScenarioId,ptr(_ScenarioCheck),U64], + "scenario_batch_copy_result": [U64,_ScenarioId,ptr(U64)], + "scenario_batch_map": [U64,_Id,ptr(_Id)], + "scenario_batch_value": [U64,_ScenarioId,_Id,ptr(F64)], + "scenario_batch_message": [U64,ptr(C.c_char),U64,ptr(U64)], + "scenario_batch_text": [U64,_ScenarioId,I32,ptr(C.c_char),U64,ptr(U64)], + "scenario_batch_definition": [U64,_ScenarioId,ptr(_ScenarioDefinitionInfo),U64], + "scenario_batch_objective": [U64,_ScenarioId,ptr(_Term),U64,ptr(U64)], + "scenario_batch_bounds": [U64,_ScenarioId,I32,ptr(_ScenarioBounds),U64,U64,ptr(U64)], + "basis_from_observed": [U64,ptr(U64)], + "basis_from_model": [U64,ptr(I32),U64,ptr(I32),U64,ptr(U64)], + "basis_info": [U64,ptr(_BasisInfo),U64], + "basis_statuses": [U64,I32,ptr(I32),U64,ptr(U64)], + "basis_row": [U64,_Id,ptr(I32)], "basis_column": [U64,_Id,ptr(I32)], + "solve_lp_with_basis": [U64,U64,ptr(_LpOptions),ptr(U64)], + "session_solve_lp_with_basis": [U64,U64,U64,ptr(_LpOptions),ptr(U64)], + "basis_result_info": [U64,ptr(_BasisResultInfo),U64], + "basis_result_message": [U64,ptr(C.c_char),U64,ptr(U64)], + "basis_result_copy_observed": [U64,ptr(U64)], "basis_result_copy_basis": [U64,ptr(U64)], + "lp_capabilities": [ptr(_LpCapabilities),U64], + "lp_capability_text": [I32,U64,ptr(C.c_char),U64,ptr(U64)], + "lp_options_default": [ptr(_LpOptions),U64], + "solve_lp_observed": [U64,ptr(_LpOptions),ptr(U64)], + "session_solve_lp_observed": [U64,U64,ptr(_LpOptions),ptr(U64)], + "lp_observed_result_copy_result": [U64,ptr(U64)], + "lp_observed_result_info": [U64,ptr(_LpInfo),U64], + "lp_observed_result_metadata": [U64,ptr(_LpMetadata),U64], + "lp_observed_result_group": [U64,I32,ptr(_LpGroup),U64], + "lp_observed_result_checks": [U64,ptr(_LpChecks),U64], + "lp_observed_result_row": [U64,_Id,ptr(_LpRow),U64], + "lp_observed_result_column": [U64,_Id,ptr(_LpColumn),U64], + "lp_observed_result_rows": [U64,ptr(_LpRow),U64,U64,ptr(U64)], + "lp_observed_result_columns": [U64,ptr(_LpColumn),U64,U64,ptr(U64)], + "lp_observed_result_text": [U64,I32,ptr(C.c_char),U64,ptr(U64)], + "quadratic_capabilities": [ptr(I32)], + "quadratic_options_default": [ptr(_QuadraticOptions),U64], + "quadratic_model_create": [ptr(U64)], "quadratic_model_identity": [U64,ptr(U64),ptr(U64)], + "quadratic_model_add_continuous": [U64,F64,F64,C.c_char_p,ptr(_Id)], + "quadratic_model_add_row": [U64,ptr(_Term),U64,F64,F64,C.c_char_p,ptr(_Id)], + "quadratic_model_set_objective": [U64,ptr(_WeightedSquare),U64,ptr(_Term),U64,I32,F64], + "quadratic_model_set_variable_bounds": [U64,_Id,F64,F64], + "quadratic_model_set_row_bounds": [U64,_Id,F64,F64], + "quadratic_model_set_coefficient": [U64,_Id,_Id,F64], + "quadratic_model_remove_variable": [U64,_Id], "quadratic_model_remove_row": [U64,_Id], + "quadratic_solve": [U64,ptr(_QuadraticOptions),ptr(U64)], + "quadratic_result_info": [U64,ptr(_QuadraticInfo),U64], + "quadratic_result_checks": [U64,ptr(_QuadraticChecks),U64], + "quadratic_result_number": [U64,I32,ptr(I32),ptr(F64)], + "quadratic_result_value": [U64,_Id,ptr(F64)], + "quadratic_result_values": [U64,ptr(F64),ptr(U8),ptr(U8),U64,ptr(U64)], + "quadratic_result_array": [U64,I32,ptr(F64),U64,ptr(U64)], + "quadratic_result_text": [U64,I32,ptr(C.c_char),U64,ptr(U64)], + "options_default": [ptr(_Options), U64], "capabilities": [I32, ptr(I32), ptr(I32), ptr(I32)], + "model_create": [ptr(U64)], "model_identity": [U64, ptr(U64), ptr(U64)], + "model_read": [C.c_char_p, ptr(U64)], "model_write": [U64, C.c_char_p], + "model_add_variable": [U64, I32, F64, F64, C.c_char_p, ptr(_Id)], + "model_add_row": [U64, ptr(_Term), U64, F64, F64, C.c_char_p, ptr(_Id)], + "model_add_variables": [U64,ptr(_VariableSpec),U64,ptr(_Id),U64], + "model_add_rows": [U64,ptr(_RowSpec),U64,ptr(_Id),U64], + "model_add_rows_sparse": [U64,ptr(_SparseRowBatch),ptr(_Id),U64], + "model_set_objective": [U64, ptr(_Term), U64, I32, F64], + "model_set_coefficient": [U64, _Id, _Id, F64], + "model_set_objective_coefficient": [U64, _Id, F64], "model_set_objective_offset": [U64, F64], + "solve": [U64, ptr(_Options), ptr(U64)], "session_solve": [U64, U64, ptr(_Options), ptr(U64)], + "session_create": [ptr(U64)], "session_reset": [U64], "session_statistics": [U64, ptr(_Statistics), U64], + "model_add_all_different": [U64, ptr(_Id), U64, C.c_char_p, ptr(_Id)], + "model_add_element": [U64, _Id, ptr(_Id), U64, _Id, I64, C.c_char_p, ptr(_Id)], + "model_add_table": [U64, ptr(_Id), U64, ptr(I64), U64, U64, C.c_char_p, ptr(_Id)], + "model_add_cumulative": [U64, ptr(_Id), U64, ptr(I64), U64, ptr(I64), U64, I64, C.c_char_p, ptr(_Id)], + "model_add_circuit": [U64, ptr(_Id), U64, I64, C.c_char_p, ptr(_Id)], + "model_add_regular": [U64,ptr(_Id),U64,U64,U64,ptr(_RegularTransition),U64,U64,ptr(U64),U64,C.c_char_p,ptr(_Id)], + "model_remove_global": [U64, _Id], "model_set_global_name": [U64, _Id, C.c_char_p], + "model_add_indicator": [U64, _Id, I32, ptr(_Term), U64, F64, F64, C.c_char_p, ptr(_Id), ptr(I32), ptr(_Id)], + "model_remove_indicator": [U64, _Id], + "model_add_boolean_and": [U64, _Id, ptr(_Id), U64, C.c_char_p], + "model_add_boolean_or": [U64, _Id, ptr(_Id), U64, C.c_char_p], + "cancellation_create": [ptr(U64)], "cancellation_cancel": [U64], + "cancellation_is_cancelled": [U64,ptr(I32)], + "cancellation_copy": [U64,ptr(U64)], + "result_info": [U64, ptr(_Info), U64], "result_number": [U64, I32, ptr(I32), ptr(F64)], + "result_value": [U64, _Id, ptr(F64)], + "result_values": [U64, ptr(F64), ptr(U8), ptr(U8), U64, ptr(U64)], + "result_text": [U64, I32, ptr(C.c_char), U64, ptr(U64)], + "pool_options_default": [ptr(_PoolOptions),U64], "pool_solve": [U64,ptr(_PoolOptions),ptr(U64)], + "pool_info": [U64,ptr(_PoolInfo),U64], "pool_projection": [U64,ptr(_Id),U64,ptr(U64)], + "pool_entry_info": [U64,U64,ptr(_PoolEntryInfo),U64], "pool_entry_result": [U64,U64,ptr(U64)], + "pool_entry_projection": [U64,U64,ptr(I64),U64,ptr(U64)], + "pool_attempt_info": [U64,U64,ptr(_PoolAttemptInfo),U64], "pool_message": [U64,ptr(C.c_char),U64,ptr(U64)], + "repair_options_default": [ptr(_RepairOptions),U64], "repair_solve": [U64,ptr(_RepairOptions),ptr(U64)], + "repair_info": [U64,ptr(_RepairInfo),U64], "repair_number": [U64,I32,ptr(I32),ptr(F64)], + "repair_original_value": [U64,_Id,ptr(F64)], "repair_original_values": [U64,ptr(F64),ptr(U8),ptr(U8),U64,ptr(U64)], + "repair_variable_map": [U64,ptr(_Id),ptr(_Id),ptr(U8),U64,ptr(U64)], + "repair_validation": [U64,ptr(_ValidationInfo),U64], "repair_item_info": [U64,U64,ptr(_RepairItemInfo),U64], + "repair_stage_info": [U64,U64,ptr(_RepairStageInfo),U64], "repair_stage_result": [U64,U64,ptr(U64)], + "repair_final_result": [U64,ptr(U64)], "repair_violation_lock": [U64,ptr(I32),ptr(_Id)], + "repair_objective_values": [U64,ptr(F64),U64,ptr(U64)], + "repair_text": [U64,I32,U64,ptr(C.c_char),U64,ptr(U64)], + } + for entity in ("model", "session", "result", "cancellation", "pool", "repair", "quadratic_model", "quadratic_result", "lp_observed_result", "basis", "basis_result", "scenario_batch", "lp_evidence", "lp_evidence_stage", "sensitivity"): + signatures[entity+"_destroy"]=[U64] + for entity in ("variable", "row"): + signatures["model_set_"+entity+"_bounds"]=[U64, _Id, F64, F64] + signatures["model_set_"+entity+"_name"]=[U64, _Id, C.c_char_p] + signatures["model_remove_"+entity]=[U64, _Id] + self._functions={} + for name, signature in signatures.items(): + try: + function=getattr(self._dll, "gecode_opt_v1_"+name) + except AttributeError as error: + raise RuntimeError("C ABI library lacks required additive symbol: gecode_opt_v1_"+name) from error + function.argtypes=signature; function.restype=I32 + self._functions[name]=function + # Runtime structure-size handshake catches ctypes layout mismatches. + temporary=_Options();self.call("options_default", C.byref(temporary), C.sizeof(temporary)) + + def call(self, name, *args): + code=self._functions[name](*args) + if code: + message=self._dll.gecode_opt_v1_last_error() + raise ApiError(code, message.decode("utf-8", errors="replace") if message else "C ABI error") + + def lp_observation_capabilities(self): + out=_LpCapabilities();self.call("lp_capabilities",C.byref(out),C.sizeof(out)) + def text(field,index=0): + count=U64();self.call("lp_capability_text",field,index,None,0,C.byref(count)) + buffer=C.create_string_buffer(count.value) + self.call("lp_capability_text",field,index,buffer,count.value,C.byref(count)) + return buffer.value.decode("utf-8",errors="replace") + return LpObservationCapabilities(bool(out.available),bool(out.duals),bool(out.basis_export), + text(0),text(1),tuple(text(2,i) for i in range(out.limitation_count))) + + def quadratic_capabilities(self): + """Availability of the explicit finite-box continuous numerical QP scope.""" + available=I32();self.call("quadratic_capabilities",C.byref(available)) + return {"available":bool(available.value)} + + def capabilities(self, backend=Backend.AUTO): + available=I32(); lp=I32(); mip=I32() + self.call("capabilities", int(Backend(backend)), C.byref(available), C.byref(lp), C.byref(mip)) + return {"available": bool(available.value), "linear_programming": bool(lp.value), "mixed_integer_linear": bool(mip.value)} + + +_libraries={} +_library_lock=threading.Lock() +def load_library(path=None): + if isinstance(path, Library): + return path + if path is None: + path=os.environ.get("GECODE_OPTIMIZE_LIBRARY") or bundled_library_path() or find_library("gecodeoptimize_c") + if not path: + raise RuntimeError("set GECODE_OPTIMIZE_LIBRARY to the built shared C ABI library") + key=os.fspath(path) + with _library_lock: + if key not in _libraries: + _libraries[key]=Library(key) + return _libraries[key] + + +@dataclass(frozen=True) +class Variable: + model_id: int + slot: int + _library: Library=field(repr=False) + def _id(self): + return _Id(_integer(self.model_id,64), _integer(self.slot,64), 1, 0) +@dataclass(frozen=True) +class Row: + model_id: int + slot: int + _library: Library=field(repr=False) + def _id(self): + return _Id(_integer(self.model_id,64), _integer(self.slot,64), 2, 0) + +@dataclass(frozen=True) +class GlobalConstraint: + model_id: int + slot: int + _library: Library=field(repr=False) + def _id(self): + return _Id(_integer(self.model_id,64), _integer(self.slot,64), 3, 0) +@dataclass(frozen=True) +class Indicator: + model_id: int + slot: int + _library: Library=field(repr=False) + inactive_gate: object=None + def _id(self): + return _Id(_integer(self.model_id,64), _integer(self.slot,64), 4, 0) + + +def _entity(value, library, expected=None): + if not isinstance(value, expected or (Variable, Row, GlobalConstraint, Indicator)): + raise TypeError("wrong model entity handle type") + if value._library is not library: + raise ValueError("handle belongs to another loaded ABI instance") + return value._id() + + +def _terms(items, library): + if hasattr(items, "items"): + items=items.items() + data=[_Term(_entity(v,library,Variable),float(c)) for v,c in items] + return (_Term*len(data))(*data) + + +def _ids(values, library): + data=[_entity(value,library,Variable) for value in values] + return (_Id*len(data))(*data) + + +def _int64s(values): + data=[_integer(value,64,True) for value in values] + return (I64*len(data))(*data) + + +@dataclass +class VariableSpec: + type: VariableType=VariableType.CONTINUOUS + lower: float=0.0 + upper: object=None # As with scalar construction: Binary 1, otherwise +inf. + name: str="" + + +@dataclass +class RowSpec: + terms: object=() + lower: float=-math.inf + upper: float=math.inf + name: str="" + + +@dataclass +class SparseRowBatch: + columns: object=() + row_start: object=(0,) + column: object=() + coefficient: object=() + lower: object=() + upper: object=() + names: object=() + + +@dataclass +class Options: + backend: Backend=Backend.AUTO + guarantee: Guarantee=Guarantee.NUMERICAL + threads: int=1 + random_seed: int=0 + time_limit_seconds: float=math.inf + relative_gap: float=1e-4 + absolute_gap: float=1e-6 + feasibility_tolerance: float=1e-7 + integrality_tolerance: float=1e-6 + node_limit: object=None + cancellation: object=None + primal_start: object=() + + def _marshal(self, library): + out=_Options();library.call("options_default",C.byref(out),C.sizeof(out)) + out.backend=int(Backend(self.backend));out.guarantee=int(Guarantee(self.guarantee)) + out.threads=_integer(self.threads,32,True);out.random_seed=_integer(self.random_seed,32,True) + for name in ("time_limit_seconds","relative_gap","absolute_gap","feasibility_tolerance","integrality_tolerance"): + setattr(out,name,float(getattr(self,name))) + if self.node_limit is not None: + out.has_node_limit=1;out.node_limit=_integer(self.node_limit,64) + if self.cancellation is not None: + if not isinstance(self.cancellation,Cancellation) or self.cancellation._library is not library: + raise TypeError("cancellation must belong to the same ABI instance") + out.cancellation=self.cancellation._open() + entries=self.primal_start.items() if hasattr(self.primal_start,"items") else self.primal_start + starts=[_Start(_entity(v,library,Variable),float(value)) for v,value in entries] + array=(_Start*len(starts))(*starts) + out.primal_start=array;out.primal_start_count=len(starts) + out._keepalive=(array,self.cancellation) + return out + + +@dataclass +class PoolOptions: + solve: Options=field(default_factory=Options) + max_solutions: int=10 + projection: object=None + def _marshal(self, library): + if not isinstance(self.solve,Options): + raise TypeError("solve must be Options") + out=_PoolOptions();library.call("pool_options_default",C.byref(out),C.sizeof(out)) + solve=self.solve._marshal(library);out.solve=solve + out.max_solutions=_integer(self.max_solutions,64) + array=None + if self.projection is not None: + array=_ids(self.projection,library);out.has_projection=1 + out.projection=array;out.projection_count=len(array) + out._keepalive=(solve,array) + return out + + +@dataclass(frozen=True) +class RelaxationSelection: + source: object + side: RelaxationSide=RelaxationSide.LOWER + penalty: float=1.0 + + +@dataclass +class RepairOptions: + solve: Options=field(default_factory=Options) + selections: object=() + optimize_original_objective: bool=False + def _marshal(self, library): + if not isinstance(self.solve,Options): + raise TypeError("solve must be Options") + if type(self.optimize_original_objective) is not bool: + raise TypeError("optimize_original_objective must be bool") + data=[] + for selection in self.selections: + if not isinstance(selection,RelaxationSelection): + raise TypeError("selections must contain RelaxationSelection records") + data.append(_Selection(_entity(selection.source,library,(Variable,Row)), + int(RelaxationSide(selection.side)),0,float(selection.penalty))) + out=_RepairOptions();library.call("repair_options_default",C.byref(out),C.sizeof(out)) + solve=self.solve._marshal(library);out.solve=solve + array=(_Selection*len(data))(*data);out.selections=array;out.selection_count=len(array) + out.optimize_original_objective=int(self.optimize_original_objective) + out._keepalive=(solve,array) + return out + + +class _Owner: + _kind=None + def _initialize(self, library, handle=None): + self._library=load_library(library);self._handle=0 + if handle is None: + out=U64();self._library.call(self._kind+"_create",C.byref(out));self._handle=out.value + else: + self._handle=handle + @property + def closed(self): + return not self._handle + def _open(self): + if self.closed: + raise RuntimeError(self._kind+" is closed") + return self._handle + def close(self): + if getattr(self,"_handle",0): + self._library.call(self._kind+"_destroy",self._handle) + self._handle=0 + def __enter__(self): + self._open();return self + def __exit__(self,*unused): + self.close() + def __del__(self): + try: + self.close() + except Exception: + pass + + +class Model(_Owner): + _kind="model" + def __init__(self, library=None): + self._initialize(library) + @classmethod + def read(cls, filename, library=None): + """Read LP/MPS into an independent owner, closed explicitly or by context.""" + lib = load_library(library) + model = cls.__new__(cls) + model._initialize(lib, 0) + out = U64() + lib.call("model_read", _text(os.fspath(filename)), C.byref(out)) + try: + model._handle = out.value + except BaseException: + lib.call("model_destroy", out) + raise + return model + @property + def identity(self): + owner=U64();revision=U64();self._library.call("model_identity",self._open(),C.byref(owner),C.byref(revision)) + return owner.value,revision.value + def add_variable(self, type=VariableType.CONTINUOUS, lower=0.0, upper=None, name=""): + kind=VariableType(type);upper=(1.0 if kind==VariableType.BINARY else math.inf) if upper is None else upper + out=_Id();self._library.call("model_add_variable",self._open(),int(kind),float(lower),float(upper),_text(name),C.byref(out)) + return Variable(out.model_id,out.slot,self._library) + def add_row(self, terms=(), lower=-math.inf, upper=math.inf, name=""): + array=_terms(terms,self._library);out=_Id() + self._library.call("model_add_row",self._open(),array,len(array),float(lower),float(upper),_text(name),C.byref(out)) + return Row(out.model_id,out.slot,self._library) + def add_variables(self, specs): + """Atomically add an ordinary sequence of VariableSpec records.""" + data=[];keepalive=[] + for spec in specs: + if not isinstance(spec,VariableSpec): + raise TypeError("expected VariableSpec entries") + kind=VariableType(spec.type) + upper=(1.0 if kind==VariableType.BINARY else math.inf) if spec.upper is None else spec.upper + name=_text(spec.name);keepalive.append(name) + data.append(_VariableSpec(C.sizeof(_VariableSpec),int(kind),0,float(spec.lower),float(upper),name)) + array=(_VariableSpec*len(data))(*data);out=(_Id*len(data))() + self._library.call("model_add_variables",self._open(),array,len(array),out,len(out)) + return tuple(Variable(v.model_id,v.slot,self._library) for v in out) + def add_rows(self, specs): + """Atomically add RowSpec entries; each terms field accepts a mapping or pairs.""" + data=[];keepalive=[] + for spec in specs: + if not isinstance(spec,RowSpec): + raise TypeError("expected RowSpec entries") + terms=_terms(spec.terms,self._library);name=_text(spec.name);keepalive.append((terms,name)) + data.append(_RowSpec(C.sizeof(_RowSpec),0,terms,len(terms),float(spec.lower),float(spec.upper),name)) + array=(_RowSpec*len(data))(*data);out=(_Id*len(data))() + self._library.call("model_add_rows",self._open(),array,len(array),out,len(out)) + return tuple(Row(r.model_id,r.slot,self._library) for r in out) + def add_rows_sparse(self, batch): + """Add a CSR batch in one C call, copying ordinary sequences, without NumPy.""" + if not isinstance(batch,SparseRowBatch): + raise TypeError("expected SparseRowBatch") + record=_SparseRowBatch();record.struct_size=C.sizeof(record);keepalive=[] + columns=_ids(batch.columns,self._library);record.columns=columns;record.columns_count=len(columns) + keepalive.append(columns) + for name in ("row_start","column"): + data=[_integer(v,64) for v in getattr(batch,name)] + array=(U64*len(data))(*data);setattr(record,name,array);setattr(record,name+"_count",len(array)) + keepalive.append(array) + for name in ("coefficient","lower","upper"): + data=[float(v) for v in getattr(batch,name)] + array=(F64*len(data))(*data);setattr(record,name,array);setattr(record,name+"_count",len(array)) + keepalive.append(array) + names=[_text(v) for v in batch.names];array=(C.c_char_p*len(names))(*names) + record.names=array;record.names_count=len(array);keepalive.extend((names,array)) + out=(_Id*record.lower_count)() + self._library.call("model_add_rows_sparse",self._open(),C.byref(record),out,len(out)) + return tuple(Row(r.model_id,r.slot,self._library) for r in out) + def set_objective(self, terms=(), *, maximize=False, offset=0.0): + if type(maximize) is not bool: + raise TypeError("maximize must be bool") + array=_terms(terms,self._library);self._library.call("model_set_objective",self._open(),array,len(array),int(maximize),float(offset)) + def set_bounds(self, entity, lower, upper): + identifier=_entity(entity,self._library,(Variable,Row));kind="variable" if isinstance(entity,Variable) else "row" + self._library.call("model_set_"+kind+"_bounds",self._open(),identifier,float(lower),float(upper)) + def set_coefficient(self, row, variable, value): + self._library.call("model_set_coefficient",self._open(),_entity(row,self._library,Row),_entity(variable,self._library,Variable),float(value)) + def set_objective_coefficient(self, variable, value): + self._library.call("model_set_objective_coefficient",self._open(),_entity(variable,self._library,Variable),float(value)) + def set_objective_offset(self, value): + self._library.call("model_set_objective_offset",self._open(),float(value)) + def set_name(self, entity, name): + identifier=_entity(entity,self._library,(Variable,Row,GlobalConstraint)) + kind="variable" if isinstance(entity,Variable) else "row" if isinstance(entity,Row) else "global" + self._library.call("model_set_"+kind+"_name",self._open(),identifier,_text(name)) + def remove(self, entity): + identifier=_entity(entity,self._library) + kind=("variable" if isinstance(entity,Variable) else "row" if isinstance(entity,Row) + else "global" if isinstance(entity,GlobalConstraint) else "indicator") + self._library.call("model_remove_"+kind,self._open(),identifier) + def add_all_different(self, variables, name=""): + array=_ids(variables,self._library);out=_Id() + self._library.call("model_add_all_different",self._open(),array,len(array),_text(name),C.byref(out)) + return GlobalConstraint(out.model_id,out.slot,self._library) + def add_element(self, index, elements, result, index_base=0, name=""): + array=_ids(elements,self._library);out=_Id() + self._library.call("model_add_element",self._open(),_entity(index,self._library,Variable),array,len(array), + _entity(result,self._library,Variable),_integer(index_base,64,True),_text(name),C.byref(out)) + return GlobalConstraint(out.model_id,out.slot,self._library) + def add_table(self, variables, tuples, name=""): + array=_ids(variables,self._library);rows=[tuple(row) for row in tuples] + if any(len(row)!=len(array) for row in rows): + raise ValueError("table tuple arity must match its variable count") + values=_int64s(value for row in rows for value in row);out=_Id() + self._library.call("model_add_table",self._open(),array,len(array),values,len(values),len(rows),_text(name),C.byref(out)) + return GlobalConstraint(out.model_id,out.slot,self._library) + def add_cumulative(self, starts, durations, heights, capacity, name=""): + array=_ids(starts,self._library);ds=_int64s(durations);hs=_int64s(heights);out=_Id() + self._library.call("model_add_cumulative",self._open(),array,len(array),ds,len(ds),hs,len(hs), + _integer(capacity,64,True),_text(name),C.byref(out)) + return GlobalConstraint(out.model_id,out.slot,self._library) + def add_circuit(self, successors, index_base=0, name=""): + array=_ids(successors,self._library);out=_Id() + self._library.call("model_add_circuit",self._open(),array,len(array),_integer(index_base,64,True),_text(name),C.byref(out)) + return GlobalConstraint(out.model_id,out.slot,self._library) + def add_regular(self, variables, state_count, initial_state, transitions, final_states, name=""): + array=_ids(variables,self._library) + edges=[] + for edge in transitions: + if not isinstance(edge,RegularTransition): + raise TypeError("transitions must contain RegularTransition records") + edges.append(_RegularTransition(C.sizeof(_RegularTransition),0,_integer(edge.from_state,64), + _integer(edge.symbol,64,True),_integer(edge.to_state,64))) + edges=(_RegularTransition*len(edges))(*edges) + finals=[_integer(value,64) for value in final_states];finals=(U64*len(finals))(*finals);out=_Id() + self._library.call("model_add_regular",self._open(),array,len(array),_integer(state_count,64),_integer(initial_state,64), + edges,len(edges),C.sizeof(_RegularTransition),finals,len(finals),_text(name),C.byref(out)) + return GlobalConstraint(out.model_id,out.slot,self._library) + def add_indicator(self, activator, active_value, terms=(), lower=-math.inf, upper=math.inf, name=""): + if type(active_value) is not bool: + raise TypeError("indicator active_value must be bool") + array=_terms(terms,self._library);out=_Id();has_gate=I32();gate=_Id() + self._library.call("model_add_indicator",self._open(),_entity(activator,self._library,Variable),int(active_value), + array,len(array),float(lower),float(upper),_text(name),C.byref(out),C.byref(has_gate),C.byref(gate)) + auxiliary=Variable(gate.model_id,gate.slot,self._library) if has_gate.value else None + return Indicator(out.model_id,out.slot,self._library,auxiliary) + def add_boolean_and(self, result, inputs=(), name=""): + array=_ids(inputs,self._library) + self._library.call("model_add_boolean_and",self._open(),_entity(result,self._library,Variable),array,len(array),_text(name)) + def add_boolean_or(self, result, inputs=(), name=""): + array=_ids(inputs,self._library) + self._library.call("model_add_boolean_or",self._open(),_entity(result,self._library,Variable),array,len(array),_text(name)) + def write(self, filename): + self._library.call("model_write",self._open(),_text(os.fspath(filename))) + def solve(self, options=None): + if options is not None and not isinstance(options,Options): + raise TypeError("options must be Options") + native=(options or Options())._marshal(self._library);out=U64() + self._library.call("solve",self._open(),C.byref(native),C.byref(out));return Result(self._library,out.value) + def analyze_lp_evidence(self, options=None): + """Explicit numerical ray/Farkas recovery in private auxiliary models.""" + started=time.monotonic() + if options is not None and not isinstance(options,LpEvidenceOptions): + raise TypeError("options must be LpEvidenceOptions") + native=(options or LpEvidenceOptions())._marshal(self._library) + if math.isfinite(native.solve.time_limit_seconds) and native.solve.time_limit_seconds>=0: + native.solve.time_limit_seconds=max(0.0,native.solve.time_limit_seconds-(time.monotonic()-started)) + return _own_call(self,LpEvidenceResult,"analyze_lp_evidence",C.byref(native)) + def solve_scenarios(self, definitions, options=None): + """Serial owning batch. Sparse absolute patches; one whole-batch allowance.""" + start=time.monotonic() + if options is not None and not isinstance(options,ScenarioOptions): + raise TypeError("options must be ScenarioOptions") + native=(options or ScenarioOptions())._marshal(self._library) + array=_scenario_definitions(definitions,self._library) + if math.isfinite(native.solve.time_limit_seconds) and native.solve.time_limit_seconds>=0: + native.solve.time_limit_seconds=max(0.0,native.solve.time_limit_seconds-(time.monotonic()-start)) + return _own_call(self,ScenarioBatchResult,"solve_scenarios",array,len(array),C.sizeof(_ScenarioDefinition),C.byref(native)) + def solve_lp_observed(self, options=None): + if options is not None and not isinstance(options,LpObservationOptions): + raise TypeError("options must be LpObservationOptions") + native=(options or LpObservationOptions())._marshal(self._library) + return _own_call(self,LpObservedResult,"solve_lp_observed",C.byref(native)) + def solve_lp_with_basis(self, basis, options=None): + native=_basis_solve_options(self,basis,options) + return _own_call(self,LpBasisSolveResult,"solve_lp_with_basis",basis._open(),C.byref(native)) + def solve_pool(self, options=None): + if options is not None and not isinstance(options,PoolOptions): + raise TypeError("options must be PoolOptions") + native=(options or PoolOptions())._marshal(self._library) + return _own_call(self,PoolResult,"pool_solve",C.byref(native)) + def relax_feasibility(self, options=None): + if options is not None and not isinstance(options,RepairOptions): + raise TypeError("options must be RepairOptions") + native=(options or RepairOptions())._marshal(self._library) + return _own_call(self,RepairResult,"repair_solve",C.byref(native)) + + +class Cancellation(_Owner): + _kind="cancellation" + def __init__(self, library=None): + self._initialize(library) + def cancel(self): + self._library.call("cancellation_cancel",self._open()) + @property + def cancelled(self): + out=I32();self._library.call("cancellation_is_cancelled",self._open(),C.byref(out));return bool(out.value) + def copy(self): + """Independent owner of the same thread-safe cancellation state.""" + return _own_call(self,Cancellation,"cancellation_copy") + + +class Session(_Owner): + _kind="session" + def __init__(self, library=None): + self._initialize(library) + def reset(self): + self._library.call("session_reset",self._open()) + @property + def statistics(self): + out=_Statistics();self._library.call("session_statistics",self._open(),C.byref(out),C.sizeof(out)) + return {name:getattr(out,name) for name,_ in out._fields_} + def solve(self, model, options=None): + if not isinstance(model,Model) or model._library is not self._library: + raise TypeError("model must belong to the same ABI instance") + if options is not None and not isinstance(options,Options): + raise TypeError("options must be Options") + native=(options or Options())._marshal(self._library);out=U64() + self._library.call("session_solve",self._open(),model._open(),C.byref(native),C.byref(out));return Result(self._library,out.value) + def solve_lp_observed(self, model, options=None): + if not isinstance(model,Model) or model._library is not self._library: + raise TypeError("model must belong to the same ABI instance") + if options is not None and not isinstance(options,LpObservationOptions): + raise TypeError("options must be LpObservationOptions") + native=(options or LpObservationOptions())._marshal(self._library) + return _own_call(self,LpObservedResult,"session_solve_lp_observed",model._open(),C.byref(native)) + + def solve_lp_with_basis(self, model, basis, options=None): + if not isinstance(model,Model) or model._library is not self._library: + raise TypeError("model must belong to the same ABI instance") + native=_basis_solve_options(self,basis,options) + return _own_call(self,LpBasisSolveResult,"session_solve_lp_with_basis",model._open(),basis._open(),C.byref(native)) + + +class Result(_Owner): + """Immutable historical result; remains usable after Model/Session.close().""" + _kind="result" + def __init__(self, library, handle): + self._initialize(library,handle) + @property + def info(self): + out=_Info();self._library.call("result_info",self._open(),C.byref(out),C.sizeof(out)) + data={name:getattr(out,name) for name,_ in out._fields_ if name!="reserved"} + data["termination"]=Termination(data["termination"]);data["guarantee"]=Guarantee(data["guarantee"]) + for key in ("has_solution","solution_validated","start_submitted"): + data[key]=bool(data[key]) + return data + @property + def termination(self): + return self.info["termination"] + @property + def has_solution(self): + return self.info["has_solution"] + def _number(self, field): + present=I32();value=F64();self._library.call("result_number",self._open(),field,C.byref(present),C.byref(value)) + return value.value if present.value else None + objective=property(lambda self:self._number(0)) + best_bound=property(lambda self:self._number(1)) + absolute_gap=property(lambda self:self._number(2)) + relative_gap=property(lambda self:self._number(3)) + native_gap=property(lambda self:self._number(4)) + def value(self, variable): + out=F64();self._library.call("result_value",self._open(),_entity(variable,self._library,Variable),C.byref(out));return out.value + @property + def values(self): + """Copy per-slot records: active, present, and value (None if absent).""" + count=U64();self._library.call("result_values",self._open(),None,None,None,0,C.byref(count)) + values=(F64*count.value)();active=(U8*count.value)();present=(U8*count.value)() + self._library.call("result_values",self._open(),values,active,present,count.value,C.byref(count)) + return [{"active":bool(active[i]),"present":bool(present[i]),"value":values[i] if present[i] else None} for i in range(count.value)] + def _text(self, field): + size=U64();self._library.call("result_text",self._open(),field,None,0,C.byref(size)) + buffer=C.create_string_buffer(size.value);self._library.call("result_text",self._open(),field,buffer,size.value,C.byref(size)) + return buffer.value.decode("utf-8",errors="replace") + backend=property(lambda self:self._text(0)) + backend_version=property(lambda self:self._text(1)) + message=property(lambda self:self._text(2)) + + +def _own_call(owner, cls, function, *args): + """Prepare the Python owner before allocating its C token.""" + result=cls.__new__(cls);result._initialize(owner._library,0) + out=U64();owner._library.call(function,owner._open(),*args,C.byref(out)) + try: + result._handle=out.value + except BaseException: + owner._library.call(cls._kind+"_destroy",out) + raise + return result + + +def _record(owner, function, cls, *args): + out=cls();owner._library.call(function,owner._open(),*args,C.byref(out),C.sizeof(out));return out + + +def _metadata(record): + result={} + for name,_ in record._fields_: + if name!="reserved": + value=getattr(record,name) + result[name]=(value.value if value.present else None) if isinstance(value,_OptionalNumber) else value + return result + + +def _array(owner, function, element, *args): + count=U64();owner._library.call(function,owner._open(),*args,None,0,C.byref(count)) + buffer=(element*count.value)();owner._library.call(function,owner._open(),*args,buffer,count.value,C.byref(count)) + return buffer + + +def _copied_text(owner, function, *args): + buffer=_array(owner,function,C.c_char,*args) + return buffer.value.decode("utf-8",errors="replace") + + +def _returned_id(identifier, library): + if identifier.model_id==0: + return None + classes={1:Variable,2:Row,3:GlobalConstraint,4:Indicator} + if identifier.reserved or identifier.kind not in classes: + raise RuntimeError("C ABI returned an invalid entity identity") + return classes[identifier.kind](identifier.model_id,identifier.slot,library) + + +@dataclass(frozen=True) +class PoolEntry: + projection_values: tuple + rank_established: bool + + +@dataclass(frozen=True) +class PoolAttempt: + termination: Termination + guarantee: Guarantee + candidate_accepted: bool + rank_established: bool + objective: object + remaining_bound: object + + +class PoolResult(_Owner): + """Owning workflow; use entry_result(i) to obtain an independent Result.""" + _kind="pool" + def __init__(self, library, handle): + self._initialize(library,handle) + @property + def info(self): + data=_metadata(_record(self,"pool_info",_PoolInfo)) + data["termination"]=Termination(data["termination"]);data["guarantee"]=Guarantee(data["guarantee"]) + data["completion"]=PoolCompletion(data["completion"]);return data + termination=property(lambda self:self.info["termination"]) + completion=property(lambda self:self.info["completion"]) + exhausted=property(lambda self:self.completion==PoolCompletion.EXHAUSTED) + ranked_prefix=property(lambda self:self.info["ranked_prefix"]) + message=property(lambda self:_copied_text(self,"pool_message")) + @property + def projection(self): + return tuple(_returned_id(value,self._library) for value in _array(self,"pool_projection",_Id)) + def entry(self, index): + index=_integer(index,64);info=_record(self,"pool_entry_info",_PoolEntryInfo,index) + return PoolEntry(tuple(_array(self,"pool_entry_projection",I64,index)),bool(info.rank_established)) + def entry_result(self, index): + return _own_call(self,Result,"pool_entry_result",_integer(index,64)) + def attempt(self, index): + info=_metadata(_record(self,"pool_attempt_info",_PoolAttemptInfo,_integer(index,64))) + info["termination"]=Termination(info["termination"]);info["guarantee"]=Guarantee(info["guarantee"]) + info["candidate_accepted"]=bool(info["candidate_accepted"]);info["rank_established"]=bool(info["rank_established"]) + return PoolAttempt(**info) + + +@dataclass(frozen=True) +class RepairItem: + source: object + slack: object + penalty_row: object + side: RelaxationSide + original_bound: float + penalty: float + activity: object + violation: object + weighted_violation: object + slack_value: object + name: str + + +@dataclass(frozen=True) +class RepairStage: + index: int + completed: bool + retention_bound: object + name: str + + +@dataclass(frozen=True) +class RepairVariable: + source: Variable + private: object + active: bool + + +@dataclass(frozen=True) +class Validation: + valid: bool + model_valid: bool + violated_globals: int + max_bound_violation: float + max_row_violation: float + max_integrality_violation: float + max_indicator_violation: float + objective: object + message: str + + +class RepairResult(_Owner): + """Owning repair evidence; original values are not a feasible solve result.""" + _kind="repair" + def __init__(self, library, handle): + self._initialize(library,handle) + @property + def info(self): + data=_metadata(_record(self,"repair_info",_RepairInfo)) + for name in ("termination","workflow_termination"): + data[name]=Termination(data[name]) + for name in ("guarantee","workflow_guarantee"): + data[name]=Guarantee(data[name]) + for name in ("has_private_model","has_repair","minimum_violation_established","original_objective_optimized"): + data[name]=bool(data[name]) + return data + termination=property(lambda self:self.info["termination"]) + has_repair=property(lambda self:self.info["has_repair"]) + def _number(self, field): + present=I32();value=F64();self._library.call("repair_number",self._open(),field,C.byref(present),C.byref(value)) + return value.value if present.value else None + minimum_weighted_violation=property(lambda self:self._number(0)) + weighted_violation=property(lambda self:self._number(1)) + original_objective=property(lambda self:self._number(2)) + message=property(lambda self:_copied_text(self,"repair_text",0,0)) + workflow_message=property(lambda self:_copied_text(self,"repair_text",1,0)) + def original_value(self, variable): + out=F64();self._library.call("repair_original_value",self._open(),_entity(variable,self._library,Variable),C.byref(out)) + return out.value + @property + def original_values(self): + count=U64();self._library.call("repair_original_values",self._open(),None,None,None,0,C.byref(count)) + values=(F64*count.value)();active=(U8*count.value)();present=(U8*count.value)() + self._library.call("repair_original_values",self._open(),values,active,present,count.value,C.byref(count)) + return tuple({"active":bool(active[i]),"present":bool(present[i]),"value":values[i] if present[i] else None} for i in range(count.value)) + @property + def variable_map(self): + count=U64();self._library.call("repair_variable_map",self._open(),None,None,None,0,C.byref(count)) + source=(_Id*count.value)();private=(_Id*count.value)();active=(U8*count.value)() + self._library.call("repair_variable_map",self._open(),source,private,active,count.value,C.byref(count)) + return tuple(RepairVariable(_returned_id(source[i],self._library),_returned_id(private[i],self._library),bool(active[i])) for i in range(count.value)) + @property + def original_validation(self): + info=_metadata(_record(self,"repair_validation",_ValidationInfo)) + info["valid"]=bool(info["valid"]);info["model_valid"]=bool(info["model_valid"]) + return Validation(**info,message=_copied_text(self,"repair_text",2,0)) + def item(self, index): + index=_integer(index,64);info=_metadata(_record(self,"repair_item_info",_RepairItemInfo,index)) + for name in ("source","slack","penalty_row"): + info[name]=_returned_id(info[name],self._library) + info["side"]=RelaxationSide(info["side"]) + return RepairItem(**info,name=_copied_text(self,"repair_text",3,index)) + def stage(self, index): + index=_integer(index,64);info=_metadata(_record(self,"repair_stage_info",_RepairStageInfo,index)) + info["completed"]=bool(info["completed"]) + return RepairStage(**info,name=_copied_text(self,"repair_text",4,index)) + def stage_result(self, index): + return _own_call(self,Result,"repair_stage_result",_integer(index,64)) + def final_result(self): + return _own_call(self,Result,"repair_final_result") + @property + def violation_lock(self): + present=I32();out=_Id();self._library.call("repair_violation_lock",self._open(),C.byref(present),C.byref(out)) + return _returned_id(out,self._library) if present.value else None + objective_values=property(lambda self:tuple(_array(self,"repair_objective_values",F64))) + + +@dataclass +class WeightedSquare: + """Positive weight times (sum(coefficient*variable) + offset)**2.""" + terms: object=() + offset: float=0.0 + weight: float=1.0 + name: str="" + + +@dataclass +class QuadraticOptions: + solve: Options=field(default_factory=Options) + iteration_limit: int=100000 + max_auxiliary_variables: int=100000 + max_lifted_nonzeros: int=2000000 + stationarity_tolerance: float=1e-7 + complementarity_tolerance: float=1e-7 + optimality_tolerance: float=1e-6 + + def _marshal(self, library): + if not isinstance(self.solve,Options): + raise TypeError("solve must be Options") + out=_QuadraticOptions();library.call("quadratic_options_default",C.byref(out),C.sizeof(out)) + solve=self.solve._marshal(library);out.solve=solve + for name in ("iteration_limit","max_auxiliary_variables","max_lifted_nonzeros"): + setattr(out,name,_integer(getattr(self,name),64)) + for name in ("stationarity_tolerance","complementarity_tolerance","optimality_tolerance"): + setattr(out,name,float(getattr(self,name))) + out._keepalive=(solve,) + return out + + +class QuadraticModel(_Owner): + """Distinct finite-box continuous QP owner; no linear Model conversion.""" + _kind="quadratic_model" + def __init__(self, library=None): + self._initialize(library) + @property + def identity(self): + owner=U64();revision=U64() + self._library.call("quadratic_model_identity",self._open(),C.byref(owner),C.byref(revision)) + return owner.value,revision.value + def add_continuous(self, lower, upper, name=""): + out=_Id();self._library.call("quadratic_model_add_continuous",self._open(),float(lower),float(upper),_text(name),C.byref(out)) + return Variable(out.model_id,out.slot,self._library) + def add_row(self, terms=(), lower=-math.inf, upper=math.inf, name=""): + array=_terms(terms,self._library);out=_Id() + self._library.call("quadratic_model_add_row",self._open(),array,len(array),float(lower),float(upper),_text(name),C.byref(out)) + return Row(out.model_id,out.slot,self._library) + def _objective(self, squares, linear, offset, maximize): + data=[];keepalive=[] + for square in squares: + if not isinstance(square,WeightedSquare): + raise TypeError("squares must contain WeightedSquare records") + terms=_terms(square.terms,self._library);name=_text(square.name);keepalive.append((terms,name)) + data.append(_WeightedSquare(C.sizeof(_WeightedSquare),0,terms,len(terms),float(square.offset),float(square.weight),name)) + array=(_WeightedSquare*len(data))(*data);linear=_terms(linear,self._library) + self._library.call("quadratic_model_set_objective",self._open(),array,len(array),linear,len(linear),maximize,float(offset)) + def minimize_squares(self, squares=(), linear=(), offset=0.0): + """Minimize linear + offset + sum(positive weighted squares).""" + self._objective(squares,linear,offset,0) + def maximize_concave_squares(self, squares=(), linear=(), offset=0.0): + """Maximize linear + offset MINUS sum(positive weighted squares).""" + self._objective(squares,linear,offset,1) + def set_bounds(self, entity, lower, upper): + identifier=_entity(entity,self._library,(Variable,Row)) + kind="variable" if isinstance(entity,Variable) else "row" + self._library.call("quadratic_model_set_"+kind+"_bounds",self._open(),identifier,float(lower),float(upper)) + def set_coefficient(self, row, variable, value): + self._library.call("quadratic_model_set_coefficient",self._open(),_entity(row,self._library,Row), + _entity(variable,self._library,Variable),float(value)) + def remove(self, entity): + identifier=_entity(entity,self._library,(Variable,Row)) + kind="variable" if isinstance(entity,Variable) else "row" + self._library.call("quadratic_model_remove_"+kind,self._open(),identifier) + def solve(self, options=None): + if options is not None and not isinstance(options,QuadraticOptions): + raise TypeError("options must be QuadraticOptions") + native=(options or QuadraticOptions())._marshal(self._library) + return _own_call(self,QuadraticResult,"quadratic_solve",C.byref(native)) + + +@dataclass(frozen=True) +class QuadraticChecks: + primal_valid: bool + objective_valid: bool + kkt_available: bool + kkt_valid: bool + bound_valid: bool + max_stationarity: object + max_complementarity: object + original_objective: object + normalized_lower_bound: object + gap_upper_bound: object + square_count: int + gradient_slots: int + message: str + + +class QuadraticResult(_Owner): + """Owning original QP result and numerical checks, distinct from Result.""" + _kind="quadratic_result" + def __init__(self, library, handle): + self._initialize(library,handle) + @property + def info(self): + out=_record(self,"quadratic_result_info",_QuadraticInfo);data=_metadata(out.result) + data["termination"]=Termination(data["termination"]);data["guarantee"]=Guarantee(data["guarantee"]) + for name in ("has_solution","solution_validated","start_submitted"): + data[name]=bool(data[name]) + data["qp_iterations"]=out.qp_iterations;data["regularization"]=out.regularization + return data + termination=property(lambda self:self.info["termination"]) + has_solution=property(lambda self:self.info["has_solution"]) + def _number(self, field): + present=I32();value=F64() + self._library.call("quadratic_result_number",self._open(),field,C.byref(present),C.byref(value)) + return value.value if present.value else None + objective=property(lambda self:self._number(0)) + best_bound=property(lambda self:self._number(1)) + absolute_gap=property(lambda self:self._number(2)) + relative_gap=property(lambda self:self._number(3)) + native_gap=property(lambda self:self._number(4)) + vendor_objective=property(lambda self:self._number(5)) + vendor_dual_estimate=property(lambda self:self._number(6)) + def value(self, variable): + out=F64();self._library.call("quadratic_result_value",self._open(),_entity(variable,self._library,Variable),C.byref(out)) + return out.value + @property + def values(self): + count=U64();self._library.call("quadratic_result_values",self._open(),None,None,None,0,C.byref(count)) + values=(F64*count.value)();active=(U8*count.value)();present=(U8*count.value)() + self._library.call("quadratic_result_values",self._open(),values,active,present,count.value,C.byref(count)) + return tuple({"active":bool(active[i]),"present":bool(present[i]),"value":values[i] if present[i] else None} + for i in range(count.value)) + backend=property(lambda self:_copied_text(self,"quadratic_result_text",0)) + backend_version=property(lambda self:_copied_text(self,"quadratic_result_text",1)) + message=property(lambda self:_copied_text(self,"quadratic_result_text",2)) + @property + def checks(self): + data=_metadata(_record(self,"quadratic_result_checks",_QuadraticChecks)) + for name in ("primal_valid","objective_valid","kkt_available","kkt_valid","bound_valid"): + data[name]=bool(data[name]) + return QuadraticChecks(**data,message=_copied_text(self,"quadratic_result_text",3)) + @property + def square_residuals(self): + """Original affine residuals, not their squared/weighted contributions.""" + if not self.checks.objective_valid: + return None + return tuple(_array(self,"quadratic_result_array",F64,0)) + @property + def original_gradient(self): + if not self.checks.objective_valid: + return None + return tuple(_array(self,"quadratic_result_array",F64,1)) + + +class LpObservationState(IntEnum): + NOT_REQUESTED=0; AVAILABLE=1; UNAVAILABLE=2; REJECTED=3 +class LpObservationReason(IntEnum): + NONE=0; NOT_REQUESTED=1; UNSUPPORTED=2; NO_BACKEND_SOLVE=3; NO_PRIMAL_POINT=4 + NOT_OPTIMAL=5; NO_DUAL_POINT=6; NO_BASIS=7; ELIDED_CONSTANT_ROWS=8; INTERRUPTED=9 + INVALID_BACKEND_DATA=10; FAILED_CHECKS=11; ALLOCATION_FAILURE=12; INVALID_MODEL=13 +class LpBasisStatus(IntEnum): + LOWER=0; BASIC=1; UPPER=2; ZERO=3; NONBASIC_UNSPECIFIED=4 +class LpDualSource(IntEnum): + NONE=0; BACKEND=1; DERIVED_CONSTANT_ROW=2 + + +@dataclass +class LpObservationOptions: + solve: Options=field(default_factory=Options) + duals: bool=True + basis: bool=True + dual_feasibility: float=1e-7 + stationarity: float=1e-7 + complementarity: float=1e-6 + objective_gap: float=1e-6 + def _marshal(self, library): + if not isinstance(self.solve,Options): + raise TypeError("solve must be Options") + if type(self.duals) is not bool or type(self.basis) is not bool: + raise TypeError("duals and basis must be bool") + out=_LpOptions();library.call("lp_options_default",C.byref(out),C.sizeof(out)) + solve=self.solve._marshal(library);out.solve=solve + out.duals=self.duals;out.basis=self.basis + for name in ("dual_feasibility","stationarity","complementarity","objective_gap"): + setattr(out,name,float(getattr(self,name))) + out._keepalive=(solve,) + return out + + +@dataclass(frozen=True) +class LpObservationCapabilities: + available: bool + duals: bool + basis_export: bool + backend: str + backend_version: str + limitations: tuple +@dataclass(frozen=True) +class LpObservationGroup: + state: LpObservationState + reason: LpObservationReason + message: str +@dataclass(frozen=True) +class LpObservationMetadata: + dual_feasibility: float + stationarity: float + complementarity: float + objective_gap: float + primal_check_tolerance: float + backend_primal_tolerance: object + backend_dual_tolerance: object + backend: str + backend_version: str +@dataclass(frozen=True) +class LpKktReport: + primal_valid: bool + dual_signs_valid: bool + stationarity_valid: bool + complementarity_valid: bool + gap_valid: bool + accepted: bool + max_dual_sign_violation: object + max_stationarity: object + max_complementarity: object + dual_objective_estimate: object + normalized_gap: object + message: str +@dataclass(frozen=True) +class LpRowObservation: + active: bool + dual_source: LpDualSource + basis: object + activity: object + lower_slack: object + upper_slack: object + dual: object +@dataclass(frozen=True) +class LpColumnObservation: + active: bool + basis: object + reduced_cost: object +@dataclass(frozen=True) +class LpObservations: + """Copied immutable original-slot data; no solver handle or borrowed child.""" + model_id: int + revision: int + metadata: LpObservationMetadata + primal_rows: LpObservationGroup + dual_point: LpObservationGroup + basis: LpObservationGroup + checks: LpKktReport + rows: tuple + columns: tuple + _library: Library=field(repr=False,compare=False) + def row(self, row): + entity=_entity(row,self._library,Row) + if entity.model_id!=self.model_id or entity.slot>=len(self.rows) or not self.rows[entity.slot].active: + raise ValueError("observation row is foreign, absent or deleted") + return self.rows[entity.slot] + def column(self, variable): + entity=_entity(variable,self._library,Variable) + if entity.model_id!=self.model_id or entity.slot>=len(self.columns) or not self.columns[entity.slot].active: + raise ValueError("observation variable is foreign, absent or deleted") + return self.columns[entity.slot] + + +def _lp_metadata(record): + return {name:value for name,value in _metadata(record).items() if name not in ("struct_size","reserved_flags")} +def _lp_entry(record): + data=_lp_metadata(record);data["active"]=bool(data["active"]) + data["basis"]=LpBasisStatus(data["basis"]) if data.pop("has_basis") else None + if isinstance(record,_LpRow): + data["dual_source"]=LpDualSource(data["dual_source"]) + return LpRowObservation(**data) + return LpColumnObservation(**data) + + +class LpObservedResult(_Owner): + def analyze_sensitivity(self, options=None): + """Analyze this historical optimal basis; no optimization solve is run.""" + start=time.monotonic() + options=LpSensitivityOptions() if options is None else options + if not isinstance(options,LpSensitivityOptions): + raise TypeError("options must be LpSensitivityOptions") + native=options._marshal(self._library) + seconds=native.time_limit_seconds + valid_options=(native.request_count>0 and not math.isnan(seconds) and seconds>=0 and + all(math.isfinite(getattr(native.checks,n)) and getattr(native.checks,n)>=0 + for n,_ in _SensitivityChecksOptions._fields_[2:])) + private_token=None + result=None + try: + # A private registry owner survives explicit close of the caller's + # token after admission, while cancellation propagates both ways. + if options.cancellation is not None: + private_token=options.cancellation.copy() + native.cancellation=private_token._open() + preparation=time.monotonic()-start + if math.isfinite(seconds) and seconds>=0: + native.time_limit_seconds=max(0.0,seconds-preparation) + result=_own_call(self,LpSensitivityResult,"analyze_lp_sensitivity",C.byref(native)) + native=None # release request buffers before the publication check + cancelled=private_token.cancelled if private_token is not None else False + if private_token is not None: + private_token.close();private_token=None + result._python_stop=Termination.CANCELLED if valid_options and cancelled else None + elapsed=time.monotonic()-start + if valid_options and result._python_stop is None and elapsed>=seconds: + result._python_stop=Termination.TIME_LIMIT + result._total_elapsed_seconds=elapsed + return result + except BaseException: + if result is not None:result.close() + raise + finally: + if private_token is not None:private_token.close() + _kind="lp_observed_result" + def __init__(self, library, handle): + self._initialize(library,handle) + @property + def info(self): + out=_record(self,"lp_observed_result_info",_LpInfo);data=_lp_metadata(out) + info=_metadata(out.result);info["termination"]=Termination(info["termination"]);info["guarantee"]=Guarantee(info["guarantee"]) + for name in ("has_solution","solution_validated","start_submitted"): + info[name]=bool(info[name]) + data["result"]=info;data["has_observations"]=bool(data["has_observations"]) + return data + termination=property(lambda self:self.info["result"]["termination"]) + has_solution=property(lambda self:self.info["result"]["has_solution"]) + def copy_result(self): + return _own_call(self,Result,"lp_observed_result_copy_result") + @property + def observations(self): + info=self.info + if not info["has_observations"]: + return None + text=lambda field:_copied_text(self,"lp_observed_result_text",field) + metadata=LpObservationMetadata(**_lp_metadata(_record(self,"lp_observed_result_metadata",_LpMetadata)),backend=text(0),backend_version=text(1)) + def group(index): + data=_lp_metadata(_record(self,"lp_observed_result_group",_LpGroup,index)) + return LpObservationGroup(LpObservationState(data["state"]),LpObservationReason(data["reason"]),text(index+2)) + check=_lp_metadata(_record(self,"lp_observed_result_checks",_LpChecks)) + for name in ("primal_valid","dual_signs_valid","stationarity_valid","complementarity_valid","gap_valid","accepted"): + check[name]=bool(check[name]) + checks=LpKktReport(**check,message=text(5)) + def entries(function,element): + count=U64();self._library.call(function,self._open(),None,C.sizeof(element),0,C.byref(count)) + data=(element*count.value)() + self._library.call(function,self._open(),data,C.sizeof(element),count.value,C.byref(count)) + return tuple(_lp_entry(item) for item in data) + return LpObservations(info["model_id"],info["revision"],metadata,group(0),group(1),group(2),checks, + entries("lp_observed_result_rows",_LpRow),entries("lp_observed_result_columns",_LpColumn),self._library) + + +class LpBasisOrigin(IntEnum): + CALLER=0; OBSERVATIONS=1 +class LpBasisSubmissionState(IntEnum): + NOT_ATTEMPTED=0; ACCEPTED=1; REPAIRED=2; REJECTED=3; INTERRUPTED=4 +@dataclass(frozen=True) +class LpBasisInfo: + model_id: int + revision: int + row_slots: int + column_slots: int + origin: LpBasisOrigin +@dataclass(frozen=True) +class LpBasisSubmission: + state: LpBasisSubmissionState + backend_attempted: bool + statuses_changed: object + message: str + + +def _basis_solve_options(owner,basis,options): + if not isinstance(basis,LpBasis) or basis._library is not owner._library: + raise TypeError("basis must belong to the same ABI instance") + if options is not None and not isinstance(options,LpObservationOptions): + raise TypeError("options must be LpObservationOptions") + return (options or LpObservationOptions())._marshal(owner._library) + + +class LpBasis(_Owner): + """Immutable source-tagged statuses. Use a factory; close releases only this token.""" + _kind="basis" + def __init__(self,library,handle): + self._initialize(library,handle) + @classmethod + def from_model(cls,model,columns,rows): + if not isinstance(model,Model): + raise TypeError("model must be Model") + def statuses(values): + encoded=[-1 if value is None else int(LpBasisStatus(_integer(value,32,True))) for value in values] + return (I32*len(encoded))(*encoded) + columns=statuses(columns);rows=statuses(rows) + return _own_call(model,cls,"basis_from_model",columns,len(columns),rows,len(rows)) + @classmethod + def from_observed(cls,observed): + if not isinstance(observed,LpObservedResult): + raise TypeError("observed must be an owning LpObservedResult") + return _own_call(observed,cls,"basis_from_observed") + @property + def info(self): + data=_lp_metadata(_record(self,"basis_info",_BasisInfo)) + data["origin"]=LpBasisOrigin(data["origin"]) + return LpBasisInfo(**data) + def _statuses(self,kind): + n=U64();self._library.call("basis_statuses",self._open(),kind,None,0,C.byref(n)) + values=(I32*n.value)();self._library.call("basis_statuses",self._open(),kind,values,n.value,C.byref(n)) + return tuple(None if value==-1 else LpBasisStatus(value) for value in values) + rows=property(lambda self:self._statuses(2)) + columns=property(lambda self:self._statuses(1)) + def row(self,row): + value=I32();self._library.call("basis_row",self._open(),_entity(row,self._library,Row),C.byref(value)) + return LpBasisStatus(value.value) + def column(self,variable): + value=I32();self._library.call("basis_column",self._open(),_entity(variable,self._library,Variable),C.byref(value)) + return LpBasisStatus(value.value) + + +class LpBasisSolveResult(_Owner): + """Submission facts and solve termination are independent; copied children own data.""" + _kind="basis_result" + def __init__(self,library,handle): + self._initialize(library,handle) + @property + def info(self): + out=_record(self,"basis_result_info",_BasisResultInfo) + result=_metadata(out.result);result["termination"]=Termination(result["termination"]) + result["guarantee"]=Guarantee(result["guarantee"]) + for name in ("has_solution","solution_validated","start_submitted"): + result[name]=bool(result[name]) + return {"result":result,"requested_model_id":out.requested_model_id if out.has_requested_basis else None, + "requested_revision":out.requested_revision if out.has_requested_basis else None} + termination=property(lambda self:self.info["result"]["termination"]) + has_solution=property(lambda self:self.info["result"]["has_solution"]) + @property + def submission(self): + out=_record(self,"basis_result_info",_BasisResultInfo) + return LpBasisSubmission(LpBasisSubmissionState(out.state),bool(out.backend_attempted), + bool(out.statuses_changed) if out.has_statuses_changed else None, + _copied_text(self,"basis_result_message")) + def copy_observed(self): + return _own_call(self,LpObservedResult,"basis_result_copy_observed") + def copy_basis(self): + return _own_call(self,LpBasis,"basis_result_copy_basis") + + +@dataclass(frozen=True) +class RegularTransition: + """One sparse edge; state indices are zero based and symbols are signed integers.""" + from_state: int + symbol: int + to_state: int + + +class ScenarioReuse(IntEnum): + AUTOMATIC=0; COLD=1 +class ScenarioRunState(IntEnum): + NOT_STARTED=0; ATTEMPTED=1 +class ScenarioBatchCompletion(IntEnum): + REJECTED=0; INTERRUPTED=1; COMPLETE=2 +@dataclass(frozen=True) +class ScenarioId: + batch_id: int + index: int + _library: Library=field(repr=False) + def _id(self): + return _ScenarioId(_integer(self.batch_id,64),_integer(self.index,64)) +@dataclass(frozen=True) +class ScenarioVariableBounds: + variable: Variable + lower: object=None + upper: object=None +@dataclass(frozen=True) +class ScenarioRowBounds: + row: Row + lower: object=None + upper: object=None +@dataclass(frozen=True) +class ScenarioDefinition: + """Absolute patches. None inherits; a zero objective coefficient removes it.""" + name: str="" + objective_coefficients: tuple=() + objective_offset: object=None + variable_bounds: tuple=() + row_bounds: tuple=() + def __post_init__(self): + terms=self.objective_coefficients + if hasattr(terms,"items"): + terms=terms.items() + object.__setattr__(self,"objective_coefficients",tuple((v,c) for v,c in terms)) + object.__setattr__(self,"variable_bounds",tuple(self.variable_bounds)) + object.__setattr__(self,"row_bounds",tuple(self.row_bounds)) +@dataclass +class ScenarioOptions: + solve: Options=field(default_factory=Options) + reuse: ScenarioReuse=ScenarioReuse.AUTOMATIC + max_scenarios: int=1000 + max_patch_entries: int=1000000 + max_saved_value_slots: int=10000000 + max_work: int=100000000 + def _marshal(self,library): + if not isinstance(self.solve,Options): + raise TypeError("solve must be Options") + out=_ScenarioOptions();library.call("scenario_options_default",C.byref(out),C.sizeof(out)) + solve=self.solve._marshal(library);out.solve=solve;out._keepalive=solve + out.reuse=int(ScenarioReuse(_integer(self.reuse,32,True))) + for name in ("max_scenarios","max_patch_entries","max_saved_value_slots","max_work"): + setattr(out,name,_integer(getattr(self,name),64)) + return out +@dataclass(frozen=True) +class ScenarioStatistics: + solve_calls: int + model_loads: int + incremental_updates: int + unchanged_models: int + basis_warm_starts: int + incumbent_starts: int +@dataclass(frozen=True) +class ScenarioBatchInfo: + model_id: int + revision: int + batch_id: object + scenario_count: int + outcome_count: int + completion: ScenarioBatchCompletion + stop_reason: object + offending_scenario: object + all_resolved: bool + attempted: int + resolved: int + work: int + elapsed_seconds: float + reuse_statistics: ScenarioStatistics +@dataclass(frozen=True) +class ScenarioResultInfo: + model_id: int + revision: int + variable_slots: int + termination: Termination + guarantee: Guarantee + has_solution: bool + solution_validated: bool + start_submitted: bool + elapsed_seconds: float +@dataclass(frozen=True) +class ScenarioCheck: + identity_valid: bool + candidate_examined: bool + objective_matches: bool + exact_witness_validated: bool + validation: object +@dataclass(frozen=True) +class ScenarioOutcome: + scenario: ScenarioId + state: ScenarioRunState + result: object + check: object + reuse_delta: ScenarioStatistics + elapsed_seconds: float + + +def _scenario_definitions(definitions,library): + data=[];keep=[] + def bounds(values,expected,entity_type,attribute): + result=[] + for item in values: + if not isinstance(item,expected): + raise TypeError("wrong scenario bound record type") + result.append(_ScenarioBounds(C.sizeof(_ScenarioBounds),0,_entity(getattr(item,attribute),library,entity_type), + int(item.lower is not None),int(item.upper is not None), + 0.0 if item.lower is None else float(item.lower),0.0 if item.upper is None else float(item.upper))) + return (_ScenarioBounds*len(result))(*result) + for definition in definitions: + if not isinstance(definition,ScenarioDefinition): + raise TypeError("definitions must contain ScenarioDefinition records") + name=_text(definition.name);terms=_terms(definition.objective_coefficients,library) + variables=bounds(definition.variable_bounds,ScenarioVariableBounds,Variable,"variable") + rows=bounds(definition.row_bounds,ScenarioRowBounds,Row,"row") + offset=definition.objective_offset + data.append(_ScenarioDefinition(C.sizeof(_ScenarioDefinition),0,name,terms,len(terms), + _OptionalNumber(offset is not None,0,0 if offset is None else float(offset)),variables,len(variables),rows,len(rows))) + keep.extend((name,terms,variables,rows)) + array=(_ScenarioDefinition*len(data))(*data);array._keepalive=keep;return array + + +class ScenarioBatchResult(_Owner): + """Owning history. copy_result() is independent and uses private mapped IDs.""" + _kind="scenario_batch" + def __init__(self,library,handle): + self._initialize(library,handle) + def _id(self,scenario): + if not isinstance(scenario,ScenarioId): + raise TypeError("scenario must be ScenarioId") + if scenario._library is not self._library: + raise ValueError("scenario belongs to another ABI instance") + return scenario._id() + @property + def info(self): + out=_record(self,"scenario_batch_info",_ScenarioInfo) + return ScenarioBatchInfo(out.model_id,out.revision,out.batch_id if out.has_batch else None, + out.scenario_count,out.outcome_count,ScenarioBatchCompletion(out.completion), + Termination(out.stop_reason) if out.has_stop_reason else None, + out.offending_scenario if out.has_offending_scenario else None,bool(out.all_resolved),out.attempted,out.resolved, + out.work,out.elapsed_seconds,ScenarioStatistics(**_metadata(out.reuse_statistics))) + message=property(lambda self:_copied_text(self,"scenario_batch_message")) + def scenario(self,index): + out=_ScenarioId();self._library.call("scenario_batch_id",self._open(),_integer(index,64),C.byref(out)) + return ScenarioId(out.batch_id,out.index,self._library) + def check(self,scenario): + id=self._id(scenario);out=_record(self,"scenario_batch_check",_ScenarioCheck,id) + if not out.has_check: + return None + validation=None + if out.candidate_examined: + data=_metadata(out.validation);data["valid"]=bool(data["valid"]);data["model_valid"]=bool(data["model_valid"]) + validation=Validation(**data,message=_copied_text(self,"scenario_batch_text",id,1)) + return ScenarioCheck(bool(out.identity_valid),bool(out.candidate_examined),bool(out.objective_matches), + bool(out.exact_witness_validated),validation) + def outcome(self,scenario): + out=_record(self,"scenario_batch_outcome",_ScenarioOutcome,self._id(scenario));info=None + if out.has_result: + data=_metadata(out.result);data["termination"]=Termination(data["termination"]);data["guarantee"]=Guarantee(data["guarantee"]) + for name in ("has_solution","solution_validated","start_submitted"): + data[name]=bool(data[name]) + info=ScenarioResultInfo(**data) + return ScenarioOutcome(ScenarioId(out.scenario.batch_id,out.scenario.index,self._library),ScenarioRunState(out.state),info, + self.check(scenario),ScenarioStatistics(**_metadata(out.reuse_delta)),out.elapsed_seconds) + def copy_result(self,scenario): + return _own_call(self,Result,"scenario_batch_copy_result",self._id(scenario)) + def map(self,original): + out=_Id();self._library.call("scenario_batch_map",self._open(),_entity(original,self._library,(Variable,Row)),C.byref(out)) + cls=Variable if out.kind==1 else Row + return cls(out.model_id,out.slot,self._library) + def value(self,scenario,original): + out=F64();self._library.call("scenario_batch_value",self._open(),self._id(scenario),_entity(original,self._library,Variable),C.byref(out)) + return out.value + def definition(self,scenario): + id=self._id(scenario);out=_record(self,"scenario_batch_definition",_ScenarioDefinitionInfo,id) + terms=_array(self,"scenario_batch_objective",_Term,id) + def bounds(kind,cls,entity_cls): + count=U64();self._library.call("scenario_batch_bounds",self._open(),id,kind,None,C.sizeof(_ScenarioBounds),0,C.byref(count)) + values=(_ScenarioBounds*count.value)() + self._library.call("scenario_batch_bounds",self._open(),id,kind,values,C.sizeof(_ScenarioBounds),count.value,C.byref(count)) + return tuple(cls(entity_cls(v.entity.model_id,v.entity.slot,self._library),v.lower if v.has_lower else None, + v.upper if v.has_upper else None) for v in values) + return ScenarioDefinition(_copied_text(self,"scenario_batch_text",id,0), + tuple((Variable(t.variable.model_id,t.variable.slot,self._library),t.coefficient) for t in terms), + out.objective_offset.value if out.objective_offset.present else None, + bounds(1,ScenarioVariableBounds,Variable),bounds(2,ScenarioRowBounds,Row)) + + +class LpEvidenceRequest(IntEnum): + AUTOMATIC=0; PRIMAL_RAY=1; FARKAS=2; BOTH=3 +class LpEvidenceState(IntEnum): + NOT_REQUESTED=0; AVAILABLE=1; UNAVAILABLE=2; REJECTED=3 +class LpEvidenceReason(IntEnum): + NONE=0; NOT_REQUESTED=1; UNSUPPORTED=2; NO_FEASIBLE_BASE=3; NO_IMPROVEMENT=4; NO_CONTRADICTION=5 + STOPPED=6; INVALID_BACKEND=7; FAILED_CHECKS=8; INCONSISTENT=9; INVALID_MODEL=10; RESOURCE_LIMIT=11; ALLOCATION=12 +class LpEvidenceCompletion(IntEnum): + COMPLETE=0; INTERRUPTED=1; REJECTED=2 +class LpEvidencePhase(IntEnum): + FEASIBLE_BASE=0; RECESSION=1; FARKAS=2 +class LpEvidenceSide(IntEnum): + LOWER=0; UPPER=1 +class LpEvidenceColumnKind(IntEnum): + SOURCE_VARIABLE=0; ROW_SIDE=1; VARIABLE_SIDE=2 +@dataclass +class LpEvidenceOptions: + solve: Options=field(default_factory=Options) + request: LpEvidenceRequest=LpEvidenceRequest.AUTOMATIC + recession: float=1e-7 + stationarity: float=1e-7 + minimum_improvement: float=1e-7 + minimum_contradiction: float=1e-7 + max_auxiliary_variables: int=1000000 + max_auxiliary_rows: int=1000000 + max_auxiliary_nonzeros: int=10000000 + max_retained_slots: int=50000000 + max_work: int=100000000 + max_auxiliary_solves: int=3 + def _marshal(self,library): + if not isinstance(self.solve,Options): + raise TypeError("solve must be Options") + out=_EvidenceOptions();library.call("evidence_options_default",C.byref(out),C.sizeof(out)) + solve=self.solve._marshal(library);out.solve=solve;out._keepalive=solve + out.request=int(LpEvidenceRequest(_integer(self.request,32,True))) + for name in ("recession","stationarity","minimum_improvement","minimum_contradiction"): + setattr(out,name,float(getattr(self,name))) + for name in ("max_auxiliary_variables","max_auxiliary_rows","max_auxiliary_nonzeros","max_retained_slots","max_work","max_auxiliary_solves"): + setattr(out,name,_integer(getattr(self,name),64)) + return out +@dataclass(frozen=True) +class LpEvidenceInfo: + model_id: int + revision: int + has_evidence: bool + completion: LpEvidenceCompletion + stop_reason: object + row_slots: int + column_slots: int + stage_count: int + attempted_calls: int + work: int + elapsed_seconds: float +@dataclass(frozen=True) +class LpEvidenceGroup: + state: LpEvidenceState + reason: LpEvidenceReason + message: str +@dataclass(frozen=True) +class LpEvidenceMetadata: + recession: float + stationarity: float + minimum_improvement: float + minimum_contradiction: float + primal_tolerance: float +@dataclass(frozen=True) +class LpPrimalEvidence: + base_check: object + direction_scale: object + normalized_objective_slope: object + max_variable_recession_violation: object + max_row_recession_violation: object +@dataclass(frozen=True) +class LpFarkasEvidence: + multiplier_scale: object + contradiction_margin: object + max_stationarity: object +@dataclass(frozen=True) +class LpEvidenceSlot: + """Original-slot diagnostics; consult group state for accepted evidence.""" + source: object + active: bool + side: object + base_value: object + direction: object + multiplier: object + contribution: object + selected_bound: object +@dataclass(frozen=True) +class LpEvidenceDiagnostics: + metadata: LpEvidenceMetadata + primal: LpPrimalEvidence + farkas: LpFarkasEvidence + columns: tuple + rows: tuple +@dataclass(frozen=True) +class LpEvidenceRawResult: + """Untrusted reported auxiliary fields; deliberately no has_solution API.""" + model_id: int + revision: int + value_count: int + mask_count: int + termination_code: int + guarantee_code: int + reported_solution_validated: bool + reported_start_submitted: bool + elapsed_seconds: float + objective: object + best_bound: object + absolute_gap: object + relative_gap: object + native_gap: object +@dataclass(frozen=True) +class LpEvidenceStageInfo: + index: int + private_model_id: int + private_revision: int + row_count: int + column_count: int + nonzeros: int + phase: LpEvidencePhase + attempted: bool + candidate_examined: bool + check: object + raw_result: object +@dataclass(frozen=True) +class LpEvidenceColumn: + private_variable: Variable + source: object + kind: LpEvidenceColumnKind + side: object +@dataclass(frozen=True) +class LpEvidenceRawValue: + slot: int + reported_value: object + reported_mask: object + + +def _evidence_validation(record,message): + data=_metadata(record);data["valid"]=bool(data["valid"]);data["model_valid"]=bool(data["model_valid"]) + return Validation(**data,message=message) + +def _evidence_source(record,library): + if record.reserved or record.kind not in (1,2): + raise RuntimeError("invalid evidence source ID") + return (Variable if record.kind==1 else Row)(record.model_id,record.slot,library) + +def _evidence_slot_record(record,library): + data=_lp_metadata(record);data["source"]=_evidence_source(record.source,library);data["active"]=bool(data["active"]) + data["side"]=LpEvidenceSide(data["side"]) if data.pop("has_side") else None + return LpEvidenceSlot(**data) + +def _evidence_array(owner,function,element,*args): + count=U64();owner._library.call(function,owner._open(),*args,None,C.sizeof(element),0,C.byref(count)) + out=(element*count.value)();owner._library.call(function,owner._open(),*args,out,C.sizeof(element),count.value,C.byref(count)) + return out + + +class LpEvidenceResult(_Owner): + """Owning numerical evidence analysis; not an original optimization Result.""" + _kind="lp_evidence" + def __init__(self,library,handle): + self._initialize(library,handle) + @property + def info(self): + out=_record(self,"lp_evidence_info",_EvidenceInfo);data=_lp_metadata(out) + data["has_evidence"]=bool(data["has_evidence"]);data["completion"]=LpEvidenceCompletion(data["completion"]) + data["stop_reason"]=Termination(data["stop_reason"]) if data.pop("has_stop_reason") else None + return LpEvidenceInfo(**data) + message=property(lambda self:_copied_text(self,"lp_evidence_text",0)) + def _group(self,index): + if not self.info.has_evidence: + return None + out=_record(self,"lp_evidence_group",_EvidenceGroup,index) + return LpEvidenceGroup(LpEvidenceState(out.state),LpEvidenceReason(out.reason),_copied_text(self,"lp_evidence_text",index+1)) + primal_ray=property(lambda self:self._group(0)) + farkas=property(lambda self:self._group(1)) + @property + def diagnostics(self): + if not self.info.has_evidence: + return None + metadata=LpEvidenceMetadata(**_lp_metadata(_record(self,"lp_evidence_metadata",_EvidenceMetadata))) + p=_record(self,"lp_evidence_primal",_EvidencePrimal);data=_lp_metadata(p) + data["base_check"]=_evidence_validation(p.base_check,_copied_text(self,"lp_evidence_text",3)) if data.pop("has_base_check") else None + primal=LpPrimalEvidence(**data);farkas=LpFarkasEvidence(**_lp_metadata(_record(self,"lp_evidence_farkas",_EvidenceFarkas))) + slots=lambda kind:tuple(_evidence_slot_record(r,self._library) for r in _evidence_array(self,"lp_evidence_slots",_EvidenceSlot,kind)) + return LpEvidenceDiagnostics(metadata,primal,farkas,slots(1),slots(2)) + def slot(self,source): + out=_record(self,"lp_evidence_slot",_EvidenceSlot,_entity(source,self._library,(Variable,Row))) + return _evidence_slot_record(out,self._library) + def _value(self,variable,field): + out=F64();self._library.call("lp_evidence_value",self._open(),_entity(variable,self._library,Variable),field,C.byref(out));return out.value + def base_value(self,variable): + """Requires Available primal-ray group; diagnostics retain base-only data.""" + return self._value(variable,0) + def direction_value(self,variable): + return self._value(variable,1) + def multiplier(self,source): + """Requires Available Farkas group; slot() provides raw diagnostics.""" + return _evidence_slot_record(_record(self,"lp_evidence_multiplier",_EvidenceSlot,_entity(source,self._library,(Variable,Row))),self._library) + def copy_stage(self,index): + return _own_call(self,LpEvidenceStage,"lp_evidence_copy_stage",_integer(index,64)) + + +class LpEvidenceStage(_Owner): + """Independent owning child: raw auxiliary diagnostics, never a Result.""" + _kind="lp_evidence_stage" + def __init__(self,library,handle): + self._initialize(library,handle) + @property + def info(self): + out=_record(self,"lp_evidence_stage_info",_EvidenceStage);data=_lp_metadata(out) + data["phase"]=LpEvidencePhase(data["phase"]);data["attempted"]=bool(data["attempted"]) + data["candidate_examined"]=bool(data["candidate_examined"]) + data["check"]=_evidence_validation(out.check,_copied_text(self,"lp_evidence_stage_text",3)) if out.candidate_examined else None + if data.pop("has_raw_result"): + raw=_lp_metadata(out.raw_result) + raw["reported_solution_validated"]=bool(raw["reported_solution_validated"]) + raw["reported_start_submitted"]=bool(raw["reported_start_submitted"]) + data["raw_result"]=LpEvidenceRawResult(**raw) + else:data["raw_result"]=None + return LpEvidenceStageInfo(**data) + @property + def columns(self): + out=[] + for r in _evidence_array(self,"lp_evidence_stage_columns",_EvidenceColumn): + out.append(LpEvidenceColumn(_evidence_source(r.private_variable,self._library),_evidence_source(r.source,self._library), + LpEvidenceColumnKind(r.kind),LpEvidenceSide(r.side) if r.has_side else None)) + return tuple(out) + @property + def raw_values(self): + return tuple(LpEvidenceRawValue(r.slot,r.reported_value.value if r.reported_value.present else None, + r.reported_mask if r.has_reported_mask else None) for r in _evidence_array(self,"lp_evidence_stage_raw_values",_EvidenceRawValue)) + raw_backend=property(lambda self:_copied_text(self,"lp_evidence_stage_text",0)) + raw_backend_version=property(lambda self:_copied_text(self,"lp_evidence_stage_text",1)) + raw_message=property(lambda self:_copied_text(self,"lp_evidence_stage_text",2)) + + +class LpSensitivityState(IntEnum): + NOT_REQUESTED=0; AVAILABLE=1; UNAVAILABLE=2; REJECTED=3 +class LpSensitivityReason(IntEnum): + NONE=0; NOT_REQUESTED=1; UNSUPPORTED=2; NOT_OPTIMAL=3; NO_BASIS=4; INVALID_SOURCE=5 + INVALID_BASIS=6; CHANGED_BASIS=7; REFERENCE_CHECKS=8; SYSTEM_CHECKS=9; INTERVAL_CHECKS=10 + RESOURCE_LIMIT=11; STOPPED=12; ALLOCATION=13; BACKEND=14 +class LpSensitivityCompletion(IntEnum): + COMPLETE=0; PARTIAL=1; INTERRUPTED=2; REJECTED=3 +class LpRangeEndKind(IntEnum): + FINITE=0; NEGATIVE_INFINITY=1; POSITIVE_INFINITY=2 +class LpSensitivitySide(IntEnum): + LOWER=0; UPPER=1; FIXED=2; FREE=3 +@dataclass(frozen=True) +class LpObjectiveParameter: + variable: Variable +@dataclass(frozen=True) +class LpEqualityRhsParameter: + row: Row +@dataclass(frozen=True) +class LpSensitivityTolerances: + primal_feasibility: float=1e-7 + dual_feasibility: float=1e-7 + stationarity: float=1e-7 + complementarity: float=1e-6 + objective_gap: float=1e-6 + system_absolute: float=1e-9 + system_relative: float=1e-9 +@dataclass(frozen=True) +class LpSensitivityLimits: + max_rows: int=4096 + max_columns: int=100000 + max_nonzeros: int=1000000 + max_requests: int=4096 + max_basis_solves: int=8194 + max_factor_entries: int=16777216 + max_retained_slots: int=20000000 + max_work: int=100000000 +@dataclass(frozen=True) +class LpSensitivityOptions: + parameters: tuple=() + backend: Backend=Backend.AUTO + time_limit_seconds: float=math.inf + cancellation: object=None + checks: LpSensitivityTolerances=field(default_factory=LpSensitivityTolerances) + limits: LpSensitivityLimits=field(default_factory=LpSensitivityLimits) + def _marshal(self,library): + if not isinstance(self.parameters,(tuple,list)): + raise TypeError("parameters must be a tuple or list") + if not isinstance(self.checks,LpSensitivityTolerances) or not isinstance(self.limits,LpSensitivityLimits): + raise TypeError("sensitivity checks/limits have the wrong type") + out=_SensitivityOptions();library.call("sensitivity_options_default",C.byref(out),C.sizeof(out)) + out.backend=int(Backend(_integer(self.backend,32,True)));out.time_limit_seconds=float(self.time_limit_seconds) + if self.cancellation is not None: + if not isinstance(self.cancellation,Cancellation) or self.cancellation._library is not library: + raise TypeError("cancellation belongs to another library or has the wrong type") + out.cancellation=self.cancellation._open() + for name,_ in _SensitivityChecksOptions._fields_[2:]: + setattr(out.checks,name,float(getattr(self.checks,name))) + for name,_ in _SensitivityLimits._fields_[2:]: + setattr(out.limits,name,_integer(getattr(self.limits,name),64)) + n=_integer(len(self.parameters),64);out.request_count=n + if n>out.limits.max_requests or n>out.limits.max_work: + # The v1 C admission gate checks these counts before dereferencing + # any element. A bounded sentinel allows an owning quota rejection + # without allocating a buffer already known to exceed the quota. + entries=(_SensitivityRequest*1)() + else: + entries=(_SensitivityRequest*n)() + for i,p in enumerate(self.parameters): + entries[i].struct_size=C.sizeof(_SensitivityRequest) + if isinstance(p,LpObjectiveParameter): + entries[i].kind=0;entries[i].entity=_entity(p.variable,library,Variable) + elif isinstance(p,LpEqualityRhsParameter): + entries[i].kind=1;entries[i].entity=_entity(p.row,library,Row) + else:raise TypeError("unknown sensitivity parameter descriptor") + out.requests=entries;out._keepalive=(entries,self.cancellation) + return out +@dataclass(frozen=True) +class LpSensitivityInfo: + model_id: int + revision: int + completion: LpSensitivityCompletion + reason: LpSensitivityReason + stop_reason: object + has_sensitivity: bool + has_basis: bool + guarantee: Guarantee + entry_count: int + factor_order_count: int + row_slots: int + column_slots: int + elapsed_seconds: float +@dataclass(frozen=True) +class LpSensitivityWork: + factor_setup_attempted: bool + basis_solves: int + coordinator_visits: int + retained_slots: int + preparation_visits: int +@dataclass(frozen=True) +class LpSensitivityGroup: + state: LpSensitivityState + reason: LpSensitivityReason + message: str +@dataclass(frozen=True) +class LpRangeEnd: + kind: LpRangeEndKind + value: object + def __post_init__(self): + if not isinstance(self.kind,LpRangeEndKind):raise TypeError("endpoint kind must be LpRangeEndKind") + if self.kind is LpRangeEndKind.FINITE: + if self.value is None or not math.isfinite(self.value):raise ValueError("finite endpoint requires a finite value") + elif self.value is not None:raise ValueError("infinite endpoint has no numeric value") +@dataclass(frozen=True) +class LpSensitivityLimiter: + entity: object + side: LpSensitivitySide + dual_condition: bool +@dataclass(frozen=True) +class LpIntervalCheckReport: + accepted: bool + inequalities: int + max_endpoint_violation: object + lower_direction_checked: bool + upper_direction_checked: bool + message: str +@dataclass(frozen=True) +class LpParameterInterval: + anchor: float + lower: LpRangeEnd + upper: LpRangeEnd + objective_slope: object + lower_limiter: object + upper_limiter: object + checks: LpIntervalCheckReport +@dataclass(frozen=True) +class LpSensitivityEntry: + index: int + parameter: object + group: LpSensitivityGroup + interval: object +@dataclass(frozen=True) +class LpSensitivityReferenceChecks: + """Copied diagnostics; default false flags do not prove a check ran.""" + primal: Validation + kkt: LpKktReport + basis_point_matches: bool + max_point_difference: object + max_system_residual: object + max_scaled_system_residual: object + + +class LpSensitivityResult(_Owner): + """Owning fixed-basis numerical ranges, distinct from the source solve.""" + _kind="sensitivity" + def __init__(self,library,handle): + self._initialize(library,handle) + def _text(self,field,index=0): + return _copied_text(self,"sensitivity_text",field,index) + @property + def info(self): + out=_record(self,"sensitivity_info",_SensitivityInfo);data=_lp_metadata(out) + data["completion"]=LpSensitivityCompletion(data["completion"]);data["reason"]=LpSensitivityReason(data["reason"]) + data["stop_reason"]=Termination(data["stop_reason"]) if data.pop("has_stop_reason") else None + data["has_sensitivity"]=bool(data["has_sensitivity"]);data["has_basis"]=bool(data["has_basis"]) + data["guarantee"]=Guarantee(data["guarantee"]) + if getattr(self,"_python_stop",None) is not None: + data.update(completion=LpSensitivityCompletion.INTERRUPTED,reason=LpSensitivityReason.STOPPED,stop_reason=self._python_stop) + data["elapsed_seconds"]=getattr(self,"_total_elapsed_seconds",data["elapsed_seconds"]) + return LpSensitivityInfo(**data) + @property + def message(self): + self._open() + if getattr(self,"_python_stop",None) is not None:return "Whole Python sensitivity allowance stopped during input cleanup" + return self._text(0) + @property + def work(self): + data=_lp_metadata(_record(self,"sensitivity_work",_SensitivityWork));data["factor_setup_attempted"]=bool(data["factor_setup_attempted"]) + return LpSensitivityWork(**data) + def copy_source_observed(self): + return _own_call(self,LpObservedResult,"sensitivity_copy_source_observed") + def copy_basis(self): + return _own_call(self,LpBasis,"sensitivity_copy_basis") + @property + def checks_options(self): + if not self.info.has_sensitivity:return None + return LpSensitivityTolerances(**_lp_metadata(_record(self,"sensitivity_checks_options",_SensitivityChecksOptions))) + @property + def reference_checks(self): + if not self.info.has_sensitivity:return None + r=_record(self,"sensitivity_reference_checks",_SensitivityReferenceChecks) + primal=_evidence_validation(r.primal,self._text(4));kkt=_lp_metadata(r.kkt) + for name in ("primal_valid","dual_signs_valid","stationarity_valid","complementarity_valid","gap_valid","accepted"): + kkt[name]=bool(kkt[name]) + return LpSensitivityReferenceChecks(primal,LpKktReport(**kkt,message=self._text(5)),bool(r.basis_point_matches), + *[getattr(r,n).value if getattr(r,n).present else None for n in + ("max_point_difference","max_system_residual","max_scaled_system_residual")]) + def _entry(self,r): + if not r.requested:return None + entity=_evidence_source(r.request.entity,self._library) + parameter=LpObjectiveParameter(entity) if r.request.kind==0 else LpEqualityRhsParameter(entity) + if getattr(self,"_python_stop",None) is not None: + return LpSensitivityEntry(r.index,parameter,LpSensitivityGroup(LpSensitivityState.UNAVAILABLE, + LpSensitivityReason.STOPPED,self.message),None) + group=LpSensitivityGroup(LpSensitivityState(r.group.state),LpSensitivityReason(r.group.reason),self._text(2,r.index)) + interval=None + if r.has_interval: + end=lambda e:LpRangeEnd(LpRangeEndKind(e.kind),e.value.value if e.value.present else None) + limiter=lambda l:LpSensitivityLimiter(_evidence_source(l.entity,self._library),LpSensitivitySide(l.side),bool(l.dual_condition)) + c=r.checks;checks=LpIntervalCheckReport(bool(c.accepted),c.inequalities, + c.max_endpoint_violation.value if c.max_endpoint_violation.present else None, + bool(c.lower_direction_checked),bool(c.upper_direction_checked),self._text(3,r.index)) + interval=LpParameterInterval(r.anchor,end(r.lower),end(r.upper),r.objective_slope.value if r.objective_slope.present else None, + limiter(r.lower_limiter) if r.has_lower_limiter else None,limiter(r.upper_limiter) if r.has_upper_limiter else None,checks) + return LpSensitivityEntry(r.index,parameter,group,interval) + def entry(self,index): + return self._entry(_record(self,"sensitivity_entry",_SensitivityEntry,_integer(index,64))) + @property + def entries(self): + if not self.info.has_sensitivity:return None + return tuple(self._entry(r) for r in _evidence_array(self,"sensitivity_entries",_SensitivityEntry)) + def objective(self,variable): + return self._entry(_record(self,"sensitivity_objective",_SensitivityEntry,_entity(variable,self._library,Variable))) + def equality_rhs(self,row): + return self._entry(_record(self,"sensitivity_equality_rhs",_SensitivityEntry,_entity(row,self._library,Row))) + @property + def factor_order(self): + if not self.info.has_sensitivity:return None + return tuple(_evidence_source(r,self._library) for r in _array(self,"sensitivity_factor_order",_Id)) + def active_slots(self,entity_type): + if entity_type not in (Variable,Row):raise TypeError("entity_type must be Variable or Row") + return tuple(bool(v) for v in _array(self,"sensitivity_active_slots",U8,1 if entity_type is Variable else 2)) + @property + def backend_version(self): + return self._text(1) if self.info.has_sensitivity else None diff --git a/python/tests/test_bulk.py b/python/tests/test_bulk.py new file mode 100644 index 0000000000..a44dafd583 --- /dev/null +++ b/python/tests/test_bulk.py @@ -0,0 +1,176 @@ +"""Atomic bulk C ABI/Python conformance; no optional numerical dependencies.""" +import ctypes as C +import gc +import math +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from gecode_optimize import (ApiError, Backend, Library, Model, Options, RowSpec, + SparseRowBatch, Termination, VariableSpec, VariableType, load_library) +from gecode_optimize import binding as B + + +class BulkConformance(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.lib=load_library() + cls.highs=cls.lib.capabilities(Backend.HIGHS)["available"] + + def check_solution(self, result, optimum): + self.assertEqual(result.termination, Termination.OPTIMAL if self.highs else Termination.UNSUPPORTED, result.message) + if self.highs: + self.assertAlmostEqual(result.objective,optimum) + + def test_scalar_equivalence_names_and_history(self): + specs=[VariableSpec(VariableType.INTEGER,-3,7,"production Ï€"), + VariableSpec(upper=10,name="recourse 😀")] + with Model(self.lib) as bulk, Model(self.lib) as scalar, tempfile.TemporaryDirectory() as directory: + dead=bulk.add_variable();bulk.remove(dead) + before=bulk.identity + variables=bulk.add_variables(specs) + self.assertEqual(bulk.identity,(before[0],before[1]+1)) + self.assertEqual([v.slot for v in variables],[1,2]) + sx=tuple(scalar.add_variable(s.type,s.lower,s.upper,s.name) for s in specs) + # Reversed mapping; coefficients +1e16+1-1e16 coalesce to exactly 1. + batch=SparseRowBatch([variables[1],variables[0]],[0,4,5],[1,1,1,0,0], + [1e16,1,-1e16,1,1],[2.5,0],[math.inf,4],["demand λ",""]) + before=bulk.identity + rows=bulk.add_rows_sparse(batch) + self.assertEqual(bulk.identity,(before[0],before[1]+1)) + scalar.add_row({sx[0]:1,sx[1]:1},lower=2.5,name="demand λ") + scalar.add_row({sx[1]:1},lower=0,upper=4) + bulk.set_objective({variables[0]:2,variables[1]:3},offset=.25) + scalar.set_objective({sx[0]:2,sx[1]:3},offset=.25) + # Generated file comments retain exact UTF-8 names, independent of + # input Python objects and temporary C-string/term arrays. + del specs,batch;gc.collect() + a=Path(directory)/'a.lp';b=Path(directory)/'b.lp';bulk.write(a);scalar.write(b) + self.assertEqual(a.read_bytes(),b.read_bytes()) + with bulk.solve(Options(backend=Backend.HIGHS)) as historical, scalar.solve(Options(backend=Backend.HIGHS)) as cold: + self.check_solution(historical,5.75);self.check_solution(cold,5.75) + identity=bulk.identity + more=bulk.add_rows([RowSpec({variables[0]:1},lower=4,name="edit")]) + self.assertEqual(bulk.identity,(identity[0],identity[1]+1)) + self.assertEqual(more[0].slot,rows[-1].slot+1) + with bulk.solve(Options(backend=Backend.HIGHS)) as current: + self.check_solution(current,8.25) + new=bulk.add_variables([VariableSpec(upper=1)]) + bulk.close() + self.assertEqual((historical.info['model_id'],historical.info['revision']),identity) + if self.highs: + self.assertEqual(historical.value(variables[0]),2) + with self.assertRaises(ApiError):historical.value(new[0]) + with self.assertRaises(ApiError):historical.value(dead) + + def test_all_types_row_sequence_and_single_c_call(self): + specs=[VariableSpec(t,lo,hi) for t,lo,hi in [ + (VariableType.CONTINUOUS,0,3),(VariableType.INTEGER,0,3), + (VariableType.BINARY,0,None),(VariableType.SEMI_CONTINUOUS,2,5), + (VariableType.SEMI_INTEGER,2.5,5)]] + with Model(self.lib) as m: + calls=[];original=self.lib.call + def count_calls(name,*args): + calls.append(name);return original(name,*args) + with patch.object(self.lib,'call',count_calls): + x=m.add_variables(specs) + self.assertEqual(calls,['model_add_variables']) + calls.clear();before=m.identity + with patch.object(self.lib,'call',count_calls): + rows=m.add_rows([RowSpec([(v,1)],lower=demand) for v,demand in zip(x,[1.5,1.5,.5,1,1])]) + self.assertEqual(calls,['model_add_rows']);self.assertEqual(m.identity[1],before[1]+1) + m.set_objective([(v,1) for v in x]) + with m.solve(Options(backend=Backend.HIGHS)) as result:self.check_solution(result,9.5) + calls.clear() + with patch.object(self.lib,'call',count_calls): + m.add_rows_sparse(SparseRowBatch(x,[0,5],list(range(5)),[1]*5,[0],[math.inf])) + self.assertEqual(calls,['model_add_rows_sparse']) + self.assertEqual(len(rows),5) + + def test_late_failures_are_atomic(self): + with Model(self.lib) as m, Model(self.lib) as foreign: + x=m.add_variable(upper=5);dead=m.add_variable();m.remove(dead);other=foreign.add_variable() + before=m.identity + for specs in ([VariableSpec(),VariableSpec(lower=math.nan)], + [VariableSpec(),VariableSpec(name='bad\0name')], + [VariableSpec(),VariableSpec(name='\ud800')], + [VariableSpec(),VariableSpec(type=999)], + [VariableSpec(),object()]): + with self.subTest(specs=specs),self.assertRaises((ApiError,ValueError,TypeError)): + m.add_variables(specs) + self.assertEqual(m.identity,before) + for bad in (RowSpec({other:1}),RowSpec({dead:1}),RowSpec({x:math.inf}), + RowSpec({x:1},lower=2,upper=1),RowSpec(name='\0')): + with self.subTest(bad=bad),self.assertRaises((ApiError,ValueError)): + m.add_rows([RowSpec({x:1}),bad]) + self.assertEqual(m.identity,before) + fresh=m.add_variables([VariableSpec()])[0] + self.assertEqual(fresh.slot,dead.slot+1) + # Wrong library instances are rejected in Python before entering C. + alias=Library(self.lib.path) + with Model(alias) as other_model: + alien=other_model.add_variable() + before=m.identity + with self.assertRaises(ValueError):m.add_rows([RowSpec({x:1}),RowSpec({alien:1})]) + with self.assertRaises(ValueError):m.add_rows_sparse(SparseRowBatch([alien])) + self.assertEqual(m.identity,before) + + def test_csr_malformed_dimensions_indices_and_unused_columns(self): + with Model(self.lib) as m, Model(self.lib) as foreign: + x=m.add_variable();dead=m.add_variable();m.remove(dead);other=foreign.add_variable() + bad=[SparseRowBatch([x],[],[],[],[],[]), + SparseRowBatch([x],[0,1],[0],[1],[0],[]), + SparseRowBatch([x],[0,1],[0],[],[0],[1]), + SparseRowBatch([x],[0,1],[0],[1],[0],[1],["a","b"]), + SparseRowBatch([x],[1,1],[0],[1],[0],[1]), + SparseRowBatch([x],[0,2,1],[0],[1],[0,0],[1,1]), + SparseRowBatch([x],[0,1],[1],[1],[0],[1]), + SparseRowBatch([x],[0,1],[0],[math.nan],[0],[1]), + SparseRowBatch([x,x]),SparseRowBatch([dead]),SparseRowBatch([other])] + before=m.identity + for batch in bad: + with self.subTest(batch=batch),self.assertRaises(ApiError):m.add_rows_sparse(batch) + self.assertEqual(m.identity,before) + for value in (-1,2**64,True,1.0): + with self.subTest(value=value),self.assertRaises((OverflowError,TypeError)): + m.add_rows_sparse(SparseRowBatch([x],[0,1],[value],[1],[0],[1])) + self.assertEqual(m.identity,before) + for batch in (SparseRowBatch(),SparseRowBatch([x])): + self.assertEqual(m.add_rows_sparse(batch),()) + self.assertEqual(m.add_variables([]),());self.assertEqual(m.add_rows([]),()) + self.assertEqual(m.identity,before) + m.close() + with self.assertRaises(RuntimeError):m.add_variables([]) + with self.assertRaises(RuntimeError):m.add_rows([]) + with self.assertRaises(RuntimeError):m.add_rows_sparse(SparseRowBatch()) + + def test_sparse_many_nonzeros_and_constant_contradiction(self): + # Large enough to catch scalar dispatch but small enough for ordinary CI. + with Model(self.lib) as m: + x=m.add_variables([VariableSpec(upper=2)]*1024) + calls=[];original=self.lib.call + def record(name,*args):calls.append(name);return original(name,*args) + with patch.object(self.lib,'call',record): + rows=m.add_rows_sparse(SparseRowBatch(x,list(range(1025)),list(range(1024)), + [1]*1024,[1]*1024,[2]*1024)) + self.assertEqual(calls,['model_add_rows_sparse']);self.assertEqual(len(rows),1024) + # Empty contradictory row must not disappear during batch conversion. + m.add_rows([RowSpec(lower=1)]) + with m.solve(Options(backend=Backend.HIGHS)) as result: + self.assertEqual(result.termination,Termination.INFEASIBLE if self.highs else Termination.UNSUPPORTED) + + def test_c_name_copy_and_output_preflight(self): + with Model(self.lib) as m, tempfile.TemporaryDirectory() as directory: + name=C.create_string_buffer('original Ï€'.encode()) + spec=B._VariableSpec(C.sizeof(B._VariableSpec),int(VariableType.CONTINUOUS),0,0,1,C.cast(name,C.c_char_p)) + before=m.identity;out=(B._Id*1)();out[0].slot=123 + for capacity,output,error in ((0,out,6),(0,None,6),(1,None,1)): + with self.assertRaises(ApiError) as failure: + self.lib.call('model_add_variables',m._open(),C.byref(spec),1,output,capacity) + self.assertEqual(failure.exception.code,error);self.assertEqual(out[0].slot,123) + self.assertEqual(m.identity,before) + self.lib.call('model_add_variables',m._open(),C.byref(spec),1,out,1) + name.value=b'changed';del name,spec;gc.collect() + file=Path(directory)/'names.lp';m.write(file) + self.assertIn('original Ï€'.encode().hex(),file.read_text()) diff --git a/python/tests/test_conformance.py b/python/tests/test_conformance.py new file mode 100644 index 0000000000..a271e6c912 --- /dev/null +++ b/python/tests/test_conformance.py @@ -0,0 +1,438 @@ +"""Run with PYTHONPATH=python GECODE_OPTIMIZE_LIBRARY=/path/to/library python3 -m unittest discover -s python/tests -v.""" +import concurrent.futures +import math +import tempfile +import threading +import unittest +from pathlib import Path +from unittest.mock import patch + +from gecode_optimize import (ApiError, Backend, Cancellation, GlobalConstraint, Guarantee, Indicator, Model, + Options, Session, Termination, VariableType, load_library) + + +class Conformance(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.lib = load_library() + cls.highs = cls.lib.capabilities(Backend.HIGHS)["available"] + cls.native = cls.lib.capabilities(Backend.NATIVE)["available"] + + def assert_solution(self, result, objective, values=()): + if not self.highs: + self.assertEqual(result.termination, Termination.UNSUPPORTED) + self.assertFalse(result.has_solution) + self.assertIsNone(result.objective) + self.assertIsNone(result.best_bound) + self.assertTrue(result.message) + return + self.assertEqual(result.termination, Termination.OPTIMAL, result.message) + self.assertTrue(result.info["solution_validated"]) + self.assertAlmostEqual(result.objective, objective, places=6) + for variable, expected in values: + self.assertAlmostEqual(result.value(variable), expected, places=6) + + def test_lp_continuous_recourse_and_result_lifetime(self): + # Analytic optimum: x + y >= 2.5, x integer, y >= 0, + # min 2x + 3y + 0.25 => x=2, y=0.5, objective=5.75. + with Model(self.lib) as model: + x = model.add_variable(VariableType.INTEGER, upper=5) + y = model.add_variable(upper=5) + dead = model.add_variable(VariableType.BINARY) + model.remove(dead) + model.add_row({x: 1, y: 1}, lower=2.5) + model.set_objective({x: 2, y: 3}, offset=.25) + result = model.solve(Options(backend=Backend.HIGHS)) + identity = model.identity + self.assertTrue(model.closed) + with result: + self.assert_solution(result, 5.75, [(x, 2), (y, .5)]) + self.assertEqual((result.info["model_id"], result.info["revision"]), identity) + self.assertEqual(result.backend, "HiGHS") + records = result.values + self.assertEqual(records[2], {"active": False, "present": False, "value": None}) + self.assertEqual(records[0]["present"], self.highs) + if self.highs: + self.assertTrue(result.backend_version) + with self.assertRaises(ApiError): + result.value(dead) + else: + with self.assertRaises(ApiError) as error: + result.value(x) + self.assertEqual(error.exception.code, 7) + result.close() # sequential close is idempotent + with self.assertRaises(RuntimeError): + result.info + + def test_lp_fractional_and_maximum_offset(self): + with Model(self.lib) as model: + x = model.add_variable(upper=10) + model.add_row({x: 2}, lower=3, upper=5) + model.set_objective({x: 4}, maximize=True, offset=-7) + with model.solve(Options(backend=Backend.HIGHS)) as result: + self.assert_solution(result, 3, [(x, 2.5)]) + + def test_domains_and_semi_zero_exception(self): + # Independent one-variable optima exercise every public type. + cases = [(VariableType.CONTINUOUS, 0, 3, 1.5, 1.5), + (VariableType.INTEGER, 0, 3, 1.5, 2), + (VariableType.BINARY, 0, 1, .5, 1), + (VariableType.SEMI_CONTINUOUS, 2, 5, 1, 2), + (VariableType.SEMI_INTEGER, 2.5, 5, 1, 3), + (VariableType.SEMI_CONTINUOUS, 2, 5, 0, 0), + (VariableType.SEMI_INTEGER, 2.5, 5, 0, 0)] + for kind, lower, upper, demand, optimum in cases: + with self.subTest(kind=kind, demand=demand), Model(self.lib) as model: + x = model.add_variable(kind, lower, upper) + model.add_row({x: 1}, lower=demand) + model.set_objective({x: 1}) + with model.solve(Options(backend=Backend.HIGHS)) as result: + self.assert_solution(result, optimum, [(x, optimum)]) + + def test_session_edits_and_snapshot_history(self): + with Model(self.lib) as model, Session(self.lib) as session: + x = model.add_variable(VariableType.INTEGER, upper=8) + row = model.add_row({x: 1}, lower=2) + model.set_objective({x: 1}, offset=3) + first = session.solve(model, Options(backend=Backend.HIGHS)) + with first: + self.assert_solution(first, 5, [(x, 2)]) + model.set_bounds(row, 3, math.inf) + model.set_bounds(x, 3, 8) + model.set_objective_coefficient(x, 2) + model.set_objective_offset(-1) + model.set_name(x, "quantity") + model.set_name(row, "demand") + with session.solve(model, Options(backend=Backend.HIGHS)) as result: + self.assert_solution(result, 5, [(x, 3)]) + if self.highs: + self.assertEqual(result.info["revision"], model.identity[1]) + self.assertLess(first.info["revision"], result.info["revision"]) + self.assertEqual(first.value(x), 2) + self.assertEqual(session.statistics["model_loads"], 1) + self.assertEqual(session.statistics["incremental_updates"], 1) + model.set_coefficient(row, x, 2) + with session.solve(model, Options(backend=Backend.HIGHS)) as result: + self.assert_solution(result, 5, [(x, 3)]) + if self.highs: + self.assertEqual(session.statistics["model_loads"], 2) + session.reset() + with session.solve(model, Options(backend=Backend.HIGHS)) as result: + self.assert_solution(result, 5) + model.remove(row) + model.set_objective() + model.remove(x) + with model.solve(Options(backend=Backend.HIGHS)) as result: + self.assert_solution(result, 0) + + def test_api_errors_and_safe_integer_conversion(self): + with Model(self.lib) as model, Model(self.lib) as foreign: + x = model.add_variable(upper=3) + y = foreign.add_variable() + with self.assertRaises(ApiError): + model.add_row({y: 1}) + row = model.add_row({x: 1}, lower=1) + with self.assertRaises(TypeError): + model.set_coefficient(x, row, 1) + with self.assertRaises(ApiError): + model.remove(x) + with self.assertRaises(ApiError): + model.set_bounds(x, math.nan, 1) + with self.assertRaises(ApiError): + model.set_objective({x: math.inf}) + with self.assertRaises(ValueError): + model.add_variable(name="bad\0name") + with self.assertRaises(ValueError): + model.add_variable(type=99) + for options, error in [(Options(node_limit=-1), OverflowError), + (Options(node_limit=2**64), OverflowError), + (Options(threads=2**31), OverflowError), + (Options(random_seed=.5), TypeError), + (Options(threads=True), TypeError), + (Options(time_limit_seconds=math.nan), ApiError), + (Options(integrality_tolerance=.5), ApiError), + (Options(primal_start={x: math.inf}), ApiError)]: + with self.subTest(options=options), self.assertRaises(error): + model.solve(options) + model.remove(row) + model.remove(x) + with self.assertRaises(ApiError): + model.set_bounds(x, 0, 1) + with self.assertRaises(RuntimeError): + model.add_variable() + + def test_starts_cancellation_options_and_guarantees(self): + with Model(self.lib) as model, Session(self.lib) as session, Cancellation(self.lib) as token: + x = model.add_variable(VariableType.INTEGER, upper=8) + model.set_objective({x: 1}, maximize=True) + with model.solve(Options(backend=Backend.HIGHS, primal_start={x: 6})) as result: + self.assert_solution(result, 8) + self.assertEqual(result.info["start_submitted"], self.highs) + token.cancel() + with session.solve(model, Options(backend=Backend.HIGHS, cancellation=token)) as result: + self.assertEqual(result.termination, Termination.CANCELLED) + self.assertFalse(result.has_solution) + self.assertIsNone(result.objective) + with session.solve(model, Options(backend=Backend.HIGHS)) as result: + self.assert_solution(result, 8) + with model.solve(Options(backend=Backend.HIGHS, guarantee=Guarantee.CERTIFIED)) as result: + self.assertEqual(result.termination, Termination.UNSUPPORTED) + with model.solve(Options(backend=Backend.HIGHS, time_limit_seconds=0)) as result: + self.assertEqual(result.termination, Termination.TIME_LIMIT) + token.close() + with self.assertRaises(RuntimeError): + model.solve(Options(cancellation=token)) + + def test_infeasible_unbounded_and_explicit_native(self): + with Model(self.lib) as model: + x = model.add_variable(upper=1) + model.add_row({x: 1}, lower=2) + with model.solve(Options(backend=Backend.HIGHS)) as result: + self.assertEqual(result.termination, Termination.INFEASIBLE if self.highs else Termination.UNSUPPORTED) + self.assertIsNone(result.objective) + with Model(self.lib) as model: + x = model.add_variable() + model.set_objective({x: -1}) + with model.solve(Options(backend=Backend.HIGHS)) as result: + expected = (Termination.UNBOUNDED, Termination.INFEASIBLE_OR_UNBOUNDED) if self.highs else (Termination.UNSUPPORTED,) + self.assertIn(result.termination, expected) + with Model(self.lib) as model: + x = model.add_variable(VariableType.INTEGER, upper=4) + model.set_objective({x: 1}, maximize=True, offset=-1) + with model.solve(Options(backend=Backend.NATIVE, guarantee=Guarantee.EXACT)) as result: + native = self.lib.capabilities(Backend.NATIVE)["available"] + self.assertEqual(result.termination, Termination.OPTIMAL if native else Termination.UNSUPPORTED) + if native: + self.assertEqual(result.objective, 3) + self.assertEqual(result.value(x), 4) + + def test_read_prepares_owner_before_allocating_c_model(self): + class AllocationFailure(Model): + def __new__(cls): + raise MemoryError("owner allocation failed") + + class InitializationFailure(Model): + def _initialize(self, library, handle=None): + raise MemoryError("owner initialization failed") + + for owner in (AllocationFailure, InitializationFailure): + with self.subTest(owner=owner.__name__): + with patch.object(self.lib, "call", wraps=self.lib.call) as call: + with self.assertRaises(MemoryError): + owner.read("not-read.lp", self.lib) + call.assert_not_called() + + def test_io_roundtrip_and_failure_preserves_destination(self): + with tempfile.TemporaryDirectory() as directory, Model(self.lib) as model: + x = model.add_variable(VariableType.INTEGER, upper=8, name="quantity") + model.add_row({x: 2}, lower=3, upper=9, name="demand") + model.set_objective({x: 3}, maximize=True, offset=-1.25) + for extension in ("lp", "mps"): + path = Path(directory) / ("model." + extension) + model.write(path) + with Model.read(path, self.lib) as imported, imported.solve(Options(backend=Backend.HIGHS)) as result: + self.assertNotEqual(imported.identity[0], model.identity[0]) + self.assert_solution(result, 10.75) + if self.highs: + with self.assertRaises(ApiError): + result.value(x) + path.write_text("preserve me", encoding="utf8") + with self.assertRaises(ApiError): + Model.read(path, self.lib) + self.assertEqual(path.read_text(), "preserve me") + bad = Path(directory) / "model.unsupported" + bad.write_text("unchanged", encoding="utf8") + with self.assertRaises(ApiError): + model.write(bad) + self.assertEqual(bad.read_text(), "unchanged") + + def test_global_builders_auto_routing_and_sessions(self): + with Model(self.lib) as model: + x = model.add_variable(VariableType.INTEGER, 0, 2) + y = model.add_variable(VariableType.INTEGER, 0, 2) + distinct = model.add_all_different([x, y], name="distinct") + self.assertIsInstance(distinct, GlobalConstraint) + table = model.add_table([x, y], [(0, 1), (2, 0)]) + # Index/result/array aliases retain their exact meaning: x=2 is + # outside this element's indices, so the only solution is (0,1). + model.add_element(x, [x, y], x) + model.add_cumulative([x, x], [0, 1], [100, 0], capacity=0) + successors = [model.add_variable(VariableType.INTEGER, v, v) for v in [2, 3, 1]] + model.add_circuit(successors, index_base=1) + model.set_objective({x: 1, y: 2}) + model.set_name(distinct, "renamed") + for backend in (Backend.AUTO, Backend.NATIVE): + with model.solve(Options(backend=backend)) as result: + self.assertEqual(result.termination, Termination.OPTIMAL if self.native else Termination.UNSUPPORTED) + if self.native: + self.assertEqual(result.objective, 2) + self.assertEqual((result.value(x), result.value(y)), (0, 1)) + with model.solve(Options(backend=Backend.HIGHS)) as result: + self.assertEqual(result.termination, Termination.UNSUPPORTED) + # The persistent numerical session keeps its declared route. + with Session(self.lib) as session: + with session.solve(model) as result: + self.assertEqual(result.termination, Termination.UNSUPPORTED) + with session.solve(model, Options(backend=Backend.NATIVE)) as result: + # A persistent numerical session never changes backend; + # explicit Native is available on Model.solve instead. + self.assertEqual(result.termination, Termination.UNSUPPORTED) + with self.assertRaises(ApiError): + model.remove(x) + with self.assertRaises(TypeError): + model.set_bounds(distinct, 0, 1) + with tempfile.TemporaryDirectory() as directory: + with self.assertRaises(ApiError): + model.write(Path(directory) / "globals.lp") + model.remove(table) + with self.assertRaises(ApiError): + model.remove(table) + with self.assertRaises(ApiError): + model.set_name(table, "deleted") + + def test_global_aliases_bases_and_zero_cases(self): + def check(model, feasible): + with model.solve(Options(backend=Backend.NATIVE)) as result: + expected = Termination.OPTIMAL if feasible else Termination.INFEASIBLE + self.assertEqual(result.termination, expected if self.native else Termination.UNSUPPORTED) + with Model(self.lib) as model: + x = model.add_variable(VariableType.INTEGER, 0, 2) + model.add_all_different([x, x]) + check(model, False) + with Model(self.lib) as model: + x = model.add_variable(VariableType.INTEGER, 0, 2) + model.add_table([x, x], [(0, 1)]) + check(model, False) + with Model(self.lib) as model: + index = model.add_variable(VariableType.INTEGER, -2, -2) + model.add_element(index, [index], index, index_base=-2) + check(model, True) + for tuples, feasible in [([], False), ([()], True)]: + with Model(self.lib) as model: + model.add_table([], tuples) + model.add_all_different([]) + model.add_cumulative([], [], [], 0) + check(model, feasible) + with Model(self.lib) as model: + x = model.add_variable(VariableType.INTEGER, 1, 2) + model.add_circuit([x, x], index_base=1) + check(model, False) + + def test_global_errors_are_atomic(self): + with Model(self.lib) as model, Model(self.lib) as foreign: + x = model.add_variable(VariableType.INTEGER, 0, 2) + y = foreign.add_variable(VariableType.INTEGER, 0, 2) + continuous = model.add_variable(upper=2) + dead = model.add_variable(VariableType.INTEGER, 0, 2) + model.remove(dead) + before = model.identity + failures = [ + (lambda: model.add_all_different([y]), ApiError), + (lambda: model.add_all_different([dead]), ApiError), + (lambda: model.add_all_different([continuous]), ApiError), + (lambda: model.add_table([x], [(1, 2)]), ValueError), + (lambda: model.add_table([x], [(2**63,)]), OverflowError), + (lambda: model.add_table([x], [(2**53+1,)]), ApiError), + (lambda: model.add_table([x], [(1.0,)]), TypeError), + (lambda: model.add_element(x, [x], x, index_base=2**63), OverflowError), + (lambda: model.add_element(x, [x], x, index_base=True), TypeError), + (lambda: model.add_cumulative([x], [], [1], 1), ApiError), + (lambda: model.add_cumulative([x], [-1], [1], 1), ApiError), + (lambda: model.add_cumulative([x], [1], [1], -1), ApiError), + (lambda: model.add_circuit([]), ApiError), + (lambda: model.add_circuit([x], index_base=2**53+1), ApiError), + ] + for action, error in failures: + with self.assertRaises(error): + action() + self.assertEqual(model.identity, before) + global_id = model.add_all_different([x]) + with self.assertRaises(ApiError): + foreign.remove(global_id) + model.remove(global_id) + model.remove(x) + + def test_indicator_guards_removal_and_boolean_semantics(self): + for active in (False, True): + with Model(self.lib) as model: + b = model.add_variable(VariableType.BINARY, int(active), int(active)) + x = model.add_variable(VariableType.INTEGER, 0, 4) + model.set_objective({x: 1}) + before = model.identity + for action, error in [ + (lambda: model.add_indicator(b, 1, {x: 1}, lower=3), TypeError), + (lambda: model.add_indicator(x, active, {x: 1}, lower=3), ApiError), + (lambda: model.add_indicator(b, active, {x: math.inf}, lower=3), ApiError), + (lambda: model.add_boolean_and(b, [x]), ApiError), + ]: + with self.assertRaises(error): + action() + self.assertEqual(model.identity, before) + indicator = model.add_indicator(b, active, {x: 1}, lower=3) + self.assertIsInstance(indicator, Indicator) + self.assertIsNotNone(indicator.inactive_gate) + with self.assertRaises(ApiError): + model.set_bounds(x, 0, 5) + with model.solve(Options(backend=Backend.HIGHS)) as result: + self.assert_solution(result, 3, [(x, 3)]) + with tempfile.TemporaryDirectory() as directory: + with self.assertRaises(ApiError): + model.write(Path(directory) / "indicator.lp") + model.remove(indicator) + with self.assertRaises(ApiError): + model.remove(indicator) + model.remove(indicator.inactive_gate) + with model.solve(Options(backend=Backend.HIGHS)) as result: + self.assert_solution(result, 0, [(x, 0)]) + tautology = model.add_indicator(b, active) + self.assertIsNone(tautology.inactive_gate) + model.remove(tautology) + for left in (0, 1): + for right in (0, 1): + with Model(self.lib) as model: + a = model.add_variable(VariableType.BINARY, left, left) + b = model.add_variable(VariableType.BINARY, right, right) + conjunction = model.add_variable(VariableType.BINARY) + disjunction = model.add_variable(VariableType.BINARY) + true = model.add_variable(VariableType.BINARY) + false = model.add_variable(VariableType.BINARY) + self.assertIsNone(model.add_boolean_and(conjunction, [a, b, a])) + model.add_boolean_or(disjunction, [a, b, b]) + model.add_boolean_and(true) + model.add_boolean_or(false) + model.add_boolean_and(a, [a, a]) # aliased output/input + with model.solve(Options(backend=Backend.HIGHS)) as result: + self.assert_solution(result, 0, [(conjunction, left & right), + (disjunction, left | right), + (true, 1), (false, 0)]) + + def test_thread_local_errors_and_independent_solves(self): + barrier = threading.Barrier(2) + def error_worker(which): + if which == 0: + code = self.lib._functions["model_destroy"](0) + else: + code = self.lib._functions["model_create"](None) + barrier.wait(timeout=5) + text = self.lib._dll.gecode_opt_v1_last_error().decode() + return code, text + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + a, b = list(executor.map(error_worker, range(2))) + self.assertEqual(a[0], 2) + self.assertIn("handle", a[1]) + self.assertEqual(b[0], 1) + self.assertIn("pointer", b[1]) + def solve_worker(lower): + with Model(self.lib) as model, Session(self.lib) as session: + x = model.add_variable(VariableType.INTEGER, lower, 20) + model.set_objective({x: 1}) + with session.solve(model, Options(backend=Backend.HIGHS)) as result: + return result.termination, result.objective + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + results = list(executor.map(solve_worker, [2, 7])) + expected = [(Termination.OPTIMAL, 2), (Termination.OPTIMAL, 7)] if self.highs else [(Termination.UNSUPPORTED, None)] * 2 + self.assertEqual(results, expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/python/tests/test_lp_basis.py b/python/tests/test_lp_basis.py new file mode 100644 index 0000000000..7167d9d359 --- /dev/null +++ b/python/tests/test_lp_basis.py @@ -0,0 +1,141 @@ +"""Owning LP basis factories and explicit submission semantics across the ABI.""" +import ctypes as C +from dataclasses import FrozenInstanceError +import math +import unittest +from gecode_optimize import (ApiError, Backend, Cancellation, Guarantee, Library, Model, + Options, Row, Session, Termination as T, VariableType, load_library, LpObservationOptions as LO, + LpBasis, LpBasisOrigin as Origin, LpBasisStatus as B, LpBasisSubmissionState as S) +from gecode_optimize import binding as b + +class LpBasisTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.library=load_library();cls.available=cls.library.lp_observation_capabilities().available + def model(self): + m=Model(self.library);x=m.add_variable();y=m.add_variable();dead=m.add_variable();m.remove(dead) + deleted=m.add_row();m.remove(deleted);row=m.add_row([(x,1),(y,1)],lower=4) + m.set_objective([(x,2),(y,3)],offset=7) + return m,x,y,row,dead,deleted + def basis(self,m):return LpBasis.from_model(m,[B.BASIC,B.LOWER,None],[None,B.LOWER]) + def test_factories_copies_and_history(self): + m,x,y,row,dead,deleted=self.model() + with m,Session(self.library) as session,self.basis(m) as basis: + self.assertEqual(basis.info.origin,Origin.CALLER) + self.assertEqual(basis.columns,(B.BASIC,B.LOWER,None));self.assertEqual(basis.rows,(None,B.LOWER)) + self.assertEqual(basis.column(x),B.BASIC);self.assertEqual(basis.row(row),B.LOWER) + with self.assertRaises(FrozenInstanceError):basis.info.origin=Origin.OBSERVATIONS + for bad in (deleted,Row(row.model_id+1,row.slot,self.library)): + with self.assertRaises(ApiError):basis.row(bad) + with self.assertRaises(ApiError):basis.column(dead) + with self.assertRaises(TypeError):basis.row(x) + with session.solve_lp_with_basis(m,basis) as solved: + self.assertEqual(solved.termination,T.OPTIMAL if self.available else T.UNSUPPORTED) + self.assertEqual(solved.submission.state,S.ACCEPTED if self.available else S.NOT_ATTEMPTED) + self.assertEqual(solved.submission.statuses_changed,False if self.available else None) + copied=solved.copy_basis();observed=solved.copy_observed();ordinary=observed.copy_result() + if self.available: + with LpBasis.from_observed(observed) as exported: + self.assertEqual(exported.info.origin,Origin.OBSERVATIONS) + with m.solve_lp_with_basis(exported) as second: + self.assertEqual(second.termination,T.OPTIMAL) + else: + with self.assertRaises(ApiError):LpBasis.from_observed(observed) + revision=copied.info.revision;m.set_bounds(row,5,math.inf) + with session.solve_lp_with_basis(m,copied) as stale: + self.assertEqual(stale.termination,T.INVALID_MODEL);self.assertFalse(stale.submission.backend_attempted) + self.assertEqual(stale.info['requested_revision'],revision) + with stale.copy_basis() as retained:self.assertEqual(retained.info.revision,revision) + with session.solve_lp_observed(m) as fresh: + if self.available: + with fresh.copy_result() as result:self.assertEqual(result.objective,17) + with copied,observed,ordinary: + self.assertEqual(copied.column(x),B.BASIC) + self.assertEqual(copied.info.revision,revision) + if self.available:self.assertEqual(ordinary.objective,15);self.assertEqual(ordinary.value(x),4) + with self.assertRaises(RuntimeError):copied.column(x) + def test_source_library_and_options(self): + m,*_=self.model();other,*_=self.model() + with m,other,self.basis(m) as basis,Session(self.library) as session: + with other.solve_lp_with_basis(basis) as foreign:self.assertEqual(foreign.termination,T.INVALID_MODEL) + for solve in (Options(backend=Backend.NATIVE),Options(guarantee=Guarantee.EXACT),Options(guarantee=Guarantee.CERTIFIED)): + with m.solve_lp_with_basis(basis,LO(solve=solve)) as out: + self.assertEqual(out.termination,T.UNSUPPORTED);self.assertFalse(out.submission.backend_attempted) + for invalid in (Options(),object()): + with self.assertRaises(TypeError):m.solve_lp_with_basis(basis,invalid) + with self.assertRaises(TypeError):m.solve_lp_with_basis(object()) + another=Library(self.library.path) + with Model(another) as alien: + with self.assertRaises(TypeError):alien.solve_lp_with_basis(basis) + with self.assertRaises(TypeError):session.solve_lp_with_basis(alien,basis) + with self.assertRaises(TypeError):LpBasis.from_observed(object()) + def test_counts_statuses_and_c_records(self): + m,x,*_=self.model() + with m: + for columns,rows in (([],[]),([None,B.LOWER,None],[None,B.BASIC]),([B.BASIC,B.LOWER,B.LOWER],[None,B.LOWER]),([B.LOWER,B.LOWER,None],[None,B.LOWER])): + with self.assertRaises(ApiError):LpBasis.from_model(m,columns,rows) + for invalid in (True,0.5,object()): + with self.assertRaises(TypeError):LpBasis.from_model(m,[invalid],[]) + for invalid in (-1,99): + with self.assertRaises(ValueError):LpBasis.from_model(m,[invalid],[]) + with self.basis(m) as basis: + for mutate in (lambda o:setattr(o,'struct_size',C.sizeof(o)-1),lambda o:setattr(o,'reserved',1), + lambda o:setattr(o,'basis',2),lambda o:setattr(o.solve,'reserved',1)): + options=LO()._marshal(self.library);mutate(options);out=b.U64(99) + with self.assertRaises(ApiError) as error:self.library.call('solve_lp_with_basis',m._open(),basis._open(),C.byref(options),C.byref(out)) + self.assertEqual(error.exception.code,1);self.assertEqual(out.value,0) + with self.assertRaises(ApiError):m.solve_lp_with_basis(basis,LO(solve=Options(primal_start=[(x,4)]))) + out=b._BasisInfo() + with self.assertRaises(ApiError):self.library.call('basis_info',basis._open(),C.byref(out),C.sizeof(out)-1) + values=(b.I32*2)(77,88);needed=b.U64() + with self.assertRaises(ApiError) as error:self.library.call('basis_statuses',basis._open(),1,values,2,C.byref(needed)) + self.assertEqual(error.exception.code,6);self.assertEqual(tuple(values),(77,88));self.assertEqual(needed.value,3) + with self.assertRaises(ApiError):self.library.call('basis_statuses',basis._open(),0,None,0,C.byref(needed)) + def test_cancellation_limits_and_reset(self): + m,*_=self.model() + with m,self.basis(m) as basis,Session(self.library) as session,Cancellation(self.library) as cancel: + cancel.cancel() + for options,expected in ((Options(time_limit_seconds=0),T.TIME_LIMIT),(Options(cancellation=cancel),T.CANCELLED)): + with session.solve_lp_with_basis(m,basis,LO(solve=options)) as out: + self.assertEqual(out.termination,expected if self.available else T.UNSUPPORTED) + self.assertFalse(out.submission.backend_attempted) + with out.copy_observed() as observed:self.assertFalse(observed.has_solution) + with session.solve_lp_with_basis(m,basis) as out:self.assertEqual(out.termination,T.OPTIMAL if self.available else T.UNSUPPORTED) + def test_singular_repair_and_final_basis_distinction(self): + with Model(self.library) as m: + x=m.add_variable();y=m.add_variable();m.add_row([(x,1),(y,1)],1,1);m.add_row([(x,2),(y,2)],2,2) + m.set_objective([(x,1),(y,2)]) + with LpBasis.from_model(m,[B.BASIC,B.BASIC],[B.LOWER,B.LOWER]) as basis,m.solve_lp_with_basis(basis) as out: + self.assertEqual(out.submission.state,S.REPAIRED if self.available else S.NOT_ATTEMPTED) + self.assertEqual(out.submission.statuses_changed,True if self.available else None) + if self.available: + with out.copy_observed() as observed,observed.copy_result() as ordinary:self.assertEqual(ordinary.objective,1) + m,*_=self.model() + with m,LpBasis.from_model(m,[B.LOWER,B.LOWER,None],[None,B.BASIC]) as logical,m.solve_lp_with_basis(logical) as out: + if self.available: + self.assertEqual(out.submission.state,S.ACCEPTED);self.assertFalse(out.submission.statuses_changed) + with out.copy_observed() as observed:self.assertEqual(observed.observations.columns[0].basis,B.BASIC) + def test_requested_basis_survives_unrequested_export(self): + m,*_=self.model() + with m,self.basis(m) as basis,m.solve_lp_with_basis(basis,LO(basis=False,duals=False)) as out: + with out.copy_basis() as copy:self.assertEqual(copy.columns,basis.columns) + with out.copy_observed() as observed: + with self.assertRaises(ApiError) as error:LpBasis.from_observed(observed) + self.assertEqual(error.exception.code,3) + with observed.copy_result() as result: + self.assertEqual(result.termination,T.OPTIMAL if self.available else T.UNSUPPORTED) + + def test_empty_and_zero_row_max(self): + with Model(self.library) as m,LpBasis.from_model(m,[],[]) as basis,m.solve_lp_with_basis(basis) as out: + self.assertEqual(basis.columns,());self.assertEqual(basis.rows,());self.assertFalse(out.submission.backend_attempted) + self.assertEqual(out.termination,T.OPTIMAL if self.available else T.UNSUPPORTED) + with Model(self.library) as m: + x=m.add_variable(lower=0,upper=5);m.set_objective([(x,2)],maximize=True,offset=7) + with LpBasis.from_model(m,[B.UPPER],[]) as basis,m.solve_lp_with_basis(basis) as out: + if self.available: + with out.copy_observed() as observed,observed.copy_result() as ordinary:self.assertEqual(ordinary.objective,17) + with Model(self.library) as m: + m.add_variable(VariableType.INTEGER,0,1) + with self.assertRaises(ApiError):LpBasis.from_model(m,[B.LOWER],[]) + +if __name__=='__main__':unittest.main() diff --git a/python/tests/test_lp_evidence.py b/python/tests/test_lp_evidence.py new file mode 100644 index 0000000000..946e5954e2 --- /dev/null +++ b/python/tests/test_lp_evidence.py @@ -0,0 +1,196 @@ +"""Numerical LP evidence boundaries, private raw stages, and historical ownership.""" +import ctypes as C +from dataclasses import FrozenInstanceError +import math +import unittest +from gecode_optimize import (ApiError, Backend, Cancellation, Guarantee, Library, Model, + Options, Row, Session, Termination as T, Variable, VariableType as V, load_library, + LpEvidenceOptions as EO, LpEvidenceRequest as Request, LpEvidenceState as State, + LpEvidenceReason as Reason, LpEvidenceCompletion as Completion, LpEvidencePhase as Phase, + LpEvidenceSide as Side, LpEvidenceColumnKind as Kind) +from gecode_optimize import binding as b + +class LpEvidenceTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.library=load_library();cls.available=cls.library.capabilities(Backend.HIGHS)['available'] + def ray_model(self): + m=Model(self.library);dead=m.add_variable();m.remove(dead) + x=m.add_variable();y=m.add_variable(lower=-math.inf) + gone=m.add_row();m.remove(gone);row=m.add_row([(x,1),(y,-1)],0,0) + m.set_objective([(x,-1)],offset=7) + return m,x,y,row,dead,gone + def test_ray_groups_and_owning_raw_stage(self): + m,x,y,row,dead,gone=self.ray_model();identity=m.identity + with m,m.analyze_lp_evidence(EO(request=Request.BOTH)) as result: + info=result.info;self.assertTrue(info.has_evidence);self.assertEqual((info.model_id,info.revision),identity) + self.assertEqual(info.stage_count,3);self.assertEqual((info.column_slots,info.row_slots),(3,2)) + self.assertEqual(m.identity,identity) + diag=result.diagnostics;self.assertEqual(len(diag.columns),3);self.assertFalse(diag.columns[0].active) + self.assertIsNone(diag.columns[0].base_value);self.assertIsNone(diag.columns[0].direction) + self.assertFalse(diag.rows[0].active);self.assertEqual(diag.columns[x.slot].source,x) + with self.assertRaises(FrozenInstanceError):info.work=0 + with self.assertRaises(FrozenInstanceError):diag.columns[0].direction=1 + if self.available: + self.assertEqual(info.completion,Completion.COMPLETE);self.assertIsNone(info.stop_reason);self.assertEqual(info.attempted_calls,3) + self.assertEqual(result.primal_ray.state,State.AVAILABLE);self.assertEqual(result.farkas.state,State.UNAVAILABLE) + self.assertEqual((result.direction_value(x),result.direction_value(y)),(1,1));self.assertEqual(result.base_value(x),0) + self.assertEqual(diag.primal.normalized_objective_slope,-1);self.assertTrue(diag.primal.base_check.valid) + self.assertEqual(result.slot(row).direction,0) + else: + self.assertEqual(info.stop_reason,T.UNSUPPORTED);self.assertEqual(info.attempted_calls,0) + self.assertEqual(result.primal_ray.state,State.REJECTED);self.assertIsNone(diag.primal.base_check) + self.assertIsNone(diag.primal.normalized_objective_slope) + with self.assertRaises(ApiError):result.direction_value(x) + for invalid in (dead,gone,Variable(x.model_id+1,x.slot,self.library)): + with self.assertRaises(ApiError):result.slot(invalid) + with self.assertRaises(TypeError):result.base_value(row) + with self.assertRaises(ApiError):result.multiplier(row) + child=result.copy_stage(1);stage=child.info;columns=child.columns;values=child.raw_values + self.assertEqual(stage.phase,Phase.RECESSION);self.assertNotEqual(stage.private_model_id,identity[0]);self.assertEqual(stage.column_count,2) + self.assertEqual(columns[0].source,x);self.assertEqual(columns[1].source,y) + self.assertEqual(columns[0].kind,Kind.SOURCE_VARIABLE);self.assertIsNone(columns[0].side) + self.assertEqual(columns[0].private_variable.model_id,stage.private_model_id) + self.assertFalse(hasattr(child,'has_solution'));self.assertFalse(hasattr(child,'copy_result')) + if self.available: + self.assertTrue(stage.attempted);self.assertTrue(stage.candidate_examined);self.assertTrue(stage.check.valid) + self.assertTrue(stage.raw_result.reported_solution_validated);self.assertEqual(stage.raw_result.termination_code,int(T.OPTIMAL)) + self.assertEqual(stage.raw_result.objective,-1);self.assertEqual(stage.raw_result.model_id,stage.private_model_id) + self.assertEqual(values[0].reported_value,1);self.assertEqual(values[0].reported_mask,1) + else: + self.assertFalse(stage.attempted);self.assertIsNone(stage.raw_result);self.assertIsNone(stage.check);self.assertEqual(values,()) + m.set_objective([(y,1)]);m.remove(row);m.remove(x) + self.assertEqual(result.slot(x),diag.columns[x.slot]) + with child: + self.assertEqual(child.info,stage);self.assertEqual(child.columns,columns);self.assertEqual(child.raw_values,values) + self.assertEqual(child.raw_backend,'HiGHS' if self.available else '') + self.assertEqual(diag.metadata.primal_tolerance,1e-7) + with self.assertRaises(RuntimeError):child.info + with self.assertRaises(RuntimeError):result.info + def test_requests_and_public_solve_counts(self): + for request,count in ((Request.AUTOMATIC,2),(Request.PRIMAL_RAY,2),(Request.FARKAS,1),(Request.BOTH,3)): + with Model(self.library) as m: + x=m.add_variable();m.set_objective([(x,-1)]) + with m.analyze_lp_evidence(EO(request=request)) as result: + self.assertEqual(result.info.attempted_calls,count if self.available else 0) + if request==Request.FARKAS:self.assertEqual(result.primal_ray.state,State.NOT_REQUESTED) + if request==Request.PRIMAL_RAY:self.assertEqual(result.farkas.state,State.NOT_REQUESTED) + if request==Request.AUTOMATIC: + with result.copy_stage(2) as unused: + self.assertFalse(unused.info.attempted);self.assertIsNone(unused.info.raw_result) + with Model(self.library) as empty,empty.analyze_lp_evidence(EO(request=Request.BOTH)) as result: + # Includes local empty/constant solve decisions, not raw vendor runs. + self.assertEqual(result.info.attempted_calls,3 if self.available else 0) + if self.available: + self.assertEqual(result.primal_ray.state,State.UNAVAILABLE);self.assertEqual(result.farkas.state,State.UNAVAILABLE) + self.assertEqual(result.diagnostics.columns,());self.assertTrue(result.diagnostics.primal.base_check.valid) + def test_farkas_signs_sides_and_auxiliary_coordinates(self): + for upper in (False,True): + for maximize in (False,True): + with Model(self.library) as m: + x=m.add_variable(lower=0 if upper else -math.inf,upper=math.inf if upper else 0) + row=m.add_row([(x,1)],lower=-math.inf if upper else 1,upper=-1 if upper else math.inf) + m.set_objective([(x,3)],maximize=maximize,offset=1e16) + with m.analyze_lp_evidence(EO(request=Request.FARKAS)) as result: + self.assertEqual(result.primal_ray.state,State.NOT_REQUESTED) + if self.available: + self.assertEqual(result.farkas.state,State.AVAILABLE) + yr=result.multiplier(row);zc=result.multiplier(x) + self.assertEqual(yr.multiplier,-1 if upper else 1);self.assertEqual(zc.multiplier,1 if upper else -1) + self.assertEqual(yr.side,Side.UPPER if upper else Side.LOWER) + self.assertEqual(zc.side,Side.LOWER if upper else Side.UPPER) + self.assertEqual(yr.contribution+zc.contribution,1);self.assertEqual(zc.selected_bound,0) + self.assertEqual(result.diagnostics.farkas.contradiction_margin,1) + else:self.assertIsNone(result.diagnostics.farkas.contradiction_margin) + with result.copy_stage(0) as stage: + maps=stage.columns;self.assertEqual(len(maps),2) + self.assertEqual(maps[0].source,row);self.assertEqual(maps[0].kind,Kind.ROW_SIDE) + self.assertEqual(maps[1].source,x);self.assertEqual(maps[1].kind,Kind.VARIABLE_SIDE) + if self.available:self.assertAlmostEqual(stage.info.raw_result.objective,0.5) + with Model(self.library) as m: + x=m.add_variable(lower=-math.inf);lower=m.add_row([(x,1)],lower=1);upper=m.add_row([(x,1)],upper=0) + with m.analyze_lp_evidence(EO(request=Request.FARKAS)) as result: + if self.available: + zero=result.multiplier(x);self.assertEqual(zero.multiplier,0);self.assertIsNone(zero.side);self.assertIsNone(zero.selected_bound) + self.assertEqual(zero.contribution,0);self.assertEqual(result.multiplier(lower).multiplier,1);self.assertEqual(result.multiplier(upper).multiplier,-1) + with Model(self.library) as m: + row=m.add_row(upper=-2) + with m.analyze_lp_evidence() as result: + if self.available:self.assertEqual(result.multiplier(row).multiplier,-1);self.assertEqual(result.diagnostics.farkas.contradiction_margin,2) + self.assertEqual(result.diagnostics.columns,()) + def test_direction_sense_offset_and_unavailable_bounded(self): + for upper in (False,True): + for maximize in (False,True): + with Model(self.library) as m: + x=m.add_variable(lower=-math.inf if upper else 0,upper=0 if upper else math.inf) + coefficient=(1 if upper else -1)*(-1 if maximize else 1) + m.set_objective([(x,coefficient)],maximize=maximize,offset=1e16) + with m.analyze_lp_evidence() as result: + if self.available:self.assertEqual(result.direction_value(x),-1 if upper else 1);self.assertEqual(result.diagnostics.primal.normalized_objective_slope,-1) + with Model(self.library) as m: + x=m.add_variable(upper=1);m.set_objective([(x,1)]) + with m.analyze_lp_evidence(EO(request=Request.BOTH)) as result: + if self.available: + self.assertEqual(result.primal_ray.state,State.UNAVAILABLE);self.assertEqual(result.farkas.state,State.UNAVAILABLE) + self.assertTrue(result.diagnostics.primal.base_check.valid) + with self.assertRaises(ApiError):result.base_value(x) + with self.assertRaises(ApiError):result.direction_value(x) + def test_limits_cancel_and_unsupported_scope(self): + with Model(self.library) as m,Cancellation(self.library) as cancel: + x=m.add_variable();m.set_objective([(x,-1)]);cancel.cancel() + for options,reason in ((EO(solve=Options(time_limit_seconds=0)),T.TIME_LIMIT), + (EO(solve=Options(cancellation=cancel)),T.CANCELLED),(EO(solve=Options(node_limit=0)),T.NODE_LIMIT), + (EO(max_work=0),T.ITERATION_LIMIT),(EO(max_auxiliary_variables=0),T.MEMORY_LIMIT), + (EO(max_auxiliary_rows=0),T.MEMORY_LIMIT),(EO(max_auxiliary_nonzeros=0),T.MEMORY_LIMIT), + (EO(max_retained_slots=0),T.MEMORY_LIMIT),(EO(max_auxiliary_solves=0),T.ITERATION_LIMIT if self.available else T.UNSUPPORTED)): + with m.analyze_lp_evidence(options) as result: + self.assertEqual(result.info.stop_reason,reason);self.assertEqual(result.info.attempted_calls,0) + if not result.info.has_evidence:self.assertIsNone(result.primal_ray);self.assertIsNone(result.farkas);self.assertIsNone(result.diagnostics) + with m.analyze_lp_evidence(EO(max_auxiliary_solves=1)) as result: + self.assertEqual(result.info.attempted_calls,1 if self.available else 0) + self.assertNotEqual(result.primal_ray.state,State.AVAILABLE) + for solve in (Options(backend=Backend.NATIVE),Options(guarantee=Guarantee.EXACT),Options(guarantee=Guarantee.CERTIFIED),Options(primal_start=[(x,0)])): + with m.analyze_lp_evidence(EO(solve=solve)) as result: + self.assertEqual(result.info.stop_reason,T.UNSUPPORTED);self.assertEqual(result.info.attempted_calls,0) + for kind in ('fixed_integer','semi','global','indicator'): + with Model(self.library) as m: + x=m.add_variable(V.INTEGER if kind=='fixed_integer' else V.SEMI_CONTINUOUS if kind=='semi' else V.CONTINUOUS,1,1) + if kind=='global':m.add_all_different([]) + if kind=='indicator': + gate=m.add_variable(V.BINARY);m.add_indicator(gate,True,[(x,1)],lower=1) + with m.analyze_lp_evidence() as result:self.assertEqual(result.info.stop_reason,T.UNSUPPORTED) + def test_c_layout_buffers_wrong_kind_and_python_types(self): + m,x,*_=self.ray_model() + with m: + for mutate in (lambda o:setattr(o,'struct_size',1),lambda o:setattr(o,'reserved',1),lambda o:setattr(o,'reserved_flags',1), + lambda o:setattr(o,'request',99),lambda o:setattr(o.solve,'struct_size',1),lambda o:setattr(o.solve,'reserved',1)): + native=EO()._marshal(self.library);mutate(native);out=b.U64(99) + with self.assertRaises(ApiError):self.library.call('analyze_lp_evidence',m._open(),C.byref(native),C.byref(out)) + self.assertEqual(out.value,0) + for opts in (EO(stationarity=-1),EO(recession=math.inf),EO(minimum_contradiction=math.nan)): + with self.assertRaises(ApiError):m.analyze_lp_evidence(opts) + for opts in (Options(),object()): + with self.assertRaises(TypeError):m.analyze_lp_evidence(opts) + for value in (True,0.5): + with self.assertRaises(TypeError):m.analyze_lp_evidence(EO(max_auxiliary_solves=value)) + other=Library(self.library.path) + with m.analyze_lp_evidence() as result,result.copy_stage(0) as child: + for owner in (result,child): + ordinary=b._Info() + with self.assertRaises(ApiError):self.library.call('result_info',owner._open(),C.byref(ordinary),C.sizeof(ordinary)) + out=b._EvidenceStage() + with self.assertRaises(ApiError):self.library.call('lp_evidence_stage_info',child._open(),C.byref(out),C.sizeof(out)-1) + with self.assertRaises(ApiError):self.library.call('lp_evidence_stage_info',result._open(),C.byref(out),C.sizeof(out)) + needed=b.U64();slots=(b._EvidenceSlot*1)();slots[0].active=77 + with self.assertRaises(ApiError) as error:self.library.call('lp_evidence_slots',result._open(),1,slots,C.sizeof(b._EvidenceSlot),1,C.byref(needed)) + self.assertEqual(error.exception.code,6);self.assertEqual(slots[0].active,77);self.assertEqual(needed.value,3) + with self.assertRaises(ApiError):self.library.call('lp_evidence_slots',result._open(),1,None,C.sizeof(b._EvidenceSlot)-1,0,C.byref(needed)) + with self.assertRaises(ApiError):self.library.call('lp_evidence_stage_text',child._open(),99,None,0,C.byref(needed)) + with self.assertRaises(ApiError):result.copy_stage(99) + with self.assertRaises(ValueError):result.slot(Variable(x.model_id,x.slot,other)) + with self.assertRaises(TypeError):result.slot(object()) + with self.assertRaises(FrozenInstanceError):child.info.phase=Phase.FARKAS + raw=b._EvidenceRawValue();self.library.call('lp_evidence_stage_raw_values',child._open(),None,C.sizeof(raw),0,C.byref(needed)) + if self.available:self.assertGreater(needed.value,0) + +if __name__=='__main__':unittest.main() diff --git a/python/tests/test_lp_observations.py b/python/tests/test_lp_observations.py new file mode 100644 index 0000000000..5bab164dda --- /dev/null +++ b/python/tests/test_lp_observations.py @@ -0,0 +1,175 @@ +"""Original-coordinate LP observations and owning C/Python boundary tests.""" +import ctypes as C +from dataclasses import FrozenInstanceError +import math +import unittest + +from gecode_optimize import ( + ApiError, Backend, Cancellation, Guarantee, Library, Model, Options, Row, + Session, Termination, VariableType, load_library, LpObservationOptions, + LpObservationState as State, LpObservationReason as Reason, LpBasisStatus, + LpDualSource, QuadraticModel) +from gecode_optimize import binding as b + + +class LpObservationsTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.library=load_library() + cls.available=cls.library.lp_observation_capabilities().available + + def model(self, maximize=False, offset=7): + m=Model(self.library) + x=m.add_variable(lower=0,upper=10);y=m.add_variable(lower=0,upper=10) + dead=m.add_variable(lower=0,upper=10);m.remove(dead) + removed=m.add_row();m.remove(removed) + row=m.add_row([(x,1),(y,1)],lower=-math.inf if maximize else 3, + upper=3 if maximize else math.inf) + objective=[(x,4 if maximize else 1),(y,1 if maximize else 2)] + m.set_objective(objective,maximize=maximize,offset=offset) + return m,x,y,row,dead,removed + + def test_analytic_signs_offsets_and_copied_history(self): + for maximize in (False,True): + for offset in (0,7,1e16): + with self.subTest(maximize=maximize,offset=offset): + m,x,y,row,dead,removed=self.model(maximize,offset) + with m,Session(self.library) as session: + observed=session.solve_lp_observed(m) + data=observed.observations + ordinary=observed.copy_result() + self.assertEqual(data.model_id,x.model_id) + self.assertEqual(len(data.rows),2);self.assertEqual(len(data.columns),3) + self.assertFalse(data.rows[0].active);self.assertIsNone(data.rows[0].dual) + with self.assertRaises(ValueError):data.row(removed) + with self.assertRaises(ValueError):data.column(dead) + with self.assertRaises(ValueError):data.row(Row(row.model_id+1,row.slot,self.library)) + with self.assertRaises(TypeError):data.row(x) + with self.assertRaises(FrozenInstanceError):data.rows[1].dual=99 + if self.available: + self.assertEqual(observed.termination,Termination.OPTIMAL) + self.assertEqual(data.dual_point.state,State.AVAILABLE) + self.assertTrue(data.checks.accepted) + self.assertAlmostEqual(data.row(row).dual,4 if maximize else 1) + self.assertAlmostEqual(data.column(y).reduced_cost,-3 if maximize else 1) + self.assertEqual(data.column(y).basis,LpBasisStatus.LOWER) + self.assertAlmostEqual(data.row(row).activity,3) + self.assertAlmostEqual(data.checks.normalized_gap,0) + self.assertIsNone(data.row(row).lower_slack if maximize else data.row(row).upper_slack) + self.assertAlmostEqual(data.row(row).upper_slack if maximize else data.row(row).lower_slack,0) + self.assertEqual(ordinary.value(x),3) + self.assertIsNotNone(data.metadata.backend_dual_tolerance) + else: + self.assertEqual(observed.termination,Termination.UNSUPPORTED) + self.assertEqual(data.dual_point.reason,Reason.UNSUPPORTED) + self.assertIsNone(data.row(row).dual);self.assertIsNone(data.checks.normalized_gap) + m.set_bounds(row,-math.inf if maximize else 4,4 if maximize else math.inf) + observed.close() + # Frozen data and separately copied Result own their history. + self.assertEqual(data.row(row).activity,3 if self.available else None) + if self.available:self.assertEqual(ordinary.value(x),3) + ordinary.close() + with self.assertRaises(RuntimeError):observed.copy_result() + + def test_constant_row_and_request_groups(self): + m,x,y,row,_,_=self.model() + with m: + constant=m.add_row(lower=-1,upper=1) + for duals,basis in ((True,True),(False,True),(True,False),(False,False)): + with m.solve_lp_observed(LpObservationOptions(duals=duals,basis=basis)) as result: + data=result.observations + if not duals: + self.assertEqual(data.dual_point.state,State.NOT_REQUESTED) + self.assertIsNone(data.row(row).dual);self.assertIsNone(data.column(y).reduced_cost) + self.assertIsNone(data.checks.max_stationarity) + elif self.available: + self.assertEqual(data.row(constant).dual,0) + self.assertEqual(data.row(constant).dual_source,LpDualSource.DERIVED_CONSTANT_ROW) + if not basis:self.assertEqual(data.basis.state,State.NOT_REQUESTED) + elif self.available: + self.assertEqual(data.basis.state,State.UNAVAILABLE) + self.assertEqual(data.basis.reason,Reason.ELIDED_CONSTANT_ROWS) + self.assertIsNone(data.row(constant).basis) + + def test_session_edits_interruption_no_stale_data(self): + m,x,_,row,_,_=self.model() + with m,Session(self.library) as session,Cancellation(self.library) as cancellation: + with session.solve_lp_observed(m) as old: + original=old.observations + m.set_bounds(row,4,math.inf) + with session.solve_lp_observed(m) as changed: + if self.available:self.assertEqual(changed.observations.row(row).activity,4) + self.assertNotEqual(changed.observations.revision,original.revision) + cancellation.cancel() + for options in (Options(time_limit_seconds=0),Options(cancellation=cancellation)): + with session.solve_lp_observed(m,LpObservationOptions(solve=options)) as stopped: + self.assertEqual(stopped.termination, + (Termination.CANCELLED if options.cancellation else Termination.TIME_LIMIT) if self.available else Termination.UNSUPPORTED) + data=stopped.observations + self.assertIsNone(data.row(row).activity);self.assertIsNone(data.row(row).dual) + self.assertIsNone(data.column(x).basis);self.assertFalse(data.checks.accepted) + self.assertIsNone(data.checks.max_stationarity) + self.assertEqual(original.row(row).activity,3 if self.available else None) + + def test_unsupported_scope_and_types(self): + m,x,_,row,_,_=self.model() + with m: + for solve in (Options(backend=Backend.NATIVE),Options(guarantee=Guarantee.EXACT),Options(guarantee=Guarantee.CERTIFIED)): + with m.solve_lp_observed(LpObservationOptions(solve=solve)) as out: + self.assertEqual(out.termination,Termination.UNSUPPORTED) + self.assertFalse(out.has_solution);self.assertIsNone(out.observations.row(row).dual) + global_id=m.add_all_different([]) + with m.solve_lp_observed() as out:self.assertEqual(out.termination,Termination.UNSUPPORTED) + m.remove(global_id);m.add_variable(VariableType.INTEGER,0,0) + with m.solve_lp_observed() as out:self.assertEqual(out.termination,Termination.UNSUPPORTED) + for options in (Options(),LpObservationOptions(duals=1),LpObservationOptions(solve=object())): + with self.assertRaises(TypeError):m.solve_lp_observed(options) + for value in (-1,math.inf,math.nan): + with self.assertRaises(ApiError):m.solve_lp_observed(LpObservationOptions(stationarity=value)) + with Session(self.library) as session,QuadraticModel(self.library) as qp: + with self.assertRaises(TypeError):session.solve_lp_observed(qp) + + def test_c_sizes_flags_buffers_and_handle_kinds(self): + m,x,y,row,_,_=self.model() + with m,m.solve_lp_observed() as result: + library=self.library + for mutate in (lambda o:setattr(o,"struct_size",C.sizeof(o)-1),lambda o:setattr(o,"reserved",1), + lambda o:setattr(o,"duals",2),lambda o:setattr(o,"basis",-1), + lambda o:setattr(o.solve,"struct_size",C.sizeof(o.solve)-1),lambda o:setattr(o.solve,"reserved",1)): + native=LpObservationOptions()._marshal(library);mutate(native);output=b.U64(99) + with self.assertRaises(ApiError) as caught:library.call("solve_lp_observed",m._open(),C.byref(native),C.byref(output)) + self.assertEqual(caught.exception.code,1);self.assertEqual(output.value,0) + out=b._LpInfo() + with self.assertRaises(ApiError):library.call("lp_observed_result_info",result._open(),C.byref(out),C.sizeof(out)-1) + with self.assertRaises(ApiError) as caught:library.call("lp_observed_result_info",m._open(),C.byref(out),C.sizeof(out)) + self.assertEqual(caught.exception.code,2) + with self.assertRaises(ApiError):library.call("result_info",result._open(),C.byref(b._Info()),C.sizeof(b._Info)) + buffer=(b._LpRow*1)();C.memset(buffer,0x5a,C.sizeof(buffer));before=bytes(buffer);needed=b.U64() + with self.assertRaises(ApiError) as caught:library.call("lp_observed_result_rows",result._open(),buffer,C.sizeof(b._LpRow),1,C.byref(needed)) + self.assertEqual(caught.exception.code,6);self.assertEqual(needed.value,2);self.assertEqual(bytes(buffer),before) + record=b._LpRow();library.call("lp_observed_result_row",result._open(),row._id(),C.byref(record),C.sizeof(record)) + self.assertEqual(record.struct_size,C.sizeof(record));self.assertEqual(record.reserved,0) + self.assertEqual(record.dual.reserved,0) + if not self.available:self.assertEqual((record.dual.present,record.dual.value),(0,0)) + count=b.U64();library.call("lp_observed_result_text",result._open(),0,None,0,C.byref(count)) + text=C.create_string_buffer(count.value);library.call("lp_observed_result_text",result._open(),0,text,count.value,C.byref(count)) + self.assertEqual(text.raw[-1],0) + + def test_empty_available_rows_are_distinct_from_unavailable_duals(self): + with Model(self.library) as model,model.solve_lp_observed() as result: + data=result.observations + self.assertEqual(data.rows,());self.assertEqual(data.columns,()) + self.assertEqual(data.primal_rows.state,State.AVAILABLE if self.available else State.UNAVAILABLE) + self.assertEqual(data.dual_point.state,State.UNAVAILABLE) + self.assertIsNone(data.checks.normalized_gap) + with result.copy_result() as ordinary: + self.assertEqual(ordinary.termination,Termination.OPTIMAL if self.available else Termination.UNSUPPORTED) + + def test_capabilities_are_copied_immutable(self): + caps=self.library.lp_observation_capabilities() + self.assertEqual(caps.available,caps.duals);self.assertEqual(caps.available,caps.basis_export) + self.assertTrue(caps.limitations);self.assertIsInstance(caps.limitations,tuple) + with self.assertRaises(FrozenInstanceError):caps.available=False + + +if __name__=="__main__":unittest.main() diff --git a/python/tests/test_lp_sensitivity.py b/python/tests/test_lp_sensitivity.py new file mode 100644 index 0000000000..bf8a842f21 --- /dev/null +++ b/python/tests/test_lp_sensitivity.py @@ -0,0 +1,204 @@ +"""Original-basis numerical ranges through owning C/Python records.""" +import ctypes as C +from dataclasses import FrozenInstanceError, replace +import math +import unittest +from unittest.mock import patch +from gecode_optimize import (ApiError, Backend, Cancellation, Guarantee, Library, Model, Options, Row, Variable, + VariableType, load_library, LpObservationOptions, LpBasisStatus, LpSensitivityOptions as SO, + LpSensitivityLimits as Limits, LpSensitivityTolerances as Checks, LpObjectiveParameter as Obj, + LpEqualityRhsParameter as Rhs, LpSensitivityCompletion as Complete, LpSensitivityReason as Reason, + LpSensitivityState as State, LpRangeEndKind as End, LpRangeEnd, Termination) +from gecode_optimize import binding as b + +class LpSensitivityTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.library=load_library();cls.available=cls.library.lp_observation_capabilities().available + def model(self,maximize=False): + m=Model(self.library);dead=m.add_variable();m.remove(dead);x=m.add_variable();y=m.add_variable();z=m.add_variable(upper=1) + gone=m.add_row();m.remove(gone);row=m.add_row([(x,1),(y,1)],3,3) + m.set_objective([(x,-2 if maximize else 2),(y,-1 if maximize else 1)],maximize=maximize,offset=7) + return m,x,y,z,row,dead,gone + def test_analytic_endpoints_and_history(self): + for maximize in (False,True): + m,x,y,z,row,dead,gone=self.model(maximize) + with m,m.solve_lp_observed() as source: + original=source.info + with source.analyze_sensitivity(SO(parameters=(Obj(x),Obj(y),Rhs(row)))) as result: + self.assertEqual(result.info.model_id,x.model_id);self.assertEqual(result.info.guarantee,Guarantee.NUMERICAL) + self.assertEqual(result.work.preparation_visits,3);copied=result.copy_source_observed() + if not self.available: + self.assertEqual(result.info.completion,Complete.REJECTED);self.assertIsNone(result.entries) + self.assertFalse(result.work.factor_setup_attempted) + with self.assertRaises(ApiError) as error:result.copy_basis() + self.assertEqual(error.exception.code,9) + copied.close();continue + self.assertEqual(result.info.completion,Complete.COMPLETE);self.assertIsNone(result.info.stop_reason) + a,c,r=result.entries;self.assertTrue(all(e.group.state is State.AVAILABLE and e.interval.checks.accepted for e in result.entries)) + if maximize: + self.assertEqual(a.interval.lower.kind,End.NEGATIVE_INFINITY);self.assertEqual(a.interval.upper.value,-1) + self.assertEqual(c.interval.lower.value,-2);self.assertEqual(c.interval.upper.kind,End.POSITIVE_INFINITY) + else: + self.assertEqual(a.interval.lower.value,1);self.assertEqual(a.interval.upper.kind,End.POSITIVE_INFINITY) + self.assertEqual(c.interval.lower.kind,End.NEGATIVE_INFINITY);self.assertEqual(c.interval.upper.value,2) + self.assertEqual(r.interval.anchor,3);self.assertEqual(r.interval.lower.value,0) + self.assertEqual(r.interval.upper.kind,End.POSITIVE_INFINITY);self.assertIsNone(r.interval.upper.value) + self.assertEqual(r.interval.objective_slope,-1 if maximize else 1) + self.assertEqual(result.objective(x),a);self.assertEqual(result.equality_rhs(row),r);self.assertIsNone(result.objective(z)) + self.assertEqual(result.factor_order,(y,));self.assertEqual(result.active_slots(Variable),(False,True,True,True)) + self.assertEqual(result.active_slots(Row),(False,True));self.assertTrue(result.reference_checks.primal.valid) + self.assertTrue(result.reference_checks.kkt.accepted);self.assertTrue(result.reference_checks.basis_point_matches) + self.assertIsNotNone(result.reference_checks.max_system_residual) + work=result.work;result.entries;result.reference_checks;result.factor_order;self.assertEqual(work,result.work) + basis=result.copy_basis();saved=result.entries;checks=result.reference_checks + for bad in (dead,Variable(x.model_id+1,x.slot,self.library)): + with self.assertRaises(ApiError):result.objective(bad) + with self.assertRaises(ApiError):result.equality_rhs(gone) + with self.assertRaises(TypeError):result.objective(row) + with self.assertRaises(FrozenInstanceError):r.interval.anchor=99 + m.set_objective_offset(999) + with copied,basis,copied.copy_result() as ordinary: + self.assertEqual(copied.info,original);self.assertEqual(ordinary.objective,4 if maximize else 10) + self.assertEqual(basis.column(y),LpBasisStatus.BASIC) + self.assertTrue(saved[0].interval.checks.accepted);self.assertTrue(checks.kkt.accepted) + with self.assertRaises(RuntimeError):result.entry(0) + def test_singletons_zero_coefficients_and_factor_entities(self): + with Model(self.library) as m: + x=m.add_variable(upper=2);z=m.add_variable(upper=1);r=m.add_row([(x,1)],1,1);q=m.add_row([(x,2)],2,2) + m.set_objective([(x,1)]) + with m.solve_lp_observed() as source,source.analyze_sensitivity(SO(parameters=(Rhs(r),Rhs(q),Obj(z)))) as result: + if not self.available:self.assertEqual(result.info.completion,Complete.REJECTED);return + self.assertEqual(result.info.completion,Complete.COMPLETE) + for entry,anchor in zip(result.entries[:2],(1,2)): + self.assertEqual(entry.interval.lower.value,anchor);self.assertEqual(entry.interval.upper.value,anchor) + self.assertEqual(result.objective(z).interval.anchor,0) + self.assertEqual(len(result.factor_order),2);self.assertTrue(any(isinstance(v,Row) for v in result.factor_order)) + def test_rejections_and_source_preservation(self): + m,x,_,_,row,*_=self.model() + with m,m.solve_lp_observed() as source: + for options in (SO(parameters=(Obj(x),),backend=Backend.NATIVE),SO(parameters=(Obj(x),Obj(x))), + SO(parameters=(Obj(Variable(x.model_id+1,x.slot,self.library)),))): + with source.analyze_sensitivity(options) as result,result.copy_source_observed() as copied: + self.assertEqual(result.info.completion,Complete.REJECTED);self.assertEqual(copied.info,source.info) + self.assertFalse(result.work.factor_setup_attempted) + for observation in (LpObservationOptions(basis=False),LpObservationOptions(duals=False),LpObservationOptions(solve=Options(time_limit_seconds=0))): + with m.solve_lp_observed(observation) as weak,weak.analyze_sensitivity(SO(parameters=(Obj(x),))) as result: + self.assertEqual(result.info.completion,Complete.REJECTED);self.assertFalse(result.work.factor_setup_attempted) + m.set_bounds(row,0,4) + with m.solve_lp_observed() as ranged,ranged.analyze_sensitivity(SO(parameters=(Rhs(row),))) as result: + self.assertEqual(result.info.completion,Complete.REJECTED) + for kind in (VariableType.INTEGER,VariableType.SEMI_CONTINUOUS): + with Model(self.library) as m: + x=m.add_variable(kind,1,2);m.add_row([(x,1)],1,2) + with m.solve_lp_observed() as source,source.analyze_sensitivity(SO(parameters=(Obj(x),))) as result: + self.assertEqual(result.info.completion,Complete.REJECTED) + def test_quotas_cancel_and_invalid_precedence(self): + m,x,*_=self.model() + with m,m.solve_lp_observed() as source,Cancellation(self.library) as token: + self.assertFalse(token.cancelled) + cases=((SO(parameters=(Obj(x),),time_limit_seconds=0),Termination.TIME_LIMIT), + (SO(parameters=(Obj(x),),limits=Limits(max_work=0)),Termination.ITERATION_LIMIT), + (SO(parameters=(Obj(x),),limits=Limits(max_requests=0)),Termination.MEMORY_LIMIT)) + for options,stop in cases: + with source.analyze_sensitivity(options) as result: + self.assertEqual(result.info.completion,Complete.INTERRUPTED);self.assertEqual(result.info.stop_reason,stop) + self.assertEqual(result.work.preparation_visits,0);self.assertFalse(result.work.factor_setup_attempted) + token.cancel();self.assertTrue(token.cancelled) + with source.analyze_sensitivity(SO(parameters=(Obj(x),),cancellation=token)) as result:self.assertEqual(result.info.stop_reason,Termination.CANCELLED) + for options in (SO(),SO(parameters=(Obj(x),),time_limit_seconds=-1,cancellation=token), + SO(parameters=(Obj(x),),checks=Checks(stationarity=math.nan),cancellation=token)): + with source.analyze_sensitivity(options) as result: + self.assertEqual(result.info.completion,Complete.REJECTED);self.assertEqual(result.info.reason,Reason.INVALID_SOURCE) + self.assertIsNone(result.info.stop_reason) + if self.available: + with source.analyze_sensitivity(SO(parameters=(Obj(x),),limits=Limits(max_work=1))) as result: + self.assertEqual(result.work.preparation_visits,1);self.assertEqual(result.work.coordinator_visits,0) + self.assertEqual(result.info.reason,Reason.RESOURCE_LIMIT);self.assertFalse(result.work.factor_setup_attempted) + def test_python_final_cleanup_clear(self): + m,x,y,z,row,*_=self.model() + with m,m.solve_lp_observed() as source,Cancellation(self.library) as token: + if not self.available:return + for cancel in (False,True): + # C++ uses its real monotonic clock. Only the outer Python clock + # advances after a successful C analysis, without a timing race. + calls=iter((0.0,0.0,0.0 if cancel else 2.0));own_call=b._own_call + def finish(*args): + result=own_call(*args) + if cancel and args[2]=='analyze_lp_sensitivity':token.cancel() + return result + with patch.object(b.time,'monotonic',side_effect=lambda:next(calls)),patch.object(b,'_own_call',side_effect=finish): + result=source.analyze_sensitivity(SO(parameters=(Obj(x),Rhs(row)),time_limit_seconds=1,cancellation=token)) + with result: + self.assertEqual(result.info.completion,Complete.INTERRUPTED);self.assertEqual(result.info.reason,Reason.STOPPED) + self.assertEqual(result.info.stop_reason,Termination.CANCELLED if cancel else Termination.TIME_LIMIT) + self.assertTrue(result.info.has_sensitivity) + for entry in (*result.entries,result.entry(0),result.objective(x),result.equality_rhs(row)): + self.assertIsNone(entry.interval);self.assertEqual(entry.group.state,State.UNAVAILABLE) + self.assertIsNone(result.objective(z));self.assertTrue(result.reference_checks.kkt.accepted) + with result.copy_source_observed() as copied:self.assertEqual(copied.info,source.info) + with result.copy_basis() as basis:self.assertEqual(basis.column(y),LpBasisStatus.BASIC) + def test_abi_records_buffers_and_types(self): + m,x,*_=self.model() + with m,m.solve_lp_observed() as source: + for mutate in (lambda o:setattr(o,'struct_size',0),lambda o:setattr(o,'reserved',1), + lambda o:setattr(o.checks,'struct_size',0),lambda o:setattr(o.limits,'reserved',1), + lambda o:setattr(o.requests[0],'struct_size',0),lambda o:setattr(o.requests[0],'reserved_flags',1)): + o=SO(parameters=(Obj(x),))._marshal(self.library);mutate(o);h=b.U64(99) + with self.assertRaises(ApiError) as error:self.library.call('analyze_lp_sensitivity',source._open(),C.byref(o),C.byref(h)) + self.assertEqual(error.exception.code,1);self.assertEqual(h.value,0) + for invalid in (Options(),object()): + with self.assertRaises(TypeError):source.analyze_sensitivity(invalid) + for params in ((object(),),(Obj(1),),(Obj(x),Rhs(x))): + with self.assertRaises(TypeError):source.analyze_sensitivity(SO(parameters=params)) + another=Library(self.library.path) + with Model(another) as other: + alien=other.add_variable() + with self.assertRaises(ValueError):source.analyze_sensitivity(SO(parameters=(Obj(alien),))) + with source.analyze_sensitivity(SO(parameters=(Obj(x),))) as result: + info=b._SensitivityInfo() + with self.assertRaises(ApiError):self.library.call('sensitivity_info',result._open(),C.byref(info),C.sizeof(info)-1) + if self.available: + needed=b.U64();entry=b._SensitivityEntry();C.memset(C.byref(entry),0xA5,C.sizeof(entry)) + with self.assertRaises(ApiError) as error:self.library.call('sensitivity_entries',result._open(),C.byref(entry),C.sizeof(entry),0,C.byref(needed)) + self.assertEqual(error.exception.code,6);self.assertEqual(entry.struct_size,0xa5a5a5a5a5a5a5a5) + for kind,value in ((End.FINITE,None),(End.FINITE,math.inf),(End.POSITIVE_INFINITY,0)): + with self.assertRaises(ValueError):LpRangeEnd(kind,value) + def test_real_partial_interval_is_not_complete(self): + with Model(self.library) as m: + y=m.add_variable(lower=-math.inf);w=m.add_variable(upper=1) + m.add_row([(y,1),(w,1e14)],1,1);m.set_objective([(y,100),(w,1e16-2)]) + with m.solve_lp_observed() as source,source.analyze_sensitivity(SO(parameters=(Obj(y),Obj(w)))) as result: + if not self.available:self.assertEqual(result.info.completion,Complete.REJECTED);return + with source.copy_result() as ordinary:self.assertEqual(ordinary.objective,98) + self.assertEqual(result.info.completion,Complete.PARTIAL) + rejected,accepted=result.entries + self.assertEqual(rejected.group.state,State.REJECTED);self.assertEqual(rejected.group.reason,Reason.INTERVAL_CHECKS) + self.assertIsNone(rejected.interval);self.assertEqual(accepted.group.state,State.AVAILABLE) + self.assertEqual(accepted.interval.upper.value,1e16);self.assertEqual(result.entry(1),accepted) + self.assertEqual(result.objective(y),rejected);self.assertTrue(result.reference_checks.kkt.accepted) + def test_cancellation_owner_close_and_shared_copy(self): + with Cancellation(self.library) as token,token.copy() as copied: + self.assertFalse(copied.cancelled);token.cancel();token.close();self.assertTrue(copied.cancelled) + with copied.copy() as second:copied.close();self.assertTrue(second.cancelled) + m,x,*_=self.model() + with m,m.solve_lp_observed() as source: + if not self.available:return + for cancel in (False,True): + with Cancellation(self.library) as token: + own_call=b._own_call + def finish(*args): + result=own_call(*args) + if args[2]=='analyze_lp_sensitivity': + if cancel:token.cancel() + token.close() + return result + with patch.object(b,'_own_call',side_effect=finish): + result=source.analyze_sensitivity(SO(parameters=(Obj(x),),cancellation=token)) + with result: + self.assertTrue(result.info.has_sensitivity) + self.assertEqual(result.info.completion,Complete.INTERRUPTED if cancel else Complete.COMPLETE) + self.assertEqual(result.info.stop_reason,Termination.CANCELLED if cancel else None) + self.assertEqual(result.entry(0).group.state,State.UNAVAILABLE if cancel else State.AVAILABLE) + self.assertTrue(result.reference_checks.kkt.accepted) +if __name__=='__main__':unittest.main() diff --git a/python/tests/test_native_starts.py b/python/tests/test_native_starts.py new file mode 100644 index 0000000000..770317f679 --- /dev/null +++ b/python/tests/test_native_starts.py @@ -0,0 +1,90 @@ +"""Native complete starts through the existing owning C/Python boundary.""" +import unittest + +from gecode_optimize import (Backend, Cancellation, Guarantee, Model, Options, + Termination, VariableType, load_library) + + +class NativeStarts(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.lib = load_library() + cls.native = cls.lib.capabilities(Backend.NATIVE)["available"] + + def options(self, start, **kwargs): + return Options(backend=Backend.NATIVE, guarantee=Guarantee.EXACT, + primal_start=start, **kwargs) + + def test_both_senses_activation_values_and_owning_history(self): + for active in (False, True): + for maximize in (False, True): + with self.subTest(active=active, maximize=maximize), Model(self.lib) as model: + b = model.add_variable(VariableType.BINARY) + x = model.add_variable(VariableType.INTEGER, 0, 4) + gate = model.add_indicator(b, active, {x: 1}, lower=3).inactive_gate + model.set_objective({x: 1, b: -10}, maximize=maximize, offset=3) + feasible = [(q, v) for q in (0, 1) for v in range(5) + if q != int(active) or v >= 3] + key = lambda pair: pair[1] - 10*pair[0] + 3 + best = (max if maximize else min)(feasible, key=key) + poor = (min if maximize else max)(feasible, key=key) + start = {b: poor[0], x: poor[1]} # omit live derived gate + result = model.solve(self.options(start)) + identity = model.identity + start.clear() + model.set_objective({}, offset=91) + with result: + self.assertEqual(result.termination, Termination.OPTIMAL if self.native else Termination.UNSUPPORTED) + self.assertEqual(result.info["start_submitted"], self.native) + self.assertEqual((result.info["model_id"], result.info["revision"]), identity) + if self.native: + self.assertTrue(result.info["solution_validated"]) + self.assertEqual(result.objective, key(best)) + self.assertEqual(result.best_bound, key(best)) + self.assertEqual((result.value(b), result.value(x)), best) + self.assertEqual(result.value(gate), int(best[0] != int(active))) + + def test_exact_input_errors_partial_starts_and_zero_budget(self): + with Model(self.lib) as model, Cancellation(self.lib) as token: + b = model.add_variable(VariableType.BINARY) + x = model.add_variable(VariableType.INTEGER, 0, 4) + gate = model.add_indicator(b, True, {x: 1}, lower=3).inactive_gate + # Each rejected complete point violates a different original rule. + cases = [({b: 0, x: 4, gate: 0}, Termination.INVALID_MODEL), + ({b: 1-1e-12, x: 3}, Termination.INVALID_MODEL), + ({b: 1, x: 2}, Termination.INVALID_MODEL), + ({b: 0}, Termination.UNSUPPORTED)] + for start, expected in cases: + with self.subTest(start=start), model.solve(self.options(start)) as result: + self.assertEqual(result.termination, expected if self.native else Termination.UNSUPPORTED) + self.assertFalse(result.has_solution) + self.assertFalse(result.info["start_submitted"]) + if self.native: + token.cancel() + for changes, expected in [({"time_limit_seconds": 0}, Termination.TIME_LIMIT), + ({"node_limit": 0}, Termination.NODE_LIMIT), + ({"cancellation": token}, Termination.CANCELLED)]: + with model.solve(self.options({b: 0, x: 4}, **changes)) as result: + self.assertEqual(result.termination, expected) + self.assertFalse(result.has_solution) + self.assertFalse(result.info["start_submitted"]) + + def test_removed_gate_requires_explicit_value_and_is_not_rederived(self): + with Model(self.lib) as model: + b = model.add_variable(VariableType.BINARY) + x = model.add_variable(VariableType.INTEGER, 0, 4) + indicator = model.add_indicator(b, True, {x: 1}, lower=3) + gate = indicator.inactive_gate + model.remove(indicator) + model.set_objective({}, offset=11) + with model.solve(self.options({b: 0, x: 4})) as incomplete: + self.assertEqual(incomplete.termination, Termination.UNSUPPORTED) + self.assertFalse(incomplete.has_solution) + # This gate=0 contradicts its old, now removed equation at b=0. + with model.solve(self.options({b: 0, x: 4, gate: 0})) as complete: + self.assertEqual(complete.termination, Termination.OPTIMAL if self.native else Termination.UNSUPPORTED) + if self.native: + self.assertTrue(complete.info["start_submitted"]) + self.assertEqual(complete.objective, 11) + self.assertEqual(complete.value(gate), 0) + self.assertEqual(complete.value(x), 4) diff --git a/python/tests/test_quadratic.py b/python/tests/test_quadratic.py new file mode 100644 index 0000000000..3954caf3a9 --- /dev/null +++ b/python/tests/test_quadratic.py @@ -0,0 +1,245 @@ +"""Distinct finite-box QP binding conformance, with analytic original oracles.""" +import ctypes as C +from dataclasses import FrozenInstanceError +import math +import unittest + +from gecode_optimize import (ApiError, Backend, Cancellation, Guarantee, Library, + Model, Options, QuadraticModel, QuadraticOptions, QuadraticResult, Result, + Session, Termination, WeightedSquare, load_library) + + +class Quadratic(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.lib=load_library() + cls.available=cls.lib.quadratic_capabilities()["available"] + + def assert_completed(self, result): + self.assertIsInstance(result,QuadraticResult) + self.assertNotIsInstance(result,Result) + self.assertEqual(result.termination,Termination.OPTIMAL if self.available else Termination.UNSUPPORTED) + self.assertEqual(result.has_solution,self.available) + self.assertEqual(result.info["guarantee"],Guarantee.NUMERICAL) + checks=result.checks + if self.available: + self.assertTrue(checks.primal_valid and checks.objective_valid and checks.kkt_available) + self.assertTrue(checks.kkt_valid and checks.bound_valid) + self.assertLessEqual(checks.gap_upper_bound,1e-6) + self.assertIsNotNone(checks.max_stationarity) + self.assertIsNotNone(checks.max_complementarity) + else: + self.assertFalse(checks.primal_valid or checks.objective_valid or checks.kkt_available) + self.assertFalse(checks.kkt_valid or checks.bound_valid) + for field in ("original_objective","normalized_lower_bound","gap_upper_bound","max_stationarity","max_complementarity"): + self.assertIsNone(getattr(checks,field)) + for field in ("objective","best_bound","absolute_gap","relative_gap","vendor_objective","vendor_dual_estimate", + "original_gradient","square_residuals"): + self.assertIsNone(getattr(result,field)) + + def test_both_senses_original_objective_gradient_bounds_and_offsets(self): + # Independent closed form: x*=2-l/(2*w), f=c+l*x+w*(x-2)^2. + for maximize in (False,True): + for weight in (0.5,1,2): + for linear in (-1,0,1): + with self.subTest(maximize=maximize,weight=weight,linear=linear),QuadraticModel(self.lib) as model: + x=model.add_continuous(-4,4,name="decision λ") + square=WeightedSquare({x:1},-2,weight,"residual Ï€") + sign=-1 if maximize else 1 + setter=model.maximize_concave_squares if maximize else model.minimize_squares + setter([square],{x:sign*linear},offset=-13) + with model.solve() as result: + self.assert_completed(result) + if not self.available: + continue + optimum=2-linear/(2*weight) + objective=-13+sign*(linear*optimum+weight*(optimum-2)**2) + self.assertAlmostEqual(result.value(x),optimum,places=6) + self.assertAlmostEqual(result.objective,objective,places=6) + self.assertAlmostEqual(result.checks.original_objective,objective,places=6) + self.assertAlmostEqual(result.square_residuals[0],optimum-2,places=6) + self.assertAlmostEqual(result.original_gradient[x.slot],0,places=6) + self.assertLessEqual(sign*result.best_bound,sign*objective+1e-12) + self.assertLessEqual(result.checks.normalized_lower_bound,sign*objective+1e-12) + self.assertEqual(result.info["regularization"],0) + self.assertIsInstance(result.message,str) + self.assertTrue(result.backend_version) + with self.assertRaises(FrozenInstanceError): result.checks.bound_valid=False + + def test_rows_mutations_tombstones_and_historical_ownership(self): + with QuadraticModel(self.lib) as model: + dead=model.add_continuous(0,1);model.remove(dead) + x=model.add_continuous(-4,4) + square=WeightedSquare({x:1},-2,2) + model.minimize_squares([square],{x:4},offset=-7) + identity=model.identity + retained=model.solve() + self.assert_completed(retained) + self.assertEqual((retained.info["model_id"],retained.info["revision"]),identity) + square.offset=99;square.terms={};square.weight=99 # Inputs were copied. + row=model.add_row({x:1},lower=2) + model.set_coefficient(row,x,2);model.set_bounds(row,4,math.inf) + with model.solve() as result: + self.assert_completed(result) + if self.available: + self.assertAlmostEqual(result.value(x),2) + self.assertAlmostEqual(result.objective,1) + model.remove(row);model.set_bounds(x,3,4) + with model.solve() as result: + self.assert_completed(result) + if self.available:self.assertAlmostEqual(result.value(x),3) + model.minimize_squares();model.remove(x) + with retained: + self.assertEqual(retained.values[dead.slot],{"active":False,"present":False,"value":None}) + if self.available: + self.assertAlmostEqual(retained.value(x),1) + self.assertAlmostEqual(retained.objective,-1) + self.assertEqual(retained.original_gradient[dead.slot],0) + with self.assertRaises(ApiError):retained.value(dead) + else: + with self.assertRaises(ApiError) as error:retained.value(x) + self.assertEqual(error.exception.code,7) + with self.assertRaises(RuntimeError):retained.checks + + def test_empty_and_constant_square_and_huge_offset(self): + with QuadraticModel(self.lib) as model: + model.minimize_squares([WeightedSquare((),3,2)],offset=-7) + with model.solve() as result: + self.assert_completed(result) + if self.available: + self.assertEqual(result.values,()) + self.assertEqual(result.original_gradient,()) + self.assertEqual(result.square_residuals,(3,)) + self.assertEqual(result.objective,11) + with QuadraticModel(self.lib) as model: + x=model.add_continuous(0,2) + model.minimize_squares([WeightedSquare({x:1},-1)],linear={x:2},offset=1e16) + with model.solve() as result: + self.assert_completed(result) + if self.available: + self.assertLessEqual(result.checks.gap_upper_bound,1e-6) + self.assertGreater(result.absolute_gap,result.checks.gap_upper_bound) + self.assertEqual(result.objective,1e16) + + def test_coupled_squares_and_equality_original_gradient(self): + with QuadraticModel(self.lib) as model: + x=model.add_continuous(-2,2);y=model.add_continuous(-2,2) + # Under x+y=1, minimize 2(x-y)^2 + (x+y-2)^2. + model.add_row({x:1,y:1},lower=1,upper=1) + model.minimize_squares([WeightedSquare([(x,2),(x,-1),(y,-1)],weight=2), + WeightedSquare({x:1,y:1},offset=-2)],offset=7) + with model.solve() as result: + self.assert_completed(result) + if self.available: + self.assertAlmostEqual(result.value(x),0.5) + self.assertAlmostEqual(result.value(y),0.5) + self.assertAlmostEqual(result.objective,8) + self.assertEqual(len(result.square_residuals),2) + self.assertAlmostEqual(result.square_residuals[0],0) + self.assertAlmostEqual(result.square_residuals[1],-1) + # A constrained optimum has a nonzero original gradient. + self.assertAlmostEqual(result.original_gradient[x.slot],-2) + self.assertAlmostEqual(result.original_gradient[y.slot],-2) + + def test_rejected_mutations_preserve_objective_and_revision(self): + with QuadraticModel(self.lib) as model,QuadraticModel(self.lib) as foreign: + x=model.add_continuous(-4,4);y=foreign.add_continuous(-4,4) + model.minimize_squares([WeightedSquare({x:1},-2)]) + revision=model.identity + bad=[WeightedSquare({y:1}),WeightedSquare({x:math.nan}),WeightedSquare({x:1},weight=-1), + WeightedSquare({x:1},weight=0),WeightedSquare({x:1},offset=math.inf)] + for square in bad: + with self.assertRaises(ApiError):model.minimize_squares([square]) + self.assertEqual(model.identity,revision) + with self.assertRaises(ApiError):model.minimize_squares([WeightedSquare({x:1})],{y:1}) + with self.assertRaises(ApiError):model.remove(x) + with self.assertRaises(ApiError):model.set_bounds(x,0,math.inf) + with self.assertRaises(ApiError):model.add_continuous(math.nan,1) + with self.assertRaises(ValueError):model.minimize_squares([WeightedSquare({x:1},name="bad\0name")]) + with self.assertRaises(TypeError):model.minimize_squares([object()]) + self.assertEqual(model.identity,revision) + with model.solve() as result: + self.assert_completed(result) + if self.available: + self.assertAlmostEqual(result.value(x),2) + with self.assertRaises(ApiError):result.value(y) + model.minimize_squares();model.remove(x) + with self.assertRaises(ApiError):model.add_row({x:1}) + + def test_distinct_classes_and_library_ownership(self): + with QuadraticModel(self.lib) as model,Model(self.lib) as linear,Session(self.lib) as session: + self.assertNotIsInstance(model,Model) + with self.assertRaises(TypeError):session.solve(model) + with self.assertRaises(TypeError):model.solve(Options()) + x=model.add_continuous(0,1) + with self.assertRaises(ApiError):linear.add_row({x:1}) + second=Library(self.lib.path) + with QuadraticModel(second) as foreign: + y=foreign.add_continuous(0,1) + with self.assertRaises(ValueError):model.add_row({y:1}) + with self.assertRaises(ValueError):model.minimize_squares([WeightedSquare({y:1})]) + # Even direct C misuse cannot erase the quadratic objective. + out=C.c_uint64() + with self.assertRaises(ApiError) as error:self.lib.call("solve",model._open(),None,C.byref(out)) + self.assertEqual(error.exception.code,2) + self.assertEqual(out.value,0) + + def test_limits_cancellation_unsupported_and_bad_option_fields(self): + with QuadraticModel(self.lib) as model,Cancellation(self.lib) as cancel: + x=model.add_continuous(-4,4);model.minimize_squares([WeightedSquare({x:1},-2)]) + unsupported=[Options(backend=Backend.NATIVE),Options(guarantee=Guarantee.EXACT), + Options(guarantee=Guarantee.CERTIFIED),Options(threads=2),Options(random_seed=1),Options(node_limit=1), + Options(primal_start={x:2})] + for solve in unsupported: + with model.solve(QuadraticOptions(solve)) as result: + self.assertEqual(result.termination,Termination.UNSUPPORTED) + self.assertFalse(result.has_solution) + self.assertIsNone(result.checks.gap_upper_bound) + for solve in (Options(time_limit_seconds=0),Options(cancellation=cancel)): + if solve.cancellation:cancel.cancel() + with model.solve(QuadraticOptions(solve)) as result: + self.assertEqual(result.termination,Termination.CANCELLED if solve.cancellation else Termination.TIME_LIMIT) + self.assertFalse(result.has_solution) + self.assertIsNone(result.objective) + self.assertFalse(result.checks.kkt_available) + self.assertIsNone(result.checks.max_stationarity) + self.assertIsNone(result.checks.max_complementarity) + for options in (QuadraticOptions(optimality_tolerance=math.nan),QuadraticOptions(stationarity_tolerance=-1), + QuadraticOptions(complementarity_tolerance=math.inf)): + with self.assertRaises(ApiError):model.solve(options) + for options in (QuadraticOptions(iteration_limit=-1),QuadraticOptions(max_auxiliary_variables=2**64)): + with self.assertRaises(OverflowError):model.solve(options) + with self.assertRaises(TypeError):model.solve(QuadraticOptions(iteration_limit=True)) + with self.assertRaises(TypeError):model.solve(QuadraticOptions(solve=object())) + with model.solve(QuadraticOptions(max_auxiliary_variables=0)) as result: + self.assertEqual(result.termination,Termination.UNSUPPORTED) + self.assertFalse(result.has_solution) + + def test_iteration_limit_keeps_candidate_separate_from_optimality(self): + with QuadraticModel(self.lib) as model: + x=model.add_continuous(-4,4) + model.minimize_squares([WeightedSquare({x:1},-2)]) + with model.solve(QuadraticOptions(iteration_limit=0)) as result: + self.assertEqual(result.termination,Termination.ITERATION_LIMIT if self.available else Termination.UNSUPPORTED) + self.assertEqual(result.info["qp_iterations"],0) + if result.has_solution: + self.assertTrue(result.checks.primal_valid and result.checks.objective_valid) + self.assertAlmostEqual(result.objective,(result.value(x)-2)**2) + self.assertFalse(result.checks.kkt_valid) + self.assertGreater(result.checks.gap_upper_bound,1e-6) + + def test_infeasible_rows_have_no_successful_original_checks(self): + with QuadraticModel(self.lib) as model: + x=model.add_continuous(0,1) + model.minimize_squares([WeightedSquare({x:1})]) + model.add_row({x:1},lower=2) + with model.solve() as result: + self.assertEqual(result.termination,Termination.INFEASIBLE if self.available else Termination.UNSUPPORTED) + self.assertFalse(result.has_solution) + self.assertFalse(result.checks.primal_valid or result.checks.objective_valid or result.checks.kkt_valid) + self.assertIsNone(result.objective) + self.assertIsNone(result.checks.gap_upper_bound) + + +if __name__=="__main__": + unittest.main() diff --git a/python/tests/test_regular.py b/python/tests/test_regular.py new file mode 100644 index 0000000000..d0a45509ab --- /dev/null +++ b/python/tests/test_regular.py @@ -0,0 +1,129 @@ +"""Independent finite-word oracles for typed Regular C/Python semantics.""" +import ctypes as C +from dataclasses import FrozenInstanceError +from itertools import product +import math +import unittest +from gecode_optimize import (ApiError,Backend,Guarantee,Library,Model,Options, + RegularTransition as Edge,Session,Termination as T,VariableType as V,load_library) +from gecode_optimize import binding as b + + +def language(length,initial,edges,finals): + # Enumerate paths first, independently of a proposed assignment's transition lookup. + frontier={(initial,())} + for _ in range(length): + frontier={(e.to_state,word+(e.symbol,)) for state,word in frontier for e in edges if e.from_state==state} + return {word for state,word in frontier if state in finals} + +class RegularTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.library=load_library();cls.native=cls.library.capabilities(Backend.NATIVE)['available'] + def test_automaton_word_oracle_both_senses_aliases_and_routing(self): + cases=[(2,0,[Edge(0,-1,0),Edge(0,1,1),Edge(1,-1,1),Edge(1,1,0)],[0,0]), + (3,0,[Edge(0,-1,1),Edge(1,1,2)],[2]), + (1,0,[],[0]),(1,0,[],[]), + ((1<<64)-1,(1<<64)-2,[Edge((1<<64)-2,-1,(1<<64)-2)],[(1<<64)-2])] + for states,initial,edges,finals in cases: + for positions in ((),(0,),(0,1),(0,1,0)): + for maximize in (False,True): + with self.subTest(states=states,positions=positions,maximize=maximize),Model(self.library) as m: + variables=[m.add_variable(V.INTEGER,-1,1) for _ in range(max(positions,default=-1)+1)] + word=[variables[i] for i in positions] + m.add_regular(word,states,initial,edges,finals) + coefficients=[2,-3][:len(variables)] + m.set_objective(list(zip(variables,coefficients)),maximize=maximize,offset=-4) + accepted=language(len(word),initial,edges,finals) + candidates=[(sum(c*x for c,x in zip(coefficients,values))-4,values) + for values in product((-1,0,1),repeat=len(variables)) if tuple(values[i] for i in positions) in accepted] + options=Options(backend=Backend.AUTO,guarantee=Guarantee.EXACT) + with m.solve(options) as out: + self.assertEqual(out.termination,(T.OPTIMAL if candidates else T.INFEASIBLE) if self.native else T.UNSUPPORTED) + if candidates and self.native: + best=(max if maximize else min)(value for value,_ in candidates) + self.assertEqual(out.objective,best);self.assertEqual(out.best_bound,best) + actual=tuple(out.value(v) for v in variables) + self.assertIn((best,actual),candidates) + with m.solve(Options(backend=Backend.HIGHS)) as out: + self.assertEqual(out.termination,T.UNSUPPORTED);self.assertFalse(out.has_solution) + def test_mixed_globals_rows_removal_and_historical_result(self): + edges=[Edge(0,-1,0),Edge(0,1,1),Edge(1,-1,1),Edge(1,1,0)] + for maximize,expected in ((False,7),(True,13)): + with Model(self.library) as m: + x,y,z=[m.add_variable(V.INTEGER,-1,1) for _ in range(3)] + regular=m.add_regular([x,y,z],2,0,edges,[0,0],name='even Ï€') + m.add_all_different([x,y]);m.add_row([(x,1),(y,1),(z,1)],lower=-1) + m.set_objective([(x,2),(y,-1),(z,3)],maximize=maximize,offset=7) + before=m.identity[1];m.set_name(regular,'renamed');self.assertEqual(m.identity[1],before+1) + with self.assertRaises(ApiError):m.remove(z) + result=m.solve(Options(backend=Backend.NATIVE,guarantee=Guarantee.EXACT)) + with Session(self.library) as session,session.solve(m) as unsupported:self.assertEqual(unsupported.termination,T.UNSUPPORTED) + m.remove(regular) + with self.assertRaises(ApiError):m.remove(regular) + with result: + if self.native: + self.assertEqual(result.termination,T.OPTIMAL);self.assertEqual(result.objective,expected) + self.assertEqual((result.value(x),result.value(y),result.value(z)),(-1,1,1) if not maximize else (1,-1,1)) + else:self.assertEqual(result.termination,T.UNSUPPORTED) + def test_atomic_invalid_model_data_and_python_integer_checks(self): + with Model(self.library) as m,Model(self.library) as other: + x=m.add_variable(V.INTEGER,-1,1);foreign=other.add_variable(V.INTEGER,-1,1) + dead=m.add_variable(V.INTEGER,-1,1);m.remove(dead);continuous=m.add_variable() + valid=[Edge(0,-1,1)] + invalid=[([foreign],2,0,valid,[1]),([dead],2,0,valid,[1]),([continuous],2,0,valid,[1]), + ([x],0,0,[],[]),([x],2,2,[],[]),([x],2,0,valid,[2]), + ([x],2,0,[Edge(2,-1,1)],[1]),([x],2,0,[Edge(0,-1,2)],[1]), + ([x],2,0,valid+valid,[1]),([x],2,0,valid+[Edge(0,-1,0)],[1]), + ([x],2,0,[Edge(0,(1<<53)+1,1)],[1])] + revision=m.identity[1] + for args in invalid: + with self.assertRaises(ApiError):m.add_regular(*args) + self.assertEqual(m.identity[1],revision) + for value in (True,0.5,'1'): + with self.assertRaises(TypeError):m.add_regular([x],value,0,valid,[1]) + with self.assertRaises(TypeError):m.add_regular([x],2,0,[Edge(0,value,1)],[1]) + for value in (-1,1<<64): + with self.assertRaises(OverflowError):m.add_regular([x],value,0,valid,[1]) + with self.assertRaises(TypeError):m.add_regular([x],2,0,[(0,-1,1)],[1]) + with self.assertRaises(FrozenInstanceError):valid[0].symbol=1 + with self.assertRaises(ValueError):m.add_regular([x],2,0,valid,[1],name='bad\0name') + another=Library(self.library.path) + with Model(another) as foreign_library: + with self.assertRaises(ValueError):foreign_library.add_regular([x],2,0,valid,[1]) + g=m.add_regular([x],2,0,valid,[1]);self.assertEqual(m.identity[1],revision+1);m.remove(g) + def test_c_layout_size_reserved_counts_and_no_partial_mutation(self): + with Model(self.library) as m: + x=m.add_variable(V.INTEGER,-1,1);ids=(b._Id*1)(x._id());edges=(b._RegularTransition*2)() + for e in edges:e.struct_size=C.sizeof(e);e.from_state=0;e.symbol=-1;e.to_state=1 + edges[1].symbol=1;finals=(b.U64*1)(1);revision=m.identity[1] + def call(variables=ids,n=1,transitions=edges,count=2,size=C.sizeof(b._RegularTransition),states=2,final=finals,nfinal=1,name=b'ok'): + out=b._Id(99,99,99,99) + try:self.library.call('model_add_regular',m._open(),variables,n,states,0,transitions,count,size,final,nfinal,name,C.byref(out)) + finally: + self.assertEqual(bytes(out),bytes(b._Id()));self.assertEqual(m.identity[1],revision) + for kwargs in ({'size':C.sizeof(b._RegularTransition)-1},{'variables':None}, {'n':(1<<64)-1}, + {'transitions':None},{'count':(1<<64)-1},{'final':None},{'nfinal':(1<<64)-1},{'name':b'\xc0\x80'}): + with self.assertRaises(ApiError) as error:call(**kwargs) + self.assertEqual(error.exception.code,1) + for field,value in (('struct_size',0),('reserved',1)): + original=getattr(edges[1],field);setattr(edges[1],field,value) + with self.assertRaises(ApiError) as error:call() + self.assertEqual(error.exception.code,1);setattr(edges[1],field,original) + edges[1].to_state=2 + with self.assertRaises(ApiError) as error:call() + self.assertEqual(error.exception.code,3) + # No edge arrays are required for an empty deterministic language. + g=m.add_regular([],1,0,[],[0]);m.remove(g) + def test_exact_symbol_endpoints_and_semicontinuous_scope(self): + with Model(self.library) as m: + x=m.add_variable(V.INTEGER,-1,1) + for symbol in (-(1<<53),1<<53): + g=m.add_regular([x],1,0,[Edge(0,symbol,0)],[0]);m.remove(g) + with Model(self.library) as m: + x=m.add_variable(V.SEMI_INTEGER,2,3);m.add_regular([x],1,0,[Edge(0,0,0),Edge(0,3,0)],[0]);m.set_objective([(x,1)],maximize=True,offset=-2) + with m.solve(Options(backend=Backend.AUTO,guarantee=Guarantee.EXACT)) as out: + self.assertEqual(out.termination,T.OPTIMAL if self.native else T.UNSUPPORTED) + if self.native:self.assertEqual(out.objective,1);self.assertEqual(out.value(x),3) + +if __name__=='__main__':unittest.main() diff --git a/python/tests/test_runtime.py b/python/tests/test_runtime.py new file mode 100644 index 0000000000..5d5f7b3d0e --- /dev/null +++ b/python/tests/test_runtime.py @@ -0,0 +1,53 @@ +"""Packaged-library selection is independent of the process working directory.""" +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from gecode_optimize import _runtime + + +class RuntimeTest(unittest.TestCase): + def test_absent_bundle_preserves_external_library_workflow(self): + with tempfile.TemporaryDirectory() as temporary: + package = Path(temporary) / "package" + package.mkdir() + (Path(temporary) / "libgecodeoptimize_c.dylib").touch() + with patch.object(_runtime, "__file__", str(package / "_runtime.py")): + self.assertIsNone(_runtime.bundled_library_path()) + + def test_exact_platform_entry_and_incomplete_bundle(self): + for platform, name in (("darwin", "libgecodeoptimize_c.dylib"), + ("linux", "libgecodeoptimize_c.so"), + ("win32", "gecodeoptimize_c.dll")): + with self.subTest(platform=platform), tempfile.TemporaryDirectory() as temporary: + package = Path(temporary) + native = package / "_native" + native.mkdir() + with patch.object(_runtime, "__file__", str(package / "_runtime.py")), \ + patch.object(_runtime.sys, "platform", platform): + with self.assertRaisesRegex(RuntimeError, "incomplete"): + _runtime.bundled_library_path() + entry = native / name + entry.touch() + self.assertEqual(_runtime.bundled_library_path(), str(entry.resolve())) + + def test_bundle_cannot_redirect_to_an_external_file(self): + with tempfile.TemporaryDirectory() as temporary: + package = Path(temporary) + native = package / "_native" + native.mkdir() + outside = package / "external.dylib" + outside.touch() + try: + (native / "libgecodeoptimize_c.dylib").symlink_to(outside) + except OSError: + self.skipTest("creating symlinks is unavailable on this host") + with patch.object(_runtime, "__file__", str(package / "_runtime.py")), \ + patch.object(_runtime.sys, "platform", "darwin"): + with self.assertRaisesRegex(RuntimeError, "outside"): + _runtime.bundled_library_path() + + +if __name__ == "__main__": + unittest.main() diff --git a/python/tests/test_scenarios.py b/python/tests/test_scenarios.py new file mode 100644 index 0000000000..58e04f5164 --- /dev/null +++ b/python/tests/test_scenarios.py @@ -0,0 +1,231 @@ +"""Scenario boundaries, independent hand/exhaustive oracles, and owning history.""" +import ctypes as C +from dataclasses import FrozenInstanceError +import math +import unittest +from gecode_optimize import (ApiError, Backend, Cancellation, Guarantee, Library, Model, + Options, Result, Row, Session, Termination as T, Variable, VariableType as V, load_library, + ScenarioDefinition as D, ScenarioVariableBounds as VB, ScenarioRowBounds as RB, + ScenarioOptions as SO, ScenarioReuse as Reuse, ScenarioBatchCompletion as Completion, + ScenarioRunState as State, ScenarioId) +from gecode_optimize import binding as b + +class ScenarioTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.library=load_library() + def model(self): + m=Model(self.library);x=m.add_variable(V.INTEGER,0,8);y=m.add_variable(V.INTEGER,0,8) + dead=m.add_variable();m.remove(dead);gone=m.add_row();m.remove(gone) + row=m.add_row([(x,1),(y,1)],lower=4);m.set_objective([(x,2),(y,3)],offset=7) + return m,x,y,row,dead,gone + def available(self,backend):return self.library.capabilities(backend)['available'] + def test_analytic_history_and_cold_reuse(self): + for backend in (Backend.HIGHS,Backend.NATIVE): + for reuse in (Reuse.AUTOMATIC,Reuse.COLD): + m,x,y,row,dead,gone=self.model() + defs=[D('base'),D('cap',variable_bounds=[VB(x,upper=1)]), + D('cost Ï€',[(x,4),(y,1)],-10),D('rhs',row_bounds=[RB(row,lower=6)]), + D('infeasible',variable_bounds=[VB(x,upper=1),VB(y,upper=1)])] + original=m.identity + with m,m.solve_scenarios(defs,SO(solve=Options(backend=backend,guarantee=Guarantee.EXACT if backend==Backend.NATIVE else Guarantee.NUMERICAL),reuse=reuse)) as batch: + ids=[batch.scenario(i) for i in range(5)];info=batch.info + self.assertEqual((info.model_id,info.revision),original);self.assertNotEqual(info.batch_id,original[0]) + self.assertEqual(m.identity,original) + saved=tuple(batch.definition(id) for id in ids);self.assertEqual(saved,tuple(defs)) + with self.assertRaises(FrozenInstanceError):info.work=1 + with self.assertRaises(FrozenInstanceError):saved[0].name='changed' + private=batch.map(x);private_row=batch.map(row) + self.assertEqual(private.model_id,info.batch_id);self.assertEqual(private_row.slot,row.slot) + for bad in (dead,gone,Variable(x.model_id+1,x.slot,self.library)): + with self.assertRaises(ApiError):batch.map(bad) + with self.assertRaises(TypeError):batch.map(ids[0]) + if self.available(backend): + self.assertTrue(info.all_resolved);self.assertEqual(info.completion,Completion.COMPLETE) + self.assertIsNone(info.stop_reason);self.assertEqual(info.attempted,5);self.assertEqual(info.resolved,5) + for id,obj,values in zip(ids,[15,18,-6,19],[(4,0),(1,3),(0,4),(6,0)]): + outcome=batch.outcome(id);self.assertEqual(outcome.result.termination,T.OPTIMAL) + self.assertEqual(outcome.result.revision,id.index+1);self.assertTrue(outcome.check.candidate_examined) + self.assertTrue(outcome.check.validation.valid);self.assertTrue(outcome.check.objective_matches) + self.assertEqual((batch.value(id,x),batch.value(id,y)),values) + with batch.copy_result(id) as child: + self.assertEqual(child.objective,obj);self.assertEqual(child.value(private),values[0]) + with self.assertRaises(ApiError):child.value(x) + bad=batch.outcome(ids[-1]);self.assertEqual(bad.result.termination,T.INFEASIBLE) + self.assertFalse(bad.check.candidate_examined);self.assertIsNone(bad.check.validation) + with self.assertRaises(ApiError):batch.value(ids[-1],x) + child=batch.copy_result(ids[0]);metadata=batch.outcome(ids[0]) + if backend==Backend.HIGHS and reuse==Reuse.AUTOMATIC: + self.assertEqual(info.reuse_statistics.solve_calls,5) + self.assertGreater(info.reuse_statistics.incremental_updates,0) + else:self.assertEqual(info.reuse_statistics.solve_calls,0) + else: + self.assertEqual(info.stop_reason,T.UNSUPPORTED);self.assertFalse(info.all_resolved) + self.assertEqual(batch.outcome(ids[0]).result.termination,T.UNSUPPORTED) + self.assertEqual(batch.outcome(ids[1]).state,State.NOT_STARTED) + self.assertIsNone(batch.outcome(ids[1]).result);self.assertIsNone(batch.check(ids[1])) + with self.assertRaises(ApiError):batch.copy_result(ids[1]) + child=batch.copy_result(ids[0]);metadata=batch.outcome(ids[0]) + m.set_bounds(row,7,math.inf);m.set_objective([(y,3)]);m.remove(row);m.remove(x) + self.assertEqual(batch.definition(ids[1]),defs[1]);self.assertEqual(batch.map(x),private) + with m.solve_scenarios([D()]) as later: + if later.info.batch_id is not None: + with self.assertRaises(ApiError):later.outcome(ids[0]) + with child: + self.assertEqual(child.info['model_id'],private.model_id) + if self.available(backend):self.assertEqual(child.value(private),4);self.assertEqual(child.objective,15) + self.assertEqual(saved[2].name,'cost Ï€');self.assertEqual(metadata.scenario,ids[0]) + with self.assertRaises(RuntimeError):batch.outcome(ids[0]) + def test_small_exhaustive_min_max(self): + for backend in (Backend.HIGHS,Backend.NATIVE): + if not self.available(backend):continue + for maximize in (False,True): + with Model(self.library) as m: + x=m.add_variable(V.INTEGER,-2,2);y=m.add_variable(V.INTEGER,-2,2) + row=m.add_row([(x,2),(y,-1)],lower=-2,upper=2);m.set_objective([(x,1),(y,2)],maximize=maximize,offset=5) + defs=[];expected=[] + for lower in (-2,0,1): + for c in (-3,0,4): + defs.append(D(objective_coefficients=[(x,c)],objective_offset=-7,variable_bounds=[VB(y,lower=lower)])) + choices=[(c*a+2*d-7,a,d) for a in range(-2,3) for d in range(lower,3) if -2<=2*a-d<=2] + expected.append((max if maximize else min)(v[0] for v in choices)) + with m.solve_scenarios(defs,SO(solve=Options(backend=backend))) as result: + self.assertTrue(result.info.all_resolved) + for i,obj in enumerate(expected): + with result.copy_result(result.scenario(i)) as child:self.assertEqual(child.objective,obj) + def test_continuous_recourse_and_unbounded_status(self): + with Model(self.library) as m: + x=m.add_variable(V.CONTINUOUS,0,math.inf);y=m.add_variable(V.CONTINUOUS,0,10) + row=m.add_row([(x,1),(y,1)],lower=3);m.set_objective([(x,2),(y,3)],offset=7) + defs=[D(),D(variable_bounds=[VB(x,upper=1)]), + D(objective_coefficients=[(x,4)]),D(row_bounds=[RB(row,lower=4)])] + with m.solve_scenarios(defs,SO(solve=Options(backend=Backend.HIGHS))) as batch: + if self.available(Backend.HIGHS): + self.assertTrue(batch.info.all_resolved) + for i,objective in enumerate((13,15,16,15)): + with batch.copy_result(batch.scenario(i)) as child:self.assertAlmostEqual(child.objective,objective) + else:self.assertEqual(batch.info.stop_reason,T.UNSUPPORTED) + with m.solve_scenarios([D()],SO(solve=Options(backend=Backend.NATIVE))) as batch: + self.assertEqual(batch.info.stop_reason,T.UNSUPPORTED) + with Model(self.library) as m: + x=m.add_variable(V.CONTINUOUS,0,math.inf);m.set_objective([(x,-1)]) + with m.solve_scenarios([D(),D(variable_bounds=[VB(x,upper=2)])],SO(solve=Options(backend=Backend.HIGHS))) as batch: + if self.available(Backend.HIGHS): + self.assertTrue(batch.info.all_resolved) + first=batch.outcome(batch.scenario(0));self.assertEqual(first.result.termination,T.UNBOUNDED) + with batch.copy_result(batch.scenario(0)) as unbounded: + # Unbounded can retain a finite feasible point; that is + # independent of the status and not an attained optimum. + if unbounded.has_solution: + self.assertTrue(first.check.candidate_examined);self.assertTrue(first.check.validation.valid) + self.assertEqual(unbounded.objective,-batch.value(batch.scenario(0),x)) + else:self.assertIsNone(unbounded.objective) + self.assertIsNone(unbounded.relative_gap) + with batch.copy_result(batch.scenario(1)) as finite:self.assertEqual(finite.objective,-2) + else:self.assertEqual(batch.info.stop_reason,T.UNSUPPORTED) + def test_empty_and_limits(self): + m,x,y,row,*_=self.model() + with m,Cancellation(self.library) as cancel: + with m.solve_scenarios([]) as empty: + self.assertTrue(empty.info.all_resolved);self.assertEqual(empty.info.attempted,0) + with self.assertRaises(ApiError):empty.scenario(0) + cancel.cancel() + cases=[(SO(solve=Options(time_limit_seconds=0)),T.TIME_LIMIT), + (SO(solve=Options(cancellation=cancel)),T.CANCELLED), + (SO(solve=Options(time_limit_seconds=0),max_scenarios=0),T.TIME_LIMIT), + (SO(solve=Options(cancellation=cancel),max_scenarios=0),T.CANCELLED), + (SO(solve=Options(node_limit=0)),T.NODE_LIMIT), + (SO(solve=Options(node_limit=1)),T.UNSUPPORTED), + (SO(max_scenarios=1),T.MEMORY_LIMIT),(SO(max_patch_entries=0),T.MEMORY_LIMIT), + (SO(max_saved_value_slots=1),T.MEMORY_LIMIT),(SO(max_work=0),T.ITERATION_LIMIT)] + for options,reason in cases: + with m.solve_scenarios([D(objective_coefficients=[(x,2)]),D()],options) as result: + self.assertEqual(result.info.stop_reason,reason);self.assertFalse(result.info.all_resolved) + self.assertEqual(result.info.attempted,0);self.assertIsNone(result.info.batch_id) + self.assertTrue(result.message) + if self.available(Backend.NATIVE): + with m.solve_scenarios([D()],SO(solve=Options(backend=Backend.NATIVE,node_limit=100000))) as result: + self.assertTrue(result.info.all_resolved) + def test_rejected_semantics_and_kinds(self): + m,x,y,row,dead,gone=self.model() + with m: + bad=[D(objective_coefficients=[(x,1),(x,2)]),D(objective_coefficients=[(dead,1)]), + D(objective_coefficients=[(Variable(x.model_id+10,x.slot,self.library),1)]), + D(objective_coefficients=[(x,math.nan)]),D(objective_offset=math.inf), + D(variable_bounds=[VB(x,lower=9)]),D(variable_bounds=[VB(x,lower=math.nan)]), + D(variable_bounds=[VB(x,lower=1),VB(x,upper=2)]),D(row_bounds=[RB(gone,lower=1)]), + D(row_bounds=[RB(row,upper=1),RB(row,lower=0)])] + for definition in bad: + with m.solve_scenarios([D(),definition]) as result: + self.assertEqual(result.info.completion,Completion.REJECTED);self.assertEqual(result.info.stop_reason,T.INVALID_MODEL) + self.assertEqual(result.info.offending_scenario,1);self.assertEqual(result.info.attempted,0) + for options in (SO(solve=Options(primal_start=[(x,4),(y,0)])),SO(solve=Options(guarantee=Guarantee.CERTIFIED))): + with m.solve_scenarios([D()],options) as result:self.assertEqual(result.info.stop_reason,T.UNSUPPORTED) + with m.solve_scenarios([D()]) as result: + id=result.scenario(0) + for wrong in (ScenarioId(id.batch_id+1,0,self.library),ScenarioId(id.batch_id,9,self.library)): + for fn in (result.outcome,result.check,result.definition,result.copy_result): + with self.assertRaises(ApiError):fn(wrong) + with self.assertRaises(TypeError):result.outcome(x) + with Session(self.library) as session: + info=b._ScenarioInfo() + with self.assertRaises(ApiError):self.library.call('scenario_batch_info',session._open(),C.byref(info),C.sizeof(info)) + for kind in ('semi','indicator','global','deleted_global'): + with Model(self.library) as m: + x=m.add_variable(V.SEMI_INTEGER if kind=='semi' else V.INTEGER,1,2) + if kind=='indicator': + gate=m.add_variable(V.BINARY);m.add_indicator(gate,True,[(x,1)],lower=1) + if kind in ('global','deleted_global'): + g=m.add_all_different([x]) + if kind=='deleted_global':m.remove(g) + with m.solve_scenarios([D()]) as result:self.assertEqual(result.info.stop_reason,T.UNSUPPORTED) + def test_record_sizes_arrays_presence_and_copy(self): + m,x,y,row,*_=self.model() + with m: + base=SO()._marshal(self.library);definition=b._scenario_definitions([D('saved',[(x,0)],0,[VB(x,upper=7)],[RB(row,lower=2)])],self.library) + for mutate in (lambda o:setattr(o,'struct_size',C.sizeof(o)-1),lambda o:setattr(o,'reserved',1), + lambda o:setattr(o,'reserved_flags',1),lambda o:setattr(o,'reuse',2), + lambda o:setattr(o.solve,'struct_size',0),lambda o:setattr(o.solve,'reserved',1)): + native=SO()._marshal(self.library);mutate(native);out=b.U64(99) + with self.assertRaises(ApiError):self.library.call('solve_scenarios',m._open(),definition,1,C.sizeof(b._ScenarioDefinition),C.byref(native),C.byref(out)) + self.assertEqual(out.value,0) + for key,val in (('struct_size',1),('reserved',1)): + data=b._scenario_definitions([D()],self.library);setattr(data[0],key,val);out=b.U64(99) + with self.assertRaises(ApiError):self.library.call('solve_scenarios',m._open(),data,1,C.sizeof(b._ScenarioDefinition),C.byref(base),C.byref(out)) + self.assertEqual(out.value,0) + for mutate in (lambda d:setattr(d[0].objective_offset,'present',2),lambda d:setattr(d[0].objective_offset,'reserved',1), + lambda d:setattr(d[0].variable_bounds[0],'struct_size',0),lambda d:setattr(d[0].variable_bounds[0],'has_upper',2), + lambda d:setattr(d[0].row_bounds[0],'reserved',1)): + data=b._scenario_definitions([D(variable_bounds=[VB(x,upper=7)],row_bounds=[RB(row,lower=2)])],self.library);mutate(data);out=b.U64() + with self.assertRaises(ApiError):self.library.call('solve_scenarios',m._open(),data,1,C.sizeof(b._ScenarioDefinition),C.byref(base),C.byref(out)) + token=b.U64();self.library.call('solve_scenarios',m._open(),definition,1,C.sizeof(b._ScenarioDefinition),C.byref(base),C.byref(token)) + definition[0].objective_offset.value=99;definition[0].variable_bounds[0].upper=1 + with b.ScenarioBatchResult(self.library,token.value) as result: + id=result.scenario(0);copied=result.definition(id);self.assertEqual(copied.objective_offset,0);self.assertEqual(copied.variable_bounds[0].upper,7) + info=b._ScenarioInfo() + with self.assertRaises(ApiError):self.library.call('scenario_batch_info',result._open(),C.byref(info),C.sizeof(info)-1) + needed=b.U64();raw=(b._ScenarioBounds*1)();raw[0].upper=123 + with self.assertRaises(ApiError):self.library.call('scenario_batch_bounds',result._open(),id._id(),1,raw,C.sizeof(b._ScenarioBounds)-1,1,C.byref(needed)) + self.assertEqual(raw[0].upper,123) + self.library.call('scenario_batch_bounds',result._open(),id._id(),1,raw,C.sizeof(b._ScenarioBounds),1,C.byref(needed)) + self.assertEqual(raw[0].reserved,0);self.assertEqual(raw[0].has_lower,0);self.assertEqual(raw[0].lower,0);self.assertEqual(raw[0].upper,7) + with self.assertRaises(ApiError):self.library.call('scenario_batch_text',result._open(),id._id(),2,None,0,C.byref(needed)) + def test_python_type_library_and_immutability(self): + m,x,*_=self.model() + with m: + terms=[(x,1)];bounds=[VB(x,upper=3)];definition=D(objective_coefficients=terms,variable_bounds=bounds) + terms.clear();bounds.clear();self.assertEqual(len(definition.objective_coefficients),1);self.assertEqual(len(definition.variable_bounds),1) + for definitions in ([object()],[D(name='bad\0name')],[D(variable_bounds=[object()])],[D(row_bounds=[VB(x,upper=1)])]): + with self.assertRaises((TypeError,ValueError)):m.solve_scenarios(definitions) + for option in (Options(),object()): + with self.assertRaises(TypeError):m.solve_scenarios([],option) + for value in (True,0.5): + with self.assertRaises(TypeError):m.solve_scenarios([],SO(max_work=value)) + other=Library(self.library.path) + with Model(other) as foreign: + alien=foreign.add_variable() + with self.assertRaises(ValueError):m.solve_scenarios([D(objective_coefficients=[(alien,1)])]) + with m.solve_scenarios([D()]) as result: + with self.assertRaises(ValueError):result.outcome(ScenarioId(result.info.batch_id,0,other)) + +if __name__=='__main__':unittest.main() diff --git a/python/tests/test_workflows.py b/python/tests/test_workflows.py new file mode 100644 index 0000000000..e7b9e94f14 --- /dev/null +++ b/python/tests/test_workflows.py @@ -0,0 +1,291 @@ +"""C ABI-backed pool/repair conformance; no timing/performance claims.""" +import concurrent.futures +from dataclasses import FrozenInstanceError +import math +import threading +import time +import unittest + +from gecode_optimize import (ApiError, Backend, Cancellation, Guarantee, Model, Options, + PoolCompletion, PoolOptions, RelaxationSelection, RelaxationSide, RepairOptions, + Termination, VariableType, load_library) + + +class Workflows(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.lib=load_library() + cls.highs=cls.lib.capabilities(Backend.HIGHS)["available"] + cls.native=cls.lib.capabilities(Backend.NATIVE)["available"] + + def test_ranked_projection_recourse_and_independent_result_lifetime(self): + with Model(self.lib) as model: + x=model.add_variable(VariableType.INTEGER,-1,1) + y=model.add_variable(upper=4) + dead=model.add_variable();model.remove(dead) + model.add_row({x:1,y:1},lower=2) + model.set_objective({x:2,y:1},offset=5) + identity=model.identity + with model.solve_pool(PoolOptions(Options(backend=Backend.HIGHS),5,[x])) as pool: + self.assertEqual((pool.info["model_id"],pool.info["revision"]),identity) + self.assertEqual(pool.projection,(x,)) + if not self.highs: + self.assertEqual(pool.termination,Termination.UNSUPPORTED) + self.assertEqual(pool.info["entry_count"],0) + self.assertFalse(pool.exhausted) + with self.assertRaises(ApiError): pool.entry_result(0) + return + self.assertEqual(pool.completion,PoolCompletion.EXHAUSTED) + self.assertEqual(pool.ranked_prefix,3) + self.assertEqual(pool.info["attempt_count"],4) + for i in range(3): + record=pool.entry(i) + self.assertEqual(record.projection_values,(i-1,)) + self.assertTrue(record.rank_established) + with self.assertRaises(FrozenInstanceError): record.rank_established=False + with pool.entry_result(i) as result: + self.assertEqual(result.termination,Termination.UNKNOWN) + self.assertIsNone(result.best_bound) + self.assertAlmostEqual(result.value(y),3-i) + self.assertAlmostEqual(result.objective,6+i) + self.assertEqual(result.values[dead.slot],{"active":False,"present":False,"value":None}) + self.assertEqual(pool.attempt(3).termination,Termination.INFEASIBLE) + self.assertFalse(pool.attempt(3).candidate_accepted) + retained=pool.entry_result(0) + model.set_bounds(x,1,1) + with retained: + self.assertEqual(retained.info["model_id"],identity[0]) + self.assertAlmostEqual(retained.value(x),-1) + self.assertAlmostEqual(retained.objective,6) + with self.assertRaises(RuntimeError): pool.entry(0) + + def test_pool_requested_limit_maximize_ties_and_explicit_native(self): + for native in (False,True): + available=self.native if native else self.highs + options=Options(backend=Backend.NATIVE if native else Backend.HIGHS, + guarantee=Guarantee.EXACT if native else Guarantee.NUMERICAL) + with self.subTest(native=native),Model(self.lib) as model: + x=model.add_variable(VariableType.INTEGER,-2,0) + y=model.add_variable(VariableType.BINARY) + model.set_objective({x:2},maximize=True,offset=-3) + with model.solve_pool(PoolOptions(options,2,[x,y])) as pool: + self.assertEqual(pool.termination,Termination.SOLUTION_LIMIT if available else Termination.UNSUPPORTED) + self.assertFalse(pool.exhausted) + if available: + self.assertEqual(pool.ranked_prefix,2) + self.assertNotEqual(pool.entry(0).projection_values,pool.entry(1).projection_values) + for i in (0,1): + with pool.entry_result(i) as result: self.assertEqual(result.objective,-3) + + def test_empty_models_zero_items_and_projection_presence(self): + with Model(self.lib) as model: + model.set_objective(offset=-7) + with model.solve_pool(PoolOptions(Options(backend=Backend.HIGHS),5,[])) as pool: + self.assertEqual(pool.projection,()) + if self.highs: + self.assertTrue(pool.exhausted) + self.assertEqual(pool.info["entry_count"],1) + self.assertEqual(pool.entry(0).projection_values,()) + with pool.entry_result(0) as result: + self.assertEqual(result.values,[]) + self.assertEqual(result.objective,-7) + else: self.assertEqual(pool.termination,Termination.UNSUPPORTED) + with model.relax_feasibility(RepairOptions(Options(backend=Backend.HIGHS),(),True)) as repair: + self.assertEqual(repair.variable_map,()) + self.assertEqual(repair.original_values,()) + self.assertEqual(repair.info["item_count"],0) + if self.highs: + self.assertTrue(repair.has_repair) + self.assertTrue(repair.original_validation.valid) + self.assertEqual(repair.weighted_violation,0) + self.assertEqual(repair.original_objective,-7) + else: self.assertEqual(repair.termination,Termination.UNSUPPORTED) + x=model.add_variable(VariableType.INTEGER,0,1) + with model.solve_pool(PoolOptions(projection=[])) as pool: + self.assertEqual(pool.termination,Termination.UNSUPPORTED) + with model.solve_pool(PoolOptions(max_solutions=0)) as pool: + self.assertEqual(pool.termination,Termination.INVALID_MODEL) + + def test_repair_original_private_mapping_stages_and_history(self): + with Model(self.lib) as model: + dead=model.add_variable();model.remove(dead) + x=model.add_variable(VariableType.INTEGER,0,2) + y=model.add_variable(upper=1) + demand=model.add_row({x:1,y:1},lower=4,name="demand λ") + model.set_objective({x:2,y:1},offset=-9) + identity=model.identity + with model.relax_feasibility(RepairOptions(Options(backend=Backend.HIGHS), + [RelaxationSelection(demand,RelaxationSide.LOWER,2)],True)) as repair: + self.assertEqual((repair.info["source_model_id"],repair.info["source_revision"]),identity) + mapping=repair.variable_map + self.assertFalse(mapping[dead.slot].active) + self.assertEqual(mapping[x.slot].source,x) + self.assertNotEqual(mapping[x.slot].private.model_id,x.model_id) + item=repair.item(0) + self.assertEqual(item.source,demand) + self.assertEqual(item.name,"demand λ") + self.assertEqual(item.slack.model_id,mapping[x.slot].private.model_id) + if not self.highs: + self.assertEqual(repair.termination,Termination.UNSUPPORTED) + self.assertFalse(repair.has_repair) + self.assertIsNone(item.activity) + self.assertFalse(repair.original_values[x.slot]["present"]) + with self.assertRaises(ApiError) as error: repair.final_result() + self.assertEqual(error.exception.code,7) + return + self.assertEqual(repair.termination,Termination.OPTIMAL) + self.assertTrue(repair.has_repair) + self.assertTrue(repair.info["minimum_violation_established"]) + self.assertTrue(repair.info["original_objective_optimized"]) + self.assertAlmostEqual(repair.minimum_weighted_violation,2) + self.assertAlmostEqual(repair.weighted_violation,2) + self.assertAlmostEqual(repair.original_objective,-4) + self.assertAlmostEqual(item.violation,1) + self.assertAlmostEqual(item.activity,3) + self.assertAlmostEqual(item.weighted_violation,2) + self.assertFalse(repair.original_validation.valid) + self.assertTrue(repair.original_validation.model_valid) + self.assertAlmostEqual(repair.original_validation.max_row_violation,1) + self.assertAlmostEqual(repair.original_value(x),2) + self.assertEqual(repair.original_values[dead.slot],{"active":False,"present":False,"value":None}) + self.assertEqual(repair.info["completed_stages"],2) + self.assertTrue(repair.stage(0).completed) + self.assertAlmostEqual(repair.stage(0).retention_bound,2) + self.assertEqual(len(repair.objective_values),2) + self.assertIsNotNone(repair.violation_lock) + with self.assertRaises(ApiError): repair.original_value(mapping[x.slot].private) + with self.assertRaises(ApiError): repair.original_value(dead) + retained=repair.final_result();stage=repair.stage_result(0) + model.set_bounds(x,0,1) + with retained,stage: + self.assertEqual(retained.termination,Termination.UNKNOWN) + self.assertIsNone(retained.best_bound) + self.assertAlmostEqual(retained.value(mapping[x.slot].private),2) + self.assertAlmostEqual(retained.value(item.slack),1) + with self.assertRaises(ApiError): retained.value(x) + self.assertEqual(stage.info["model_id"],mapping[x.slot].private.model_id) + with self.assertRaises(RuntimeError): repair.original_value(x) + + def test_repair_binary_bounds_remain_intrinsic_and_semis_are_explicit(self): + with Model(self.lib) as model: + x=model.add_variable(VariableType.BINARY,.25,.75) + model.set_objective({x:1}) + selections=[RelaxationSelection(x,side) for side in RelaxationSide] + with model.relax_feasibility(RepairOptions(Options(backend=Backend.HIGHS),selections,True)) as repair: + if self.highs: + self.assertTrue(repair.has_repair) + self.assertAlmostEqual(repair.minimum_weighted_violation,.25) + self.assertAlmostEqual(repair.original_value(x),0) + self.assertAlmostEqual(repair.original_validation.max_bound_violation,.25) + else: self.assertEqual(repair.termination,Termination.UNSUPPORTED) + with Model(self.lib) as model: + semi=model.add_variable(VariableType.SEMI_INTEGER,2,4) + with model.relax_feasibility(RepairOptions(selections=[RelaxationSelection(semi)])) as repair: + self.assertEqual(repair.termination,Termination.UNSUPPORTED) + self.assertFalse(repair.has_repair) + + def test_invalid_inputs_status_and_ownership(self): + with Model(self.lib) as model,Model(self.lib) as foreign: + x=model.add_variable(VariableType.INTEGER,0,1) + other=foreign.add_variable(VariableType.INTEGER,0,1) + dead=model.add_variable(VariableType.INTEGER,0,1);model.remove(dead) + row=model.add_row({x:1},lower=1) + identity=model.identity + for projection in ([other],[dead],[x,x]): + with model.solve_pool(PoolOptions(projection=projection)) as pool: + self.assertEqual(pool.termination,Termination.INVALID_MODEL) + with self.assertRaises(TypeError): model.solve_pool(PoolOptions(projection=[row])) + with self.assertRaises(OverflowError): model.solve_pool(PoolOptions(max_solutions=2**64)) + for source in (other,dead): + with model.relax_feasibility(RepairOptions(selections=[RelaxationSelection(source)])) as repair: + self.assertEqual(repair.termination,Termination.INVALID_MODEL) + for penalty in (0,-1,math.nan,math.inf): + with self.assertRaises(ApiError): + model.relax_feasibility(RepairOptions(selections=[RelaxationSelection(row,penalty=penalty)])) + with self.assertRaises(ValueError): model.relax_feasibility(RepairOptions(selections=[RelaxationSelection(row,9)])) + with self.assertRaises(TypeError): model.relax_feasibility(RepairOptions(optimize_original_objective=1)) + with self.assertRaises(TypeError): model.relax_feasibility(Options()) + with self.assertRaises(TypeError): model.solve_pool(Options()) + self.assertEqual(model.identity,identity) + + def test_repair_failed_refinement_preserves_only_established_evidence(self): + with Model(self.lib) as model: + x=model.add_variable() + model.set_objective({x:1},maximize=True) + with model.relax_feasibility(RepairOptions(Options(backend=Backend.HIGHS),(),True)) as repair: + if not self.highs: + self.assertEqual(repair.termination,Termination.UNSUPPORTED) + else: + self.assertEqual(repair.termination,Termination.UNBOUNDED) + self.assertTrue(repair.info["minimum_violation_established"]) + self.assertFalse(repair.info["original_objective_optimized"]) + self.assertEqual(repair.info["completed_stages"],1) + self.assertTrue(repair.has_repair) + self.assertEqual(repair.minimum_weighted_violation,0) + self.assertTrue(repair.stage(0).completed) + self.assertFalse(repair.stage(1).completed) + with repair.stage_result(1) as stage: + self.assertEqual(stage.termination,Termination.UNBOUNDED) + with repair.final_result() as retained: + self.assertEqual(retained.termination,Termination.UNKNOWN) + self.assertIsNone(retained.best_bound) + with Model(self.lib) as model: + x=model.add_variable(VariableType.INTEGER,.25,.75) + with model.relax_feasibility(RepairOptions(Options(backend=Backend.HIGHS))) as repair: + self.assertEqual(repair.termination,Termination.INFEASIBLE if self.highs else Termination.UNSUPPORTED) + self.assertFalse(repair.has_repair) + self.assertFalse(repair.info["minimum_violation_established"]) + self.assertIsNone(repair.minimum_weighted_violation) + + def test_cancellation_limits_missing_capabilities_and_no_fallback(self): + with Model(self.lib) as model,Cancellation(self.lib) as token: + x=model.add_variable(VariableType.INTEGER,0,1) + token.cancel() + with model.solve_pool(PoolOptions(Options(cancellation=token))) as pool: + self.assertEqual(pool.termination,Termination.CANCELLED) + self.assertEqual(pool.info["entry_count"],0) + self.assertIsNotNone(pool.message) + with model.relax_feasibility(RepairOptions(Options(cancellation=token))) as repair: + self.assertEqual(repair.termination,Termination.CANCELLED) + self.assertFalse(repair.has_repair) + self.assertEqual(repair.variable_map[0].source,x) + self.assertIsNone(repair.variable_map[0].private) + for method,wrapper in ((model.solve_pool,PoolOptions),(model.relax_feasibility,RepairOptions)): + with method(wrapper(Options(time_limit_seconds=0))) as result: + self.assertEqual(result.termination,Termination.TIME_LIMIT) + with method(wrapper(Options(guarantee=Guarantee.CERTIFIED))) as result: + self.assertEqual(result.termination,Termination.UNSUPPORTED) + with model.solve_pool(PoolOptions(Options(node_limit=1),max_solutions=2)) as pool: + self.assertEqual(pool.termination,Termination.UNSUPPORTED) + + def test_cancel_and_destroy_source_during_pool_call(self): + # Many requested classes prevent a vacuous already-completed oracle. + # A backend-disabled build still checks its owning Unsupported outcome. + model=Model(self.lib) + for _ in range(16): model.add_variable(VariableType.BINARY) + identity=model.identity + with Cancellation(self.lib) as token,concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: + entered=threading.Event() + function=self.lib._functions["pool_solve"] + def observed(*args): + entered.set() + return function(*args) + self.lib._functions["pool_solve"]=observed + try: + future=executor.submit(model.solve_pool,PoolOptions(Options(backend=Backend.HIGHS, + cancellation=token,time_limit_seconds=3),max_solutions=10000)) + self.assertTrue(entered.wait(1)) + time.sleep(.02) + if self.highs: self.assertFalse(future.done(),"fixture completed before testing in-flight destruction") + token.cancel();model.close() + with future.result(timeout=4) as pool: + self.assertEqual((pool.info["model_id"],pool.info["revision"]),identity) + self.assertEqual(pool.termination,Termination.CANCELLED if self.highs else Termination.UNSUPPORTED) + self.assertFalse(pool.exhausted) + finally: + self.lib._functions["pool_solve"]=function + model.close() + + +if __name__=="__main__": + unittest.main() diff --git a/test/flatzinc-capture/capture.cpp b/test/flatzinc-capture/capture.cpp new file mode 100644 index 0000000000..3ad67219ca --- /dev/null +++ b/test/flatzinc-capture/capture.cpp @@ -0,0 +1,147 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace Gecode::FlatZinc; +namespace C=Gecode::FlatZinc::Capture; +namespace { +unsigned checked=0; +std::shared_ptr good(const std::string& text) { + auto r=C::parse_string(text);++checked; + if(r.status!=C::Status::Complete){ + std::cerr<raw_variables.size()==3&&r->raw_domains.size()==1&&r->raw_constraints.size()==2); + assert(variable(*r,C::Type::Integer,1).alias&&variable(*r,C::Type::Integer,1).target.index==0); + assert(variable(*r,C::Type::Integer,2).assigned&&variable(*r,C::Type::Integer,2).value.integer==3); + assert(variable(*r,C::Type::Integer,2,false).alias&&!variable(*r,C::Type::Integer,2,false).assigned); + assert(r->raw_domains[0].arguments[0].reference.index==1&&r->raw_domains[0].arguments[1].set.lower==2); + assert(r->domains.size()==2&&r->coverage.size()==2&&r->coverage[0].kind==C::CoverageKind::AliasEquality); + assert(r->coverage[1].raw_constraint==1&&r->constraints.size()==1); + assert(r->solve.method==C::Method::Minimize&&r->solve.objective.reference.index==1); + assert(r->output.size()==1&&r->output[0].name=="x"); + auto b=good("var bool: b; var bool: c=true; var {false}: d=b; constraint bool_eq(b,c); constraint bool2int(b,1); solve satisfy;"); + assert(variable(*b,C::Type::Boolean,1).value.boolean); + assert(b->raw_domains.size()==1&&b->raw_domains[0].arguments[0].reference.type==C::Type::Boolean); + assert(b->domains.back().arguments[1].set.lower==1&&!b->solve.has_objective); + auto typed=good("int: n=7; bool: p=true; float: f=1.25; set of int: s={2,7}; array[1..1] of var int: a=[n]; array[1..1] of var bool: b=[p]; array[1..1] of var float: c=[f]; array[1..1] of var set of int: d=[s]; solve maximize f;"); + assert(variable(*typed,C::Type::Integer,0).value.integer==7); + assert(variable(*typed,C::Type::Boolean,0).value.boolean); + assert(variable(*typed,C::Type::Float,0).value.floating==1.25); + assert(variable(*typed,C::Type::Set,0).value.set.values.size()==2); + assert(typed->solve.objective.floating==1.25&&typed->raw_variables.size()==4); + for(const auto& literal:std::vector>{{"23",23},{"-23",-23},{"0o10",8},{"010",10},{"08",8},{"0x1a",26}}){ + auto k=good("solve minimize "+literal.first+";");assert(k->raw_variables.empty()&&k->solve.objective.integer==literal.second); + } + auto f=good("var 1.0..2.0: f; var 1.5..3.0: g=f; var set of {1,3}: s; var set of int: t=s; solve maximize g;"); + assert(f->raw_variables.size()==4&&f->raw_domains.size()==2&&f->solve.objective.reference.type==C::Type::Float); + auto a=good("var 0..5: x; array[1..3] of var 1..4: a :: output_array([0..2])=[x,x,3]; constraint custom(a, [x,3], true); solve minimize a[3];"); + assert(a->raw_variables.size()==2&&a->raw_domains.size()==3&&a->raw_constraints[0].arguments[0].elements.size()==3); + assert(a->solve.objective.reference.index==1&&a->output.size()==1); + assert(a->output[0].expression.elements[1].elements[0].reference.index==0); + assert(a->output[0].expression.elements[1].elements[1].reference.index==0); + auto empty=good("array[1..0] of var 1..2: a :: output_array([1..0]); solve satisfy;"); + assert(empty->raw_variables.empty()&&empty->output[0].expression.elements[1].elements.empty()); + auto reif=good("var bool: b; var 0..3: x; constraint int_le_reif(x,2,b); solve :: int_search([x],input_order,indomain_min,complete) satisfy;"); + assert(reif->raw_constraints[0].id=="int_le_reif"&&reif->solve.annotations.size()==1); + auto annotations=good(R"(var 0..1: x :: one([1,2]) :: two(1,2) :: quoted("Hello,\n World\t\"quoted\"!"); solve satisfy;)"); + const auto& anns=annotations->declaration_annotations[0].annotations; + assert(anns[0].elements.size()==1&&anns[0].elements[0].kind==C::ValueKind::Array&&anns[0].elements[0].elements.size()==2); + assert(anns[1].elements.size()==2&&anns[1].elements[0].kind==C::ValueKind::Integer); + assert(anns[2].elements[0].text=="Hello,\n World\t\"quoted\"!"); + const auto& output_ann=a->declaration_annotations.back().annotations[0]; + assert(output_ann.elements.size()==1&&output_ann.elements[0].kind==C::ValueKind::Array); + auto canonical=good("var {3,1,3}: x; solve satisfy;"); + assert((variable(*canonical,C::Type::Integer,0).domain.integers.values==std::vector{1,3})); + auto params=good("array[1..2] of bool: flags=[true,false]; constraint custom(flags[2]); solve satisfy;"); + assert(params->raw_constraints[0].arguments[0].kind==C::ValueKind::Boolean&&!params->raw_constraints[0].arguments[0].boolean); + std::ostringstream chain;chain<<"array[1..512] of var int: a;"; + for(int i=512;i>1;--i)chain<<"constraint int_eq(a["<variables.size()==512&&normalized->coverage.size()==1023); + for(std::size_t i=1;i<512;++i)assert(normalized->variables[i].alias&&normalized->variables[i].target.index==0&&!normalized->raw_variables[i].alias); + registry().add("capture_test_never_post",poster); + auto unknown=good("constraint capture_test_never_post(1); solve satisfy;"); + assert(posted==0&&unknown->raw_constraints[0].id=="capture_test_never_post"); +} +void malformed_cases(){ + for(const auto& s:std::vector{ + "", "var int: x;", "constraint int_eq(); solve satisfy;", "constraint int_eq(1); solve satisfy;", + "constraint int_eq(true,1); solve satisfy;", "constraint bool2int(1,true); solve satisfy;", + "constraint foo(missing); solve satisfy;", "var int: x=missing; solve satisfy;", + "int: x :: ignored = 1; solve satisfy;", + "array[1..1] of int: x :: ignored = [1]; solve satisfy;", + "int: x=true; solve satisfy;", "bool: x=1; solve satisfy;", "float: x=false; solve satisfy;", "set of int: x=1; solve satisfy;", + "var int: x=true; solve satisfy;", "var bool: x=1; solve satisfy;", "var float: x=false; solve satisfy;", "var set of int: x=1; solve satisfy;", + "var int: x; var int: x; solve satisfy;", "solve minimize missing;", "solve minimize missing[-1];", + "array[1..1] of var int: a; solve minimize a[-1];", "array[1..1] of var int: a; solve minimize a[2];", + "int: a=1; solve minimize a[1];", "array[1..1] of var bool: a; solve minimize a[1];", + "array[1..2] of var int: a=[1]; solve satisfy;", "array[1..1] of var int: a=[true]; solve satisfy;", + "array[0..1] of var int: a; solve satisfy;", "array[1..1] of var int: a :: output_array([true]); solve satisfy;", + "array[1..1] of var int: a :: output_array([1..2]); solve satisfy;", + "var int: a :: output_array([1..1]); solve satisfy;", + "array[1..0] of var int: a :: output_array([]); solve satisfy;", + "constraint foo(missing[missing]); solve satisfy;", "var int: x :: foo([1,2; solve satisfy;", + "solve :: name(\"unfinished) satisfy;", + "solve minimize 9999999999999999999999999;", "solve minimize 1.0e999;", "solve minimize 1.0e-999;"})reject(s); + reject("constraint gecode_on_restart_status(1); solve satisfy;",C::Status::Unsupported); + reject(std::string("solve satisfy;\0",15)); + C::Options o;o.max_input_bytes=4;reject("solve satisfy;",C::Status::ResourceLimit,o); + o={};o.max_array_elements=1;reject("array[1..2] of var int: a; solve satisfy;",C::Status::ResourceLimit,o); + o={};o.max_variables=1;reject("array[1..2] of var int: a; solve satisfy;",C::Status::ResourceLimit,o); + reject("var int: x; var int: y; solve satisfy;",C::Status::ResourceLimit,o); + o={};o.max_constraints=0;reject("constraint int_eq(1,1); solve satisfy;",C::Status::ResourceLimit,o); + reject("var 1..2: x=1; solve satisfy;",C::Status::ResourceLimit,o); + o.max_constraints=1;reject("var 1..2: x=1; constraint int_eq(x,1); solve satisfy;",C::Status::ResourceLimit,o); + o.max_constraints=0;reject("var 1.0..2.0: x=1.5; solve satisfy;",C::Status::ResourceLimit,o); + o={};o.max_value_depth=2;reject("solve :: a(b(c(1))) satisfy;",C::Status::ResourceLimit,o); + reject(R"(solve :: unsupported("\123") satisfy;)",C::Status::Unsupported); + reject(std::string("solve :: text(\"\xc0\xaf\") satisfy;")); + auto unicode=good(u8"solve :: text(\"λ😀\") satisfy;");assert(unicode->solve.annotations[0].elements[0].text==u8"λ😀"); + std::istringstream input("solve maximize 9;");input.exceptions(std::ios::failbit|std::ios::badbit); + auto r=C::parse(input);assert(r.status==C::Status::Complete&&r.records->solve.objective.integer==9); + struct BadBuffer:std::streambuf{int_type underflow()override{throw std::runtime_error("broken source");}} buffer; + std::istream broken(&buffer);broken.exceptions(std::ios::badbit); + auto failed=C::parse(broken);assert(failed.status==C::Status::InvalidInput&&!failed.records); + std::istringstream exact("solve satisfy;");C::Options exact_options;exact_options.max_input_bytes=14; + auto equal=C::parse(exact,exact_options);assert(equal.status==C::Status::Complete); + std::istringstream over("solve satisfy; ");auto too_long=C::parse(over,exact_options);assert(too_long.status==C::Status::ResourceLimit&&!too_long.records); +} +void legacy_case(){ + std::istringstream input("var 1..3: x :: output_var; constraint int_eq(x,2); solve satisfy;"); + Printer printer;std::ostringstream errors;std::unique_ptr space(Gecode::FlatZinc::parse(input,printer,errors)); + assert(space&&errors.str().empty()&&space->iv.size()==1&&space->iv[0].assigned()&&space->iv[0].val()==2); +} +} +int main(int argc,char**){ + semantic_cases();malformed_cases();if(argc==1)legacy_case(); + for(int n=0;n<20;++n){good("var int: x; array[1..2] of var int: a=[x,x]; solve satisfy;");reject("var int: x; array[1..2] of var int: a=[x]; solve satisfy;");} + const std::string source="var {1,3}: x :: output_var; array[1..2] of var int: a :: output_array([1..2])=[x,2]; constraint int_le(x,3); solve minimize x;"; + for(std::size_t n=0;n correct{true};std::vector workers; + for(int n=0;n<4;++n)workers.emplace_back([&]{for(int k=0;k<8;++k){auto r=C::parse_string(source);if(r.status!=C::Status::Complete||!r.records||r.records->raw_variables.size()!=2)correct=false;}}); + for(auto& worker:workers)worker.join();assert(correct); + std::cout<<"FlatZinc capture "< +#include +#include +#include +#include + +using namespace Gecode; +namespace { +void check(bool okay) { if (!okay) throw std::runtime_error("brancher lifecycle regression"); } +class State : public Space { +public: + IntVarArray x; + explicit State(int size) : x(*this,size,0,1) { + for(int i=0;i ids() { + std::vector result; + for(Space::Branchers b(*this);b();++b) result.push_back(b.brancher().id()); + return result; + } + unsigned int propagators() { + unsigned int count=0; + for(Space::Propagators p(*this);p();++p) { (void)&p.propagator();++count; } + return count; + } + unsigned int idle_propagators() { + unsigned int count=0; + for(Space::IdlePropagators p(*this);p();++p) { (void)&p.propagator();++count; } + return count; + } +}; +} +int main() { + State empty(0); + check(empty.ids().empty() && empty.status()==SS_SOLVED && empty.choice()==nullptr); + check(empty.propagators()==0 && empty.idle_propagators()==0); + std::unique_ptr empty_clone(static_cast(empty.clone())); + check(empty_clone->ids().empty() && empty_clone->status()==SS_SOLVED); + + State original(3);check(original.status()==SS_BRANCH); + const auto original_ids=original.ids();check(original_ids.size()==3); + std::unique_ptr first(original.choice()); + std::unique_ptr child(static_cast(original.clone())); + child->commit(*first,0);check(child->status()==SS_BRANCH && child->x[0].val()==0); + std::unique_ptr second(child->choice()); + check(child->ids()==std::vector({original_ids[1],original_ids[2]})); + Archive archive;second->archive(archive); + std::unique_ptr restored(child->choice(archive)); + std::unique_ptr grandchild(static_cast(child->clone())); + grandchild->commit(*restored,1); + check(grandchild->status()==SS_BRANCH && grandchild->x[1].val()==1); + std::unique_ptr third(grandchild->choice()); + grandchild->commit(*third,0);check(grandchild->status()==SS_SOLVED); + check(grandchild->choice()==nullptr && grandchild->ids().empty()); + + // Look up a later brancher, then wrap around to an earlier one without + // calling choice(), which would delete the exhausted actors. + std::unique_ptr wrap(static_cast(original.clone())); + wrap->commit(*third,0);wrap->commit(*first,0); + check(wrap->x[0].val()==0 && wrap->x[2].val()==0 && wrap->status()==SS_BRANCH); + std::unique_ptr missing_middle(static_cast(original.clone())); + BrancherGroup middle;middle.move(*missing_middle,original_ids[1]);middle.kill(*missing_middle); + bool no_middle=false; + try { missing_middle->commit(*second,0); } catch(const SpaceNoBrancher&) { no_middle=true; } + check(no_middle && missing_middle->ids().size()==2); + missing_middle->commit(*third,0);missing_middle->commit(*first,0); + check(missing_middle->status()==SS_SOLVED); + + std::unique_ptr killed(static_cast(original.clone())); + BrancherGroup removal; + for (auto id : {original_ids[1],original_ids[0],original_ids[2]}) { + removal.move(*killed,id);removal.kill(*killed); + } + check(killed->ids().empty() && killed->status()==SS_SOLVED); + bool missing=false; + try { killed->commit(*first,0); } catch(const SpaceNoBrancher&) { missing=true; } + check(missing); + std::unique_ptr killed_clone(static_cast(killed->clone())); + check(killed_clone->status()==SS_SOLVED && killed_clone->ids().empty()); + + State failed(2);failed.fail(); + check(failed.status()==SS_FAILED && failed.choice()==nullptr && failed.ids().empty()); + State constrained(3);rel(constrained,constrained.x[0],IRT_LQ,constrained.x[1]); + check(constrained.propagators()>0); + check(constrained.status()==SS_BRANCH && constrained.idle_propagators()>0); + Search::Options options;options.threads=1;options.c_d=3;options.a_d=2; + DFS search(&original,options);unsigned int seen=0,mask=0; + while(std::unique_ptr solution{search.next()}) { + ++seen;mask |= 1U << (solution->x[0].val()+2*solution->x[1].val()+4*solution->x[2].val()); + } + check(seen==8 && mask==255 && !search.stopped()); +} diff --git a/test/optimize/bulk.cpp b/test/optimize/bulk.cpp new file mode 100644 index 0000000000..75c5e89f2a --- /dev/null +++ b/test/optimize/bulk.cpp @@ -0,0 +1,178 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Deterministic allocation failures exercise the public all-or-nothing boundary. +namespace Allocation { +long fail_after = -1; +std::size_t maximum = std::numeric_limits::max(), largest = 0; +} +void* operator new(std::size_t bytes) { + Allocation::largest = std::max(Allocation::largest, bytes); + if (bytes > Allocation::maximum || Allocation::fail_after == 0) throw std::bad_alloc(); + if (Allocation::fail_after > 0) --Allocation::fail_after; + if (void* memory = std::malloc(bytes ? bytes : 1)) return memory; + throw std::bad_alloc(); +} +void* operator new[](std::size_t bytes) { return ::operator new(bytes); } +void operator delete(void* memory) noexcept { std::free(memory); } +void operator delete[](void* memory) noexcept { std::free(memory); } +void operator delete(void* memory, std::size_t) noexcept { std::free(memory); } +void operator delete[](void* memory, std::size_t) noexcept { std::free(memory); } + +using namespace Gecode::Optimize; +namespace { +constexpr double inf = std::numeric_limits::infinity(); +std::string fingerprint(const Model& model) { + const auto source = model.snapshot(); std::ostringstream out; + out.precision(17); out << source.model_id << ':' << source.revision; + for (const auto& v : source.variables) + out << '/' << v.variable.model_id << ':' << v.variable.id << ':' << int(v.type) + << ':' << v.lower << ':' << v.upper << ':' << v.active << ':' << v.name; + for (const auto& r : source.rows) { + out << '/' << r.constraint.model_id << ':' << r.constraint.id << ':' << r.lower << ':' << r.upper << ':' << r.active << ':' << r.name; + for (const auto& t : r.terms) out << ':' << t.variable.model_id << ',' << t.variable.id << ',' << t.coefficient; + } + out << '/' << int(source.objective.sense) << ':' << source.objective.offset; + for (const auto& t : source.objective.terms) out << ':' << t.variable.id << ',' << t.coefficient; + return out.str(); +} +template void rejects_unchanged(Model& model, Action action) { + const auto before = fingerprint(model); + bool caught = false; + try { action(); } catch (const ModelError&) { caught = true; } + assert(caught && fingerprint(model) == before); +} + +void scalar_and_bulk_equivalence() { + Model model; + const auto removed = model.add_integer(0, 1); model.remove(removed); + const auto old = model.snapshot(); const auto revision = model.revision(); + const std::vector specs{ + {VariableType::Integer, -3, 7, "integer"}, {VariableType::Continuous, -inf, inf, "free"}, + {VariableType::Binary, .2, .8, "empty integer domain"}, + {VariableType::SemiContinuous, 2, 9, "semi"}, {VariableType::SemiInteger, 2.5, 7, "semi integer"}}; + const auto variables = model.add_variables(specs); + assert(model.revision() == revision + 1 && variables.size() == specs.size()); + for (std::size_t i = 0; i < variables.size(); ++i) { + const auto& value = model.variable(variables[i]); + assert(variables[i].model_id == model.id() && variables[i].id == i + 1); + assert(value.type == specs[i].type && value.lower == specs[i].lower && value.upper == specs[i].upper && value.name == specs[i].name); + } + assert(old.variables.size() == 1 && !old.variables[0].active); + const std::vector rows{ + {{{variables[0], 1e16}, {variables[1], 2}, {variables[0], 1}, {variables[0], -1e16}}, -3, 8, "compensated"}, + {{}, -inf, inf, "empty"}, {{{variables[4], 3}, {variables[4], -3}}, 1, inf, "contradiction"}}; + const auto before = model.revision(); const auto handles = model.add_rows(rows); + assert(model.revision() == before + 1 && handles.size() == 3); + assert(model.row(handles[0]).terms.size() == 2 && model.row(handles[0]).terms[0].coefficient == 1); + assert(model.row(handles[1]).terms.empty() && model.row(handles[2]).terms.empty()); + Model scalar; const auto sv = scalar.add_variables(specs); + for (std::size_t i = 0; i < rows.size(); ++i) { + auto terms = rows[i].terms; for (auto& term : terms) term.variable = sv[term.variable.id - 1]; + const auto handle = scalar.add_row(terms, rows[i].lower, rows[i].upper, rows[i].name); + const auto& actual = model.row(handles[i]); const auto& expected = scalar.row(handle); + assert(actual.lower == expected.lower && actual.upper == expected.upper && actual.name == expected.name); + assert(actual.terms.size() == expected.terms.size()); + for (std::size_t k = 0; k < actual.terms.size(); ++k) + assert(actual.terms[k].coefficient == expected.terms[k].coefficient && actual.terms[k].variable.id == expected.terms[k].variable.id + 1); + } + validate_structure(model.snapshot()); + const auto unchanged = fingerprint(model); + assert(model.add_variables({}).empty() && model.add_rows({}).empty() && model.add_rows_sparse({}).empty()); + assert(fingerprint(model) == unchanged); +} + +void csr_semantics_and_rejections() { + Model model; + auto vars = model.add_variables({{VariableType::Integer, -2, 2, "x"}, {VariableType::Integer, 0, 3, "y"}}); + auto gone = model.add_integer(0, 1); model.remove(gone); + const auto removed = model.add_row({}, -inf, inf); model.remove(removed); + SparseRowBatch rows; + rows.columns = {vars[1], vars[0]}; // explicit mapping differs from slot order + rows.row_start = {0, 4, 4, 5}; rows.column = {1, 0, 1, 1, 0}; + rows.coefficient = {1e16, 2, 1, -1e16, -3}; rows.lower = {-1, -inf, -9}; rows.upper = {4, inf, 0}; + rows.names = {"row", "empty", "bound"}; + const auto revision = model.revision(); const auto added = model.add_rows_sparse(rows); + assert(model.revision() == revision + 1 && added.size() == 3 && added[0].id == 1); + assert(model.row(added[0]).terms.size() == 2 && model.row(added[0]).terms[0].variable == vars[0]); + assert(model.row(added[0]).terms[0].coefficient == 1 && model.row(added[0]).terms[1].coefficient == 2); + for (int x = -2; x <= 2; ++x) for (int y = 0; y <= 3; ++y) { + const auto checked = validate(model.snapshot(), {double(x), double(y), 0}, 0, 0); + assert(checked.valid == (-1 <= x + 2*y && x + 2*y <= 4)); + } + const auto bad = [&](auto change) { auto malformed = rows; change(malformed); rejects_unchanged(model, [&] { model.add_rows_sparse(malformed); }); }; + bad([](auto& r) { r.row_start.clear(); }); bad([](auto& r) { r.row_start[0] = 1; }); + bad([](auto& r) { r.row_start.back() = 4; }); bad([](auto& r) { r.row_start[1] = 6; }); + bad([](auto& r) { r.row_start[2] = 3; }); bad([](auto& r) { r.upper.pop_back(); }); + bad([](auto& r) { r.names.pop_back(); }); bad([](auto& r) { r.column.pop_back(); }); + bad([](auto& r) { r.column.back() = 2; }); bad([](auto& r) { r.coefficient.back() = inf; }); + bad([](auto& r) { r.lower.back() = 1; }); bad([](auto& r) { r.columns.push_back(r.columns[0]); }); + bad([&](auto& r) { r.columns.push_back(gone); }); + Model foreign; const auto other = foreign.add_binary(); bad([&](auto& r) { r.columns.push_back(other); }); + rejects_unchanged(model, [&] { model.add_rows({{{{vars[0], 1}}, 0, 1, {}}, {{{other, 0}}, 0, 1, {}}}); }); + rejects_unchanged(model, [&] { model.add_variables({{VariableType::Integer, 0, 1, {}}, {VariableType::Binary, -1, 1, {}}}); }); + rejects_unchanged(model, [&] { model.add_variables({{static_cast(-1), 0, 1, {}}}); }); + Model moved = std::move(model); + bool caught = false; try { model.add_variables({}); } catch (const ModelError&) { caught = true; } assert(caught); + assert(moved.row(added[0]).constraint.model_id == moved.id()); +} + +void allocation_atomicity() { + for (int operation = 0; operation < 3; ++operation) { + bool succeeded = false; unsigned failures = 0; + for (long stop = 0; stop < 100 && !succeeded; ++stop) { + Model model; auto x = model.add_integer(0, 3); + auto original = model.add_row({{x, 2}}, 0, 6, "existing"); model.minimize({{x, 1}}, 2); + const auto* variable_view = &model.variable(x); const auto* row_view = &model.row(original); + const auto before = fingerprint(model); + std::vector variables(3, {VariableType::Integer, -2, 2, std::string(100, 'v')}); + std::vector rows(3, {{{x, 2}, {x, -1}}, 0, 6, std::string(100, 'r')}); + SparseRowBatch sparse; sparse.columns = {x}; sparse.row_start = {0, 1, 2, 3}; sparse.column = {0, 0, 0}; + sparse.coefficient = {1, 2, 3}; sparse.lower = {0, 0, 0}; sparse.upper = {3, 6, 9}; sparse.names = {std::string(100, 's'), "b", "c"}; + Allocation::fail_after = stop; + try { + if (operation == 0) (void) model.add_variables(variables); + else if (operation == 1) (void) model.add_rows(rows); + else (void) model.add_rows_sparse(sparse); + Allocation::fail_after = -1; succeeded = true; + } catch (const std::bad_alloc&) { + Allocation::fail_after = -1; ++failures; + assert(fingerprint(model) == before); + assert(&model.variable(x) == variable_view && &model.row(original) == row_view); + } + } + assert(succeeded && failures > 3); + } +} + +void sparse_scale() { + constexpr std::size_t n = 16384; + Model model; std::vector specs(n, {VariableType::Integer, 0, 1, {}}); + Allocation::largest = 0; Allocation::maximum = 4 * 1024 * 1024; + const auto variables = model.add_variables(specs); + SparseRowBatch input; input.columns = variables; input.row_start.resize(n + 1); + input.column.resize(n); input.coefficient.assign(n, 1); input.lower.assign(n, 0); input.upper.assign(n, 1); + for (std::size_t i = 0; i <= n; ++i) input.row_start[i] = i; + for (std::size_t i = 0; i < n; ++i) input.column[i] = n - 1 - i; + const auto rows = model.add_rows_sparse(input); + Allocation::maximum = std::numeric_limits::max(); + assert(rows.size() == n && model.revision() == 2); + const auto snapshot = model.snapshot(); std::size_t nonzeros = 0; + for (const auto& row : snapshot.rows) nonzeros += row.terms.size(); + assert(nonzeros == n && Allocation::largest < 4 * 1024 * 1024); +} +} +int main() { + scalar_and_bulk_equivalence(); csr_semantics_and_rejections(); allocation_atomicity(); sparse_scale(); + std::cout << "Atomic bulk/CSR equivalence, allocation failure and 16K sparse construction pass\n"; +} diff --git a/test/optimize/c_api.c b/test/optimize/c_api.c new file mode 100644 index 0000000000..0869081887 --- /dev/null +++ b/test/optimize/c_api.c @@ -0,0 +1,677 @@ +/* Compile this file as C99, then link the shared C ABI library. */ +#include +#include +#include +#include + +#define CHECK(test) do { if(!(test)){fprintf(stderr,"FAIL line %d: %s (%s)\n",__LINE__,#test,gecode_opt_v1_last_error());return 1;} } while(0) +#define OK(call) CHECK((call)==GECODE_OPT_OK) +static int near(double a,double b){return isfinite(a)&&fabs(a-b)<1e-6;} + +static int bulk(void) { + gecode_opt_handle m=0,foreign=0,historical=0,current=0; + gecode_opt_id dead,other,vars[5],rows[2],sentinel[2],mapping[2]; + gecode_opt_variable_spec_v1 specs[5];gecode_opt_row_spec_v1 rs[2];gecode_opt_sparse_row_batch_v1 csr; + gecode_opt_term ts[4];gecode_opt_options_v1 options;gecode_opt_result_info_v1 info; + uint64_t owner=0,revision=0,after=0,start[3]={0,3,4},column[4]={1,1,1,0}; + double coefficient[4]={1e16,1,-1e16,1},lower[2]={2,0},upper[2]={INFINITY,4},value=0; + int32_t available=0,lp=0,mip=0,present=0;size_t i; + char name[]="retained";const char* names[2]={"demand",NULL}; + memset(specs,0,sizeof(specs));memset(rs,0,sizeof(rs));memset(&csr,0,sizeof(csr)); + memset(sentinel,0x5a,sizeof(sentinel));memcpy(rows,sentinel,sizeof(rows)); + for(i=0;i<5;++i){specs[i].struct_size=sizeof(specs[i]);specs[i].type=(int32_t)i;specs[i].upper=i==2?1:5;} + specs[3].lower=2;specs[4].lower=2.5;specs[0].name=name; + OK(gecode_opt_v1_model_create(&m));OK(gecode_opt_v1_model_create(&foreign)); + OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_CONTINUOUS,0,1,"hole",&dead)); + OK(gecode_opt_v1_model_remove_variable(m,dead)); + OK(gecode_opt_v1_model_add_variable(foreign,GECODE_OPT_CONTINUOUS,0,1,"",&other)); + OK(gecode_opt_v1_model_identity(m,&owner,&revision)); + CHECK(gecode_opt_v1_model_add_variables(m,specs,5,NULL,0)==GECODE_OPT_BUFFER_TOO_SMALL); + CHECK(gecode_opt_v1_model_add_variables(m,specs,5,NULL,5)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_model_add_variables(m,specs,5,rows,2)==GECODE_OPT_BUFFER_TOO_SMALL); + CHECK(memcmp(rows,sentinel,sizeof(rows))==0); + CHECK(gecode_opt_v1_model_add_variables(m,NULL,1,vars,5)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_model_add_variables(m,specs,UINT64_MAX,vars,5)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_model_add_variables(m,specs,5,vars,UINT64_MAX)==GECODE_OPT_INVALID_ARGUMENT); + specs[4].struct_size--;CHECK(gecode_opt_v1_model_add_variables(m,specs,5,vars,5)==GECODE_OPT_INVALID_ARGUMENT);specs[4].struct_size++; + specs[4].reserved=1;CHECK(gecode_opt_v1_model_add_variables(m,specs,5,vars,5)==GECODE_OPT_INVALID_ARGUMENT);specs[4].reserved=0; + specs[4].type=99;CHECK(gecode_opt_v1_model_add_variables(m,specs,5,vars,5)==GECODE_OPT_INVALID_ARGUMENT);specs[4].type=4; + specs[4].lower=NAN;CHECK(gecode_opt_v1_model_add_variables(m,specs,5,vars,5)==GECODE_OPT_MODEL_ERROR);specs[4].lower=2.5; + {const char* bad[]={"\xc0\x80","\xed\xa0\x80","\xf4\x90\x80\x80","\x80","\xe2"}; + for(i=0;i1); + CHECK(gecode_opt_v1_pool_message(pool,message,1,&needed)==GECODE_OPT_BUFFER_TOO_SMALL&&message[0]=='!'); + if(highs){ + CHECK(pi.completion==GECODE_OPT_POOL_EXHAUSTED&&pi.entry_count==3&&pi.ranked_prefix==3&&pi.attempt_count==4); + OK(gecode_opt_v1_pool_entry_info(pool,0,&ei,sizeof(ei)));CHECK(ei.rank_established&&ei.projection_count==1); + CHECK(gecode_opt_v1_pool_entry_projection(pool,0,&point,0,&needed)==GECODE_OPT_BUFFER_TOO_SMALL&&point==99); + OK(gecode_opt_v1_pool_entry_projection(pool,0,&point,1,&needed));CHECK(point==-1); + OK(gecode_opt_v1_pool_attempt_info(pool,3,&ai,sizeof(ai)));CHECK(ai.termination==GECODE_OPT_INFEASIBLE&&!ai.candidate_accepted); + OK(gecode_opt_v1_pool_entry_result(pool,0,&entry)); + OK(gecode_opt_v1_result_info(entry,&result_info,sizeof(result_info)));CHECK(result_info.termination==GECODE_OPT_UNKNOWN); + OK(gecode_opt_v1_result_number(entry,GECODE_OPT_BEST_BOUND,&present,&value));CHECK(!present&&value==0); + OK(gecode_opt_v1_result_values(entry,values,active,available,3,&needed));CHECK(!active[2]&&!available[2]&&values[2]==0); + }else CHECK(pi.completion==GECODE_OPT_POOL_INCOMPLETE&&pi.entry_count==0); + OK(gecode_opt_v1_pool_destroy(pool));CHECK(gecode_opt_v1_pool_destroy(pool)==GECODE_OPT_INVALID_HANDLE); + po.max_solutions=1;OK(gecode_opt_v1_pool_solve(m,&po,&pool));OK(gecode_opt_v1_pool_info(pool,&pi,sizeof(pi))); + if(highs)CHECK(pi.completion==GECODE_OPT_POOL_REQUESTED_LIMIT&&pi.termination==GECODE_OPT_SOLUTION_LIMIT&&pi.entry_count==1); + OK(gecode_opt_v1_pool_destroy(pool)); + /* Keep a historical entry across source edits and destruction. */ + OK(gecode_opt_v1_model_set_row_bounds(m,row,6,INFINITY)); + selection.source=row;selection.side=GECODE_OPT_RELAX_LOWER;selection.reserved=0;selection.penalty=2; + ro.solve.backend=GECODE_OPT_HIGHS;ro.selections=&selection;ro.selection_count=1;ro.optimize_original_objective=1; + ro.struct_size--;CHECK(gecode_opt_v1_repair_solve(m,&ro,&repair)==GECODE_OPT_INVALID_ARGUMENT&&repair==0);ro.struct_size++; + ro.reserved=1;CHECK(gecode_opt_v1_repair_solve(m,&ro,&repair)==GECODE_OPT_INVALID_ARGUMENT);ro.reserved=0; + ro.selection_count=UINT64_MAX;CHECK(gecode_opt_v1_repair_solve(m,&ro,&repair)==GECODE_OPT_INVALID_ARGUMENT);ro.selection_count=1; + ro.selections=NULL;CHECK(gecode_opt_v1_repair_solve(m,&ro,&repair)==GECODE_OPT_INVALID_ARGUMENT);ro.selections=&selection; + selection.reserved=1;CHECK(gecode_opt_v1_repair_solve(m,&ro,&repair)==GECODE_OPT_INVALID_ARGUMENT);selection.reserved=0; + selection.source.reserved=1;CHECK(gecode_opt_v1_repair_solve(m,&ro,&repair)==GECODE_OPT_INVALID_ARGUMENT);selection.source.reserved=0; + selection.side=7;CHECK(gecode_opt_v1_repair_solve(m,&ro,&repair)==GECODE_OPT_INVALID_ARGUMENT);selection.side=GECODE_OPT_RELAX_LOWER; + selection.penalty=NAN;CHECK(gecode_opt_v1_repair_solve(m,&ro,&repair)==GECODE_OPT_INVALID_ARGUMENT);selection.penalty=2; + selection.source=other;OK(gecode_opt_v1_repair_solve(m,&ro,&repair));OK(gecode_opt_v1_repair_info(repair,&ri,sizeof(ri))); + CHECK(ri.termination==GECODE_OPT_INVALID_MODEL);OK(gecode_opt_v1_repair_destroy(repair));selection.source=row; + OK(gecode_opt_v1_repair_solve(m,&ro,&repair));OK(gecode_opt_v1_repair_info(repair,&ri,sizeof(ri))); + CHECK(ri.termination==(highs?GECODE_OPT_OPTIMAL:GECODE_OPT_UNSUPPORTED));CHECK(ri.source_model_id==owner&&ri.variable_slots==3); + CHECK(gecode_opt_v1_pool_destroy(repair)==GECODE_OPT_INVALID_HANDLE); + OK(gecode_opt_v1_repair_variable_map(repair,sources,mapped,active,3,&needed));CHECK(needed==3&&!active[2]); + CHECK(sources[0].model_id==owner&&mapped[0].model_id!=owner&&mapped[0].model_id==ri.private_model_id); + OK(gecode_opt_v1_repair_item_info(repair,0,&item,sizeof(item)));CHECK(item.source.kind==GECODE_OPT_ROW_ID&&item.source.slot==row.slot); + CHECK(item.slack.model_id==ri.private_model_id&&near(item.penalty,2)); + if(highs){ + CHECK(ri.has_repair&&ri.minimum_violation_established&&ri.original_objective_optimized&&ri.completed_stages==2); + OK(gecode_opt_v1_repair_number(repair,GECODE_OPT_REPAIR_VIOLATION,&present,&value));CHECK(present&&near(value,2)); + OK(gecode_opt_v1_repair_original_value(repair,x,&value));CHECK(near(value,1)); + CHECK(gecode_opt_v1_repair_original_value(repair,mapped[0],&value)==GECODE_OPT_MODEL_ERROR); + CHECK(gecode_opt_v1_repair_original_value(repair,dead,&value)==GECODE_OPT_MODEL_ERROR); + OK(gecode_opt_v1_repair_validation(repair,&validation,sizeof(validation)));CHECK(!validation.valid&&validation.model_valid&&near(validation.max_row_violation,1)); + OK(gecode_opt_v1_repair_stage_info(repair,0,&si,sizeof(si)));CHECK(si.completed&&si.retention_bound.present); + OK(gecode_opt_v1_repair_stage_result(repair,0,&stage));OK(gecode_opt_v1_repair_final_result(repair,&final)); + OK(gecode_opt_v1_repair_violation_lock(repair,&present,&lock));CHECK(present&&lock.model_id==ri.private_model_id); + OK(gecode_opt_v1_repair_original_values(repair,values,active,available,3,&needed));CHECK(available[0]&&!available[2]&&!active[2]); + }else{ + CHECK(!ri.has_repair);CHECK(gecode_opt_v1_repair_final_result(repair,&final)==GECODE_OPT_NO_SOLUTION&&final==0); + CHECK(!item.activity.present&&item.activity.value==0); + } + values[0]=99;active[0]=9;available[0]=9; + CHECK(gecode_opt_v1_repair_original_values(repair,values,active,available,1,&needed)==GECODE_OPT_BUFFER_TOO_SMALL); + CHECK(needed==3&&values[0]==99&&active[0]==9&&available[0]==9); + CHECK(gecode_opt_v1_repair_text(repair,GECODE_OPT_REPAIR_MESSAGE,1,NULL,0,&needed)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_repair_stage_result(repair,UINT64_MAX,&token)==GECODE_OPT_INVALID_ARGUMENT&&token==0); + OK(gecode_opt_v1_repair_destroy(repair));OK(gecode_opt_v1_model_destroy(m));OK(gecode_opt_v1_model_destroy(foreign)); + if(highs){ + OK(gecode_opt_v1_result_number(entry,GECODE_OPT_OBJECTIVE,&present,&value));CHECK(present&&near(value,6)); + OK(gecode_opt_v1_result_value(entry,x,&value));CHECK(near(value,-1));OK(gecode_opt_v1_result_destroy(entry)); + OK(gecode_opt_v1_result_value(final,mapped[0],&value));CHECK(near(value,1)); + CHECK(gecode_opt_v1_result_value(final,x,&value)==GECODE_OPT_MODEL_ERROR); + OK(gecode_opt_v1_result_info(stage,&result_info,sizeof(result_info)));CHECK(result_info.model_id==mapped[0].model_id); + OK(gecode_opt_v1_result_destroy(stage));OK(gecode_opt_v1_result_destroy(final)); + } + /* Cancellation and zero deadlines are workflow outcomes without a fallback. */ + OK(gecode_opt_v1_model_create(&m));OK(gecode_opt_v1_cancellation_create(&token));OK(gecode_opt_v1_cancellation_cancel(token)); + OK(gecode_opt_v1_pool_options_default(&po,sizeof(po)));po.solve.cancellation=token; + OK(gecode_opt_v1_pool_solve(m,&po,&pool));OK(gecode_opt_v1_pool_info(pool,&pi,sizeof(pi)));CHECK(pi.termination==GECODE_OPT_CANCELLED&&pi.entry_count==0); + OK(gecode_opt_v1_pool_destroy(pool));OK(gecode_opt_v1_cancellation_destroy(token)); + OK(gecode_opt_v1_repair_options_default(&ro,sizeof(ro)));ro.solve.time_limit_seconds=0; + OK(gecode_opt_v1_repair_solve(m,&ro,&repair));OK(gecode_opt_v1_repair_info(repair,&ri,sizeof(ri)));CHECK(ri.termination==GECODE_OPT_TIME_LIMIT&&!ri.has_repair); + OK(gecode_opt_v1_repair_destroy(repair)); + /* Native exact enumeration is explicit and needs no persistent session. */ + OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_INTEGER,-1,0,"x",&x)); + OK(gecode_opt_v1_pool_options_default(&po,sizeof(po)));po.solve.backend=GECODE_OPT_NATIVE;po.solve.guarantee=GECODE_OPT_EXACT; + OK(gecode_opt_v1_pool_solve(m,&po,&pool));OK(gecode_opt_v1_pool_info(pool,&pi,sizeof(pi))); + CHECK(pi.termination==(native?GECODE_OPT_OPTIMAL:GECODE_OPT_UNSUPPORTED)); + if(native)CHECK(pi.entry_count==2&&pi.completion==GECODE_OPT_POOL_EXHAUSTED&&pi.guarantee==GECODE_OPT_EXACT); + OK(gecode_opt_v1_pool_destroy(pool));OK(gecode_opt_v1_model_destroy(m)); + return 0; +} + +static int globals_and_logic(void) { + gecode_opt_handle m=0,foreign=0,r=0,session=0; + gecode_opt_id x,y,index,other,g,table,element,cumulative,circuit,successors[2],ids[2]; + gecode_opt_result_info_v1 info; + gecode_opt_options_v1 options; + gecode_opt_term objective[2]; + uint64_t owner=0,before=0,after=0; + int32_t native=0,highs=0,lp=0,mip=0,present=0; + int64_t tuples[4]={0,1,1,0},durations[2]={0,1},heights[2]={100,0}; + double value=0; + OK(gecode_opt_v1_capabilities(GECODE_OPT_NATIVE,&native,&lp,&mip)); + OK(gecode_opt_v1_capabilities(GECODE_OPT_HIGHS,&highs,&lp,&mip)); + OK(gecode_opt_v1_model_create(&m));OK(gecode_opt_v1_model_create(&foreign)); + OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_INTEGER,0,1,"x",&x)); + OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_INTEGER,0,1,"y",&y)); + OK(gecode_opt_v1_model_add_variable(foreign,GECODE_OPT_INTEGER,0,1,"other",&other)); + ids[0]=x;ids[1]=other; + OK(gecode_opt_v1_model_identity(m,&owner,&before)); + CHECK(gecode_opt_v1_model_add_all_different(m,ids,2,"foreign",&g)==GECODE_OPT_MODEL_ERROR); + CHECK(gecode_opt_v1_model_add_all_different(m,NULL,1,"null",&g)==GECODE_OPT_INVALID_ARGUMENT); + ids[1]=y; + CHECK(gecode_opt_v1_model_add_table(m,ids,2,tuples,3,2,"arity",&g)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_model_add_table(m,ids,2,NULL,4,2,"null",&g)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_model_add_table(m,ids,2,tuples,0,UINT64_MAX,"overflow",&g)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_model_add_cumulative(m,ids,2,durations,1,heights,2,0,"length",&g)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_model_add_cumulative(m,ids,2,durations,2,heights,2,-1,"capacity",&g)==GECODE_OPT_MODEL_ERROR); + CHECK(gecode_opt_v1_model_add_circuit(m,ids,2,INT64_MAX,"base",&g)==GECODE_OPT_MODEL_ERROR); + OK(gecode_opt_v1_model_identity(m,&owner,&after));CHECK(before==after); + OK(gecode_opt_v1_model_add_all_different(m,ids,2,"distinct",&g));CHECK(g.kind==GECODE_OPT_GLOBAL_ID); + CHECK(gecode_opt_v1_model_remove_variable(m,x)==GECODE_OPT_MODEL_ERROR); + CHECK(gecode_opt_v1_model_remove_global(m,x)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_model_remove_global(foreign,g)==GECODE_OPT_MODEL_ERROR); + OK(gecode_opt_v1_model_set_global_name(m,g,"renamed")); + OK(gecode_opt_v1_model_add_table(m,ids,2,tuples,4,2,"allowed",&table)); + /* One-based element index selects x, which also aliases the result. */ + OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_INTEGER,1,1,"index",&index)); + OK(gecode_opt_v1_model_add_element(m,index,ids,2,x,1,"element",&element)); + ids[1]=x; + OK(gecode_opt_v1_model_add_cumulative(m,ids,2,durations,2,heights,2,0,"zero usage",&cumulative)); + OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_INTEGER,2,2,"s0",&successors[0])); + OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_INTEGER,1,1,"s1",&successors[1])); + OK(gecode_opt_v1_model_add_circuit(m,successors,2,1,"cycle",&circuit)); + objective[0].variable=x;objective[0].coefficient=1;objective[1].variable=y;objective[1].coefficient=2; + OK(gecode_opt_v1_model_set_objective(m,objective,2,GECODE_OPT_MINIMIZE,0)); + OK(gecode_opt_v1_options_default(&options,sizeof(options))); + OK(gecode_opt_v1_solve(m,&options,&r));OK(gecode_opt_v1_result_info(r,&info,sizeof(info))); + CHECK(info.termination==(native?GECODE_OPT_OPTIMAL:GECODE_OPT_UNSUPPORTED)); + if(native){OK(gecode_opt_v1_result_number(r,GECODE_OPT_OBJECTIVE,&present,&value));CHECK(present&&near(value,1));} + OK(gecode_opt_v1_result_destroy(r));options.backend=GECODE_OPT_HIGHS; + OK(gecode_opt_v1_solve(m,&options,&r));OK(gecode_opt_v1_result_info(r,&info,sizeof(info))); + CHECK(info.termination==GECODE_OPT_UNSUPPORTED);OK(gecode_opt_v1_result_destroy(r)); + options.backend=GECODE_OPT_AUTO;OK(gecode_opt_v1_session_create(&session)); + OK(gecode_opt_v1_session_solve(session,m,&options,&r));OK(gecode_opt_v1_result_info(r,&info,sizeof(info))); + CHECK(info.termination==GECODE_OPT_UNSUPPORTED);OK(gecode_opt_v1_result_destroy(r));OK(gecode_opt_v1_session_destroy(session)); + OK(gecode_opt_v1_model_remove_global(m,table)); + CHECK(gecode_opt_v1_model_remove_global(m,table)==GECODE_OPT_MODEL_ERROR); + CHECK(gecode_opt_v1_model_set_global_name(m,table,"dead")==GECODE_OPT_MODEL_ERROR); + OK(gecode_opt_v1_model_destroy(m));OK(gecode_opt_v1_model_destroy(foreign)); + + /* Zero-arity table distinguishes no tuples (false) from one empty tuple (true). */ + {int rows; + for(rows=0;rows<=1;++rows){ + OK(gecode_opt_v1_model_create(&m)); + OK(gecode_opt_v1_model_add_table(m,NULL,0,NULL,0,(uint64_t)rows,"zero arity",&g)); + OK(gecode_opt_v1_solve(m,NULL,&r));OK(gecode_opt_v1_result_info(r,&info,sizeof(info))); + CHECK(info.termination==(native?(rows?GECODE_OPT_OPTIMAL:GECODE_OPT_INFEASIBLE):GECODE_OPT_UNSUPPORTED)); + OK(gecode_opt_v1_result_destroy(r));OK(gecode_opt_v1_model_destroy(m)); + } + } + /* A bounded indicator keeps its domain guard and exposes its gate lifetime. */ + {gecode_opt_id b,indicator,gate,a,o;gecode_opt_term term;int32_t has_gate=0; + OK(gecode_opt_v1_model_create(&m)); + OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_BINARY,1,1,"b",&b)); + OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_INTEGER,0,4,"x",&x)); + term.variable=x;term.coefficient=1; + OK(gecode_opt_v1_model_identity(m,&owner,&before)); + CHECK(gecode_opt_v1_model_add_indicator(m,b,2,&term,1,3,INFINITY,"bad",&indicator,&has_gate,&gate)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_model_add_indicator(m,b,1,&term,1,3,INFINITY,"bad",&indicator,NULL,&gate)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_model_add_boolean_and(m,b,&x,1,"integer")==GECODE_OPT_MODEL_ERROR); + OK(gecode_opt_v1_model_identity(m,&owner,&after));CHECK(before==after); + OK(gecode_opt_v1_model_add_indicator(m,b,1,&term,1,3,INFINITY,"guard",&indicator,&has_gate,&gate)); + CHECK(indicator.kind==GECODE_OPT_INDICATOR_ID&&has_gate&&gate.kind==GECODE_OPT_VARIABLE_ID); + CHECK(gecode_opt_v1_model_set_variable_bounds(m,x,0,5)==GECODE_OPT_MODEL_ERROR); + CHECK(gecode_opt_v1_model_remove_indicator(m,x)==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_model_set_objective(m,&term,1,GECODE_OPT_MINIMIZE,0));options.backend=GECODE_OPT_HIGHS; + OK(gecode_opt_v1_solve(m,&options,&r));OK(gecode_opt_v1_result_info(r,&info,sizeof(info))); + CHECK(info.termination==(highs?GECODE_OPT_OPTIMAL:GECODE_OPT_UNSUPPORTED)); + if(highs){OK(gecode_opt_v1_result_value(r,x,&value));CHECK(near(value,3));} + OK(gecode_opt_v1_result_destroy(r));OK(gecode_opt_v1_model_remove_indicator(m,indicator)); + CHECK(gecode_opt_v1_model_remove_indicator(m,indicator)==GECODE_OPT_MODEL_ERROR); + OK(gecode_opt_v1_model_remove_variable(m,gate)); + OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_BINARY,0,1,"and",&a)); + OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_BINARY,0,1,"or",&o)); + OK(gecode_opt_v1_model_add_boolean_and(m,a,NULL,0,"empty and")); + OK(gecode_opt_v1_model_add_boolean_or(m,o,NULL,0,"empty or")); + ids[0]=a;ids[1]=a;OK(gecode_opt_v1_model_add_boolean_and(m,a,ids,2,"alias")); + OK(gecode_opt_v1_solve(m,&options,&r));OK(gecode_opt_v1_result_info(r,&info,sizeof(info))); + CHECK(info.termination==(highs?GECODE_OPT_OPTIMAL:GECODE_OPT_UNSUPPORTED)); + if(highs){OK(gecode_opt_v1_result_value(r,x,&value));CHECK(near(value,0)); + OK(gecode_opt_v1_result_value(r,a,&value));CHECK(near(value,1));OK(gecode_opt_v1_result_value(r,o,&value));CHECK(near(value,0));} + OK(gecode_opt_v1_result_destroy(r));OK(gecode_opt_v1_model_destroy(m)); + } + return 0; +} + +static int native_starts(void) { + gecode_opt_handle model=0,result=0,old=0; + gecode_opt_id b,x,gate,indicator; + gecode_opt_term terms[2];gecode_opt_start start[3]; + gecode_opt_options_v1 options;gecode_opt_result_info_v1 info; + int32_t native=0,lp=0,mip=0,has_gate=0,present=0;int maximize; + double value=0; + OK(gecode_opt_v1_capabilities(GECODE_OPT_NATIVE,&native,&lp,&mip)); + OK(gecode_opt_v1_model_create(&model)); + OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_BINARY,0,1,"b",&b)); + OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_INTEGER,0,4,"x",&x)); + terms[0].variable=x;terms[0].coefficient=1; + OK(gecode_opt_v1_model_add_indicator(model,b,1,terms,1,3,INFINITY,"enabled demand",&indicator,&has_gate,&gate)); + CHECK(has_gate);terms[1].variable=b;terms[1].coefficient=-10; + OK(gecode_opt_v1_options_default(&options,sizeof(options))); + options.backend=GECODE_OPT_NATIVE;options.guarantee=GECODE_OPT_EXACT; + options.primal_start=start;options.primal_start_count=2; + start[0].variable=b;start[1].variable=x;start[2].variable=gate; + for(maximize=0;maximize!=2;++maximize) { + /* The deliberately poor original point omits its live derived gate. */ + start[0].value=maximize?1:0;start[1].value=maximize?3:4; + OK(gecode_opt_v1_model_set_objective(model,terms,2,maximize?GECODE_OPT_MAXIMIZE:GECODE_OPT_MINIMIZE,3)); + OK(gecode_opt_v1_solve(model,&options,&result));OK(gecode_opt_v1_result_info(result,&info,sizeof(info))); + CHECK(info.termination==(native?GECODE_OPT_OPTIMAL:GECODE_OPT_UNSUPPORTED)); + CHECK(info.start_submitted==native&&info.has_solution==native); + if(native) { + CHECK(info.solution_validated&&info.guarantee==GECODE_OPT_EXACT); + OK(gecode_opt_v1_result_number(result,GECODE_OPT_OBJECTIVE,&present,&value));CHECK(present&&value==(maximize?7:-4)); + OK(gecode_opt_v1_result_number(result,GECODE_OPT_BEST_BOUND,&present,&value));CHECK(present&&value==(maximize?7:-4)); + OK(gecode_opt_v1_result_value(result,b,&value));CHECK(value==(maximize?0:1)); + OK(gecode_opt_v1_result_value(result,x,&value));CHECK(value==(maximize?4:3)); + OK(gecode_opt_v1_result_value(result,gate,&value));CHECK(value==(maximize?1:0)); + } + if(!maximize)old=result;else OK(gecode_opt_v1_result_destroy(result)); + } + if(native) { + start[0].value=0;start[1].value=4;start[2].value=0;options.primal_start_count=3; + OK(gecode_opt_v1_solve(model,&options,&result));OK(gecode_opt_v1_result_info(result,&info,sizeof(info))); + CHECK(info.termination==GECODE_OPT_INVALID_MODEL&&!info.start_submitted&&!info.has_solution);OK(gecode_opt_v1_result_destroy(result)); + options.primal_start_count=1; + OK(gecode_opt_v1_solve(model,&options,&result));OK(gecode_opt_v1_result_info(result,&info,sizeof(info))); + CHECK(info.termination==GECODE_OPT_UNSUPPORTED&&!info.start_submitted);OK(gecode_opt_v1_result_destroy(result)); + options.primal_start_count=2;start[0].value=1-1e-12;start[1].value=3; + OK(gecode_opt_v1_solve(model,&options,&result));OK(gecode_opt_v1_result_info(result,&info,sizeof(info))); + CHECK(info.termination==GECODE_OPT_INVALID_MODEL&&!info.has_solution);OK(gecode_opt_v1_result_destroy(result)); + start[0].value=0;start[1].value=4;options.has_node_limit=1;options.node_limit=0; + OK(gecode_opt_v1_solve(model,&options,&result));OK(gecode_opt_v1_result_info(result,&info,sizeof(info))); + CHECK(info.termination==GECODE_OPT_NODE_LIMIT&&!info.start_submitted&&!info.has_solution);OK(gecode_opt_v1_result_destroy(result)); + options.has_node_limit=0; + OK(gecode_opt_v1_model_remove_indicator(model,indicator)); + OK(gecode_opt_v1_model_set_objective(model,NULL,0,GECODE_OPT_MINIMIZE,11)); + OK(gecode_opt_v1_solve(model,&options,&result));OK(gecode_opt_v1_result_info(result,&info,sizeof(info))); + CHECK(info.termination==GECODE_OPT_UNSUPPORTED);OK(gecode_opt_v1_result_destroy(result)); + /* Removed gate is ordinary state: accept explicit zero even though b=0. */ + options.primal_start_count=3; + OK(gecode_opt_v1_solve(model,&options,&result));OK(gecode_opt_v1_result_info(result,&info,sizeof(info))); + CHECK(info.termination==GECODE_OPT_OPTIMAL&&info.start_submitted); + start[1].value=99;start[2].value=99; + OK(gecode_opt_v1_result_value(result,x,&value));CHECK(value==4); + OK(gecode_opt_v1_result_value(result,gate,&value));CHECK(value==0); + OK(gecode_opt_v1_result_destroy(result)); + } + OK(gecode_opt_v1_model_destroy(model)); + if(native){OK(gecode_opt_v1_result_value(old,x,&value));CHECK(value==3); + OK(gecode_opt_v1_result_number(old,GECODE_OPT_OBJECTIVE,&present,&value));CHECK(present&&value==-4);} + OK(gecode_opt_v1_result_destroy(old));return 0; +} + +static int quadratic(void) { + gecode_opt_handle m=0,other_model=0,linear=0,result=0,old=0,cancel=0,session=0; + gecode_opt_id x,dead,other,row; + gecode_opt_term term,linear_term; + gecode_opt_weighted_square_v1 square; + gecode_opt_quadratic_options_v1 options; + gecode_opt_quadratic_info_v1 info; + gecode_opt_quadratic_checks_v1 checks; + gecode_opt_result_info_v1 linear_info; + uint64_t owner=0,revision=0,after=0,needed=0; + int32_t available=0,present=0; + double value=0,values[2]={44,55};uint8_t active[2]={7,7},valid[2]={7,7}; + memset(&square,0,sizeof(square));square.struct_size=sizeof(square);square.weight=2;square.offset=-2; + OK(gecode_opt_v1_quadratic_capabilities(&available)); + CHECK(gecode_opt_v1_quadratic_capabilities(NULL)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_quadratic_options_default(&options,sizeof(options)-1)==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_quadratic_options_default(&options,sizeof(options))); + CHECK(options.struct_size==sizeof(options)&&options.solve.struct_size==sizeof(options.solve)); + CHECK(options.reserved==0&&options.solve.reserved==0); + CHECK(options.iteration_limit==100000&&near(options.optimality_tolerance,1e-6)); + CHECK(gecode_opt_v1_quadratic_model_create(NULL)==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_quadratic_model_create(&m));OK(gecode_opt_v1_quadratic_model_create(&other_model)); + OK(gecode_opt_v1_model_create(&linear));OK(gecode_opt_v1_session_create(&session)); + OK(gecode_opt_v1_quadratic_model_add_continuous(m,0,1,"hole",&dead)); + OK(gecode_opt_v1_quadratic_model_remove_variable(m,dead)); + OK(gecode_opt_v1_quadratic_model_add_continuous(m,-4,4,"x",&x)); + OK(gecode_opt_v1_quadratic_model_add_continuous(other_model,-4,4,"foreign",&other)); + CHECK(gecode_opt_v1_quadratic_model_add_continuous(m,0,INFINITY,"",&row)==GECODE_OPT_MODEL_ERROR); + CHECK(gecode_opt_v1_quadratic_model_add_continuous(m,NAN,1,"",&row)==GECODE_OPT_MODEL_ERROR); + CHECK(gecode_opt_v1_model_destroy(m)==GECODE_OPT_INVALID_HANDLE); + CHECK(gecode_opt_v1_quadratic_model_destroy(linear)==GECODE_OPT_INVALID_HANDLE); + CHECK(gecode_opt_v1_solve(m,NULL,&result)==GECODE_OPT_INVALID_HANDLE&&result==0); + CHECK(gecode_opt_v1_session_solve(session,m,NULL,&result)==GECODE_OPT_INVALID_HANDLE&&result==0); + CHECK(gecode_opt_v1_pool_solve(m,NULL,&result)==GECODE_OPT_INVALID_HANDLE&&result==0); + CHECK(gecode_opt_v1_repair_solve(m,NULL,&result)==GECODE_OPT_INVALID_HANDLE&&result==0); + CHECK(gecode_opt_v1_quadratic_solve(linear,NULL,&result)==GECODE_OPT_INVALID_HANDLE&&result==0); + CHECK(gecode_opt_v1_model_set_variable_bounds(m,x,0,1)==GECODE_OPT_INVALID_HANDLE); + term.variable=x;term.coefficient=1;square.terms=&term;square.term_count=1; + linear_term.variable=x;linear_term.coefficient=4; + /* 2(x-2)^2 + 4x - 7 has minimum -1 at x=1. */ + OK(gecode_opt_v1_quadratic_model_set_objective(m,&square,1,&linear_term,1,GECODE_OPT_MINIMIZE,-7)); + OK(gecode_opt_v1_quadratic_model_identity(m,&owner,&revision)); + square.struct_size--;CHECK(gecode_opt_v1_quadratic_model_set_objective(m,&square,1,NULL,0,0,0)==GECODE_OPT_INVALID_ARGUMENT);square.struct_size++; + square.reserved=1;CHECK(gecode_opt_v1_quadratic_model_set_objective(m,&square,1,NULL,0,0,0)==GECODE_OPT_INVALID_ARGUMENT);square.reserved=0; + square.terms=NULL;CHECK(gecode_opt_v1_quadratic_model_set_objective(m,&square,1,NULL,0,0,0)==GECODE_OPT_INVALID_ARGUMENT);square.terms=&term; + CHECK(gecode_opt_v1_quadratic_model_set_objective(m,NULL,1,NULL,0,0,0)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_quadratic_model_set_objective(m,&square,UINT64_MAX,NULL,0,0,0)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_quadratic_model_set_objective(m,&square,1,NULL,1,0,0)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_quadratic_model_set_objective(m,&square,1,NULL,0,99,0)==GECODE_OPT_INVALID_ARGUMENT); + square.weight=0;CHECK(gecode_opt_v1_quadratic_model_set_objective(m,&square,1,NULL,0,0,0)==GECODE_OPT_MODEL_ERROR);square.weight=2; + square.offset=INFINITY;CHECK(gecode_opt_v1_quadratic_model_set_objective(m,&square,1,NULL,0,0,0)==GECODE_OPT_MODEL_ERROR);square.offset=-2; + square.name="\xe2";CHECK(gecode_opt_v1_quadratic_model_set_objective(m,&square,1,NULL,0,0,0)==GECODE_OPT_INVALID_ARGUMENT);square.name=NULL; + term.variable=other;CHECK(gecode_opt_v1_quadratic_model_set_objective(m,&square,1,NULL,0,0,0)==GECODE_OPT_MODEL_ERROR); + term.variable=dead;CHECK(gecode_opt_v1_quadratic_model_set_objective(m,&square,1,NULL,0,0,0)==GECODE_OPT_MODEL_ERROR);term.variable=x; + CHECK(gecode_opt_v1_quadratic_model_remove_variable(m,x)==GECODE_OPT_MODEL_ERROR); + OK(gecode_opt_v1_quadratic_model_identity(m,&owner,&after));CHECK(after==revision); + OK(gecode_opt_v1_quadratic_solve(m,NULL,&result)); + OK(gecode_opt_v1_quadratic_result_info(result,&info,sizeof(info))); + CHECK(info.result.model_id==owner&&info.result.revision==revision&&info.result.variable_slots==2); + CHECK(info.result.reserved==0); + CHECK(info.result.termination==(available?GECODE_OPT_OPTIMAL:GECODE_OPT_UNSUPPORTED)); + CHECK(gecode_opt_v1_result_info(result,&linear_info,sizeof(linear_info))==GECODE_OPT_INVALID_HANDLE); + CHECK(gecode_opt_v1_result_destroy(result)==GECODE_OPT_INVALID_HANDLE); + CHECK(gecode_opt_v1_quadratic_result_info(result,&info,sizeof(info)-1)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_quadratic_result_checks(result,&checks,sizeof(checks)-1)==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_quadratic_result_checks(result,&checks,sizeof(checks))); + CHECK(checks.reserved==0&&checks.max_stationarity.reserved==0&&checks.original_objective.reserved==0); + OK(gecode_opt_v1_quadratic_result_values(result,NULL,NULL,NULL,0,&needed));CHECK(needed==2); + CHECK(gecode_opt_v1_quadratic_result_values(result,values,active,valid,1,&needed)==GECODE_OPT_BUFFER_TOO_SMALL); + CHECK(values[0]==44&&active[0]==7&&valid[0]==7); + CHECK(gecode_opt_v1_quadratic_result_values(result,values,NULL,valid,2,&needed)==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_quadratic_result_values(result,values,active,valid,2,&needed)); + CHECK(!active[0]&&!valid[0]&&values[0]==0&&active[1]); + OK(gecode_opt_v1_quadratic_result_number(result,GECODE_OPT_OBJECTIVE,&present,&value)); + CHECK(present==available); + if(available){ + CHECK(near(value,-1)&&near(values[1],1)&&valid[1]); + CHECK(checks.primal_valid&&checks.objective_valid&&checks.kkt_available&&checks.kkt_valid&&checks.bound_valid); + CHECK(checks.original_objective.present&&near(checks.original_objective.value,-1)); + CHECK(checks.gap_upper_bound.present&&checks.gap_upper_bound.value<=options.optimality_tolerance); + CHECK(checks.max_stationarity.present&&checks.max_complementarity.present); + CHECK(checks.square_count==1&&checks.gradient_slots==2); + OK(gecode_opt_v1_quadratic_result_array(result,GECODE_OPT_QP_SQUARE_RESIDUALS,values,2,&needed));CHECK(needed==1&&near(values[0],-1)); + values[0]=44;CHECK(gecode_opt_v1_quadratic_result_array(result,GECODE_OPT_QP_ORIGINAL_GRADIENT,values,1,&needed)==GECODE_OPT_BUFFER_TOO_SMALL);CHECK(values[0]==44); + OK(gecode_opt_v1_quadratic_result_array(result,GECODE_OPT_QP_ORIGINAL_GRADIENT,values,2,&needed));CHECK(near(values[0],0)&&near(values[1],0)); + CHECK(gecode_opt_v1_quadratic_result_value(result,other,&value)==GECODE_OPT_MODEL_ERROR); + CHECK(gecode_opt_v1_quadratic_result_value(result,dead,&value)==GECODE_OPT_MODEL_ERROR); + }else{ + CHECK(!checks.primal_valid&&!checks.objective_valid&&!checks.kkt_available&&!checks.kkt_valid&&!checks.bound_valid); + CHECK(!checks.original_objective.present&&!checks.normalized_lower_bound.present&&!checks.gap_upper_bound.present); + CHECK(!checks.max_stationarity.present&&!checks.max_complementarity.present&&checks.gradient_slots==0); + CHECK(gecode_opt_v1_quadratic_result_value(result,x,&value)==GECODE_OPT_NO_SOLUTION); + } + CHECK(gecode_opt_v1_quadratic_result_array(result,99,NULL,0,&needed)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_quadratic_result_number(result,99,&present,&value)==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_quadratic_result_number(result,GECODE_OPT_QP_VENDOR_OBJECTIVE,&present,&value)); + CHECK(present==available); + {char buffer[256];buffer[0]='Z'; + OK(gecode_opt_v1_quadratic_result_text(result,GECODE_OPT_BACKEND_NAME,NULL,0,&needed));CHECK(needed>1); + CHECK(gecode_opt_v1_quadratic_result_text(result,GECODE_OPT_BACKEND_NAME,buffer,1,&needed)==GECODE_OPT_BUFFER_TOO_SMALL);CHECK(buffer[0]=='Z'); + OK(gecode_opt_v1_quadratic_result_text(result,GECODE_OPT_QP_CHECK_MESSAGE,buffer,sizeof(buffer),&needed));} + old=result;result=0; + OK(gecode_opt_v1_quadratic_model_add_row(m,&term,1,2,INFINITY,"demand",&row)); + OK(gecode_opt_v1_quadratic_model_set_coefficient(m,row,x,2)); + OK(gecode_opt_v1_quadratic_model_set_row_bounds(m,row,4,INFINITY)); + OK(gecode_opt_v1_quadratic_solve(m,&options,&result)); + if(available){OK(gecode_opt_v1_quadratic_result_value(result,x,&value));CHECK(near(value,2));} + OK(gecode_opt_v1_quadratic_result_destroy(result)); + OK(gecode_opt_v1_quadratic_model_remove_row(m,row)); + options.struct_size--;CHECK(gecode_opt_v1_quadratic_solve(m,&options,&result)==GECODE_OPT_INVALID_ARGUMENT&&result==0);options.struct_size++; + options.solve.struct_size--;CHECK(gecode_opt_v1_quadratic_solve(m,&options,&result)==GECODE_OPT_INVALID_ARGUMENT);options.solve.struct_size++; + options.reserved=1;CHECK(gecode_opt_v1_quadratic_solve(m,&options,&result)==GECODE_OPT_INVALID_ARGUMENT);options.reserved=0; + options.solve.reserved=1;CHECK(gecode_opt_v1_quadratic_solve(m,&options,&result)==GECODE_OPT_INVALID_ARGUMENT);options.solve.reserved=0; + options.optimality_tolerance=NAN;CHECK(gecode_opt_v1_quadratic_solve(m,&options,&result)==GECODE_OPT_MODEL_ERROR);options.optimality_tolerance=1e-6; + options.max_lifted_nonzeros=UINT64_MAX;CHECK(gecode_opt_v1_quadratic_solve(m,&options,&result)==GECODE_OPT_INVALID_ARGUMENT);options.max_lifted_nonzeros=2000000; + options.solve.backend=GECODE_OPT_NATIVE; + OK(gecode_opt_v1_quadratic_solve(m,&options,&result));OK(gecode_opt_v1_quadratic_result_info(result,&info,sizeof(info))); + CHECK(info.result.termination==GECODE_OPT_UNSUPPORTED);OK(gecode_opt_v1_quadratic_result_destroy(result));options.solve.backend=GECODE_OPT_AUTO; + options.solve.guarantee=GECODE_OPT_EXACT; + OK(gecode_opt_v1_quadratic_solve(m,&options,&result));OK(gecode_opt_v1_quadratic_result_info(result,&info,sizeof(info))); + CHECK(info.result.termination==GECODE_OPT_UNSUPPORTED);OK(gecode_opt_v1_quadratic_result_destroy(result));options.solve.guarantee=GECODE_OPT_NUMERICAL; + OK(gecode_opt_v1_cancellation_create(&cancel));OK(gecode_opt_v1_cancellation_cancel(cancel));options.solve.cancellation=cancel; + OK(gecode_opt_v1_quadratic_solve(m,&options,&result));OK(gecode_opt_v1_quadratic_result_info(result,&info,sizeof(info))); + CHECK(info.result.termination==GECODE_OPT_CANCELLED); + OK(gecode_opt_v1_quadratic_result_checks(result,&checks,sizeof(checks))); + CHECK(!checks.kkt_available&&!checks.max_stationarity.present&&!checks.max_complementarity.present); + OK(gecode_opt_v1_quadratic_result_destroy(result)); + OK(gecode_opt_v1_cancellation_destroy(cancel));options.solve.cancellation=0; + options.solve.time_limit_seconds=0; + OK(gecode_opt_v1_quadratic_solve(m,&options,&result));OK(gecode_opt_v1_quadratic_result_info(result,&info,sizeof(info))); + CHECK(info.result.termination==GECODE_OPT_TIME_LIMIT);OK(gecode_opt_v1_quadratic_result_destroy(result)); + OK(gecode_opt_v1_quadratic_model_destroy(m));OK(gecode_opt_v1_quadratic_model_destroy(other_model)); + OK(gecode_opt_v1_model_destroy(linear));OK(gecode_opt_v1_session_destroy(session)); + OK(gecode_opt_v1_quadratic_result_info(old,&info,sizeof(info)));CHECK(info.result.revision==revision); + if(available){OK(gecode_opt_v1_quadratic_result_value(old,x,&value));CHECK(near(value,1));} + OK(gecode_opt_v1_quadratic_result_destroy(old)); + CHECK(gecode_opt_v1_quadratic_result_destroy(old)==GECODE_OPT_INVALID_HANDLE); + CHECK(gecode_opt_v1_quadratic_model_identity(m,&owner,&revision)==GECODE_OPT_INVALID_HANDLE); + return 0; +} + +int main(void){ + gecode_opt_options_v1 options; + gecode_opt_handle model=0,foreign=0,session=0,result=0,old=0,cancel=0; + gecode_opt_id x,dead,other,row; + gecode_opt_term term; + gecode_opt_result_info_v1 info; + int32_t available=0,lp=0,mip=0,present=0; + uint64_t owner=0,revision=0,needed=0; + double value=0; + CHECK(gecode_opt_v1_abi_version()==1); + CHECK(globals_and_logic()==0); + CHECK(workflows()==0);CHECK(bulk()==0);CHECK(quadratic()==0); + CHECK(gecode_opt_v1_options_default(NULL,sizeof(options))==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_options_default(&options,sizeof(options)-1)==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_options_default(&options,sizeof(options))); + CHECK(options.struct_size==sizeof(options)&&options.threads==1); + CHECK(gecode_opt_v1_capabilities(-1,&available,&lp,&mip)==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_capabilities(GECODE_OPT_HIGHS,&available,&lp,&mip)); + CHECK(gecode_opt_v1_model_create(NULL)==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_model_create(&model));OK(gecode_opt_v1_model_create(&foreign)); + CHECK(model!=foreign&&model!=0); + CHECK(gecode_opt_v1_model_add_variable(model,-1,0,1,"bad",&x)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_CONTINUOUS,NAN,1,"bad",&x)==GECODE_OPT_MODEL_ERROR); + OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_INTEGER,0,6,"x",&x)); + OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_BINARY,0,1,"dead",&dead)); + OK(gecode_opt_v1_model_remove_variable(model,dead)); + CHECK(gecode_opt_v1_model_set_variable_bounds(model,dead,0,1)==GECODE_OPT_MODEL_ERROR); + OK(gecode_opt_v1_model_add_variable(foreign,GECODE_OPT_INTEGER,0,6,"other",&other)); + term.variable=other;term.coefficient=1; + CHECK(gecode_opt_v1_model_add_row(model,&term,1,2,INFINITY,"foreign",&row)==GECODE_OPT_MODEL_ERROR); + CHECK(gecode_opt_v1_model_add_row(model,NULL,1,2,INFINITY,"null",&row)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_model_add_row(model,&term,UINT64_MAX,2,INFINITY,"count",&row)==GECODE_OPT_INVALID_ARGUMENT); + term.variable=x;term.coefficient=INFINITY; + CHECK(gecode_opt_v1_model_add_row(model,&term,1,2,INFINITY,"inf",&row)==GECODE_OPT_INVALID_ARGUMENT); + term.coefficient=1; + OK(gecode_opt_v1_model_add_row(model,&term,1,2,INFINITY,"demand",&row)); + CHECK(row.kind==GECODE_OPT_ROW_ID&&x.kind==GECODE_OPT_VARIABLE_ID); + CHECK(gecode_opt_v1_model_set_variable_bounds(model,row,0,1)==GECODE_OPT_INVALID_ARGUMENT); + term.coefficient=2; + OK(gecode_opt_v1_model_set_objective(model,&term,1,GECODE_OPT_MINIMIZE,-3)); + CHECK(gecode_opt_v1_model_set_objective(model,&term,1,-1,0)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_model_remove_variable(model,x)==GECODE_OPT_MODEL_ERROR); + OK(gecode_opt_v1_model_identity(model,&owner,&revision));CHECK(owner==x.model_id&&revision>0); + OK(gecode_opt_v1_session_create(&session)); + CHECK(gecode_opt_v1_model_destroy(session)==GECODE_OPT_INVALID_HANDLE); + options.backend=GECODE_OPT_HIGHS; + OK(gecode_opt_v1_session_solve(session,model,&options,&result)); + OK(gecode_opt_v1_result_info(result,&info,sizeof(info))); + CHECK(info.termination==(available?GECODE_OPT_OPTIMAL:GECODE_OPT_UNSUPPORTED)); + CHECK(info.has_solution==available); + OK(gecode_opt_v1_result_number(result,GECODE_OPT_OBJECTIVE,&present,&value)); + CHECK(present==available);CHECK(available?near(value,1):value==0); + CHECK(gecode_opt_v1_result_info(result,&info,sizeof(info)-1)==GECODE_OPT_INVALID_ARGUMENT); + CHECK(gecode_opt_v1_result_number(result,-1,&present,&value)==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_result_text(result,GECODE_OPT_BACKEND_NAME,NULL,0,&needed));CHECK(needed>1); + {char buffer[128];buffer[0]='Z'; + CHECK(gecode_opt_v1_result_text(result,GECODE_OPT_BACKEND_NAME,buffer,1,&needed)==GECODE_OPT_BUFFER_TOO_SMALL); + CHECK(buffer[0]=='Z'&&gecode_opt_v1_last_error()[0]); + OK(gecode_opt_v1_result_text(result,GECODE_OPT_BACKEND_NAME,buffer,sizeof(buffer),&needed));CHECK(strcmp(buffer,"HiGHS")==0); + CHECK(gecode_opt_v1_last_error()[0]=='\0');} + if(available){ + double values[2]={88,99};uint8_t active[2]={7,7},valid[2]={7,7}; + OK(gecode_opt_v1_result_values(result,NULL,NULL,NULL,0,&needed));CHECK(needed==2); + CHECK(gecode_opt_v1_result_values(result,values,active,valid,1,&needed)==GECODE_OPT_BUFFER_TOO_SMALL); + CHECK(values[0]==88&&active[0]==7&&valid[0]==7); + OK(gecode_opt_v1_result_values(result,values,active,valid,2,&needed)); + CHECK(active[0]&&valid[0]&&near(values[0],2));CHECK(!active[1]&&!valid[1]&&values[1]==0); + CHECK(gecode_opt_v1_result_value(result,other,&value)==GECODE_OPT_MODEL_ERROR); + CHECK(gecode_opt_v1_result_value(result,dead,&value)==GECODE_OPT_MODEL_ERROR); + old=result;result=0; + OK(gecode_opt_v1_model_set_variable_bounds(model,x,4,6)); + OK(gecode_opt_v1_session_solve(session,model,&options,&result)); + OK(gecode_opt_v1_result_number(result,GECODE_OPT_OBJECTIVE,&present,&value));CHECK(present&&near(value,5)); + OK(gecode_opt_v1_result_value(old,x,&value));CHECK(near(value,2)); + {gecode_opt_session_statistics_v1 stats; + OK(gecode_opt_v1_session_statistics(session,&stats,sizeof(stats)));CHECK(stats.model_loads==1&&stats.incremental_updates==1);} + OK(gecode_opt_v1_result_destroy(old)); + }else CHECK(gecode_opt_v1_result_value(result,x,&value)==GECODE_OPT_NO_SOLUTION); + OK(gecode_opt_v1_result_destroy(result)); + options.reserved=1; + CHECK(gecode_opt_v1_solve(model,&options,&result)==GECODE_OPT_INVALID_ARGUMENT);CHECK(result==0); + options.reserved=0;options.backend=99; + CHECK(gecode_opt_v1_solve(model,&options,&result)==GECODE_OPT_INVALID_ARGUMENT); + options.backend=GECODE_OPT_HIGHS;options.primal_start_count=1;options.primal_start=NULL; + CHECK(gecode_opt_v1_solve(model,&options,&result)==GECODE_OPT_INVALID_ARGUMENT); + options.primal_start_count=0;options.time_limit_seconds=NAN; + CHECK(gecode_opt_v1_solve(model,&options,&result)==GECODE_OPT_MODEL_ERROR); + options.time_limit_seconds=INFINITY;options.guarantee=GECODE_OPT_CERTIFIED; + OK(gecode_opt_v1_solve(model,&options,&result));OK(gecode_opt_v1_result_info(result,&info,sizeof(info))); + CHECK(info.termination==GECODE_OPT_UNSUPPORTED);OK(gecode_opt_v1_result_destroy(result));options.guarantee=GECODE_OPT_NUMERICAL; + OK(gecode_opt_v1_cancellation_create(&cancel));OK(gecode_opt_v1_cancellation_cancel(cancel));options.cancellation=cancel; + OK(gecode_opt_v1_solve(model,&options,&result));OK(gecode_opt_v1_result_info(result,&info,sizeof(info))); + CHECK(info.termination==GECODE_OPT_CANCELLED);OK(gecode_opt_v1_result_destroy(result)); + OK(gecode_opt_v1_cancellation_destroy(cancel));options.cancellation=0; + {int32_t native=0;OK(gecode_opt_v1_capabilities(GECODE_OPT_NATIVE,&native,&lp,&mip));options.backend=GECODE_OPT_NATIVE; + OK(gecode_opt_v1_solve(model,&options,&result));OK(gecode_opt_v1_result_info(result,&info,sizeof(info))); + CHECK(info.termination==(native?GECODE_OPT_OPTIMAL:GECODE_OPT_UNSUPPORTED));OK(gecode_opt_v1_result_destroy(result));} + options.backend=GECODE_OPT_HIGHS; + OK(gecode_opt_v1_solve(model,&options,&result)); + OK(gecode_opt_v1_session_destroy(session));OK(gecode_opt_v1_model_destroy(model));OK(gecode_opt_v1_model_destroy(foreign)); + CHECK(gecode_opt_v1_model_destroy(model)==GECODE_OPT_INVALID_HANDLE); + CHECK(gecode_opt_v1_model_identity(model,&owner,&revision)==GECODE_OPT_INVALID_HANDLE); + OK(gecode_opt_v1_result_info(result,&info,sizeof(info)));CHECK(info.model_id==x.model_id); + if(available){OK(gecode_opt_v1_result_value(result,x,&value));CHECK(near(value,4));} + OK(gecode_opt_v1_result_destroy(result));CHECK(gecode_opt_v1_result_destroy(result)==GECODE_OPT_INVALID_HANDLE); + CHECK(native_starts()==0); + puts("C99 ABI ownership, error, session and backend contracts passed");return 0; +} diff --git a/test/optimize/constraints.cpp b/test/optimize/constraints.cpp new file mode 100644 index 0000000000..eae3b5bafb --- /dev/null +++ b/test/optimize/constraints.cpp @@ -0,0 +1,257 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include + +#include +#include +#include +#include + +using namespace Gecode::Optimize; +namespace { +constexpr double inf = std::numeric_limits::infinity(); + +template void rejects(Action action) { + bool rejected = false; + try { action(); } catch (const ModelError&) { rejected = true; } + assert(rejected); +} + +bool linear_rows(const ModelSnapshot& model, const std::vector& values, + long double tolerance = 0.0L) { + for (const auto& row : model.rows) { + if (!row.active) continue; + long double value = 0; + for (const auto& term : row.terms) + value += static_cast(term.coefficient) * values[term.variable.id]; + if (value < static_cast(row.lower) - tolerance || + value > static_cast(row.upper) + tolerance) return false; + } + return true; +} + +void indicator_equivalence() { + for (bool active : {false, true}) + for (int kind = 0; kind < 3; ++kind) { + Model model; + auto b = model.add_binary("b"); + auto x = model.add_integer(-3, 3, "x"); + auto y = model.add_integer(-2, 2, "y"); + const double lower = kind == 1 ? -inf : -2.0; + const double upper = kind == 0 ? inf : 4.0; + const auto before = model.revision(); + auto formulation = add_indicator(model, b, active, + {{x, 2}, {y, -3}, {b, 1}}, lower, upper); + assert(model.revision() == before + 1); + assert(formulation.inactive_gate); + const auto snapshot = model.snapshot(); + validate_structure(snapshot); + assert(formulation.rows.size() == (kind == 2 ? 3U : 2U)); + for (int bv = 0; bv <= 1; ++bv) + for (int xv = -3; xv <= 3; ++xv) + for (int yv = -2; yv <= 2; ++yv) + for (int gv = 0; gv <= 1; ++gv) { + std::vector values(snapshot.variables.size()); + values[b.id] = bv; values[x.id] = xv; values[y.id] = yv; + values[formulation.inactive_gate->id] = gv; + const double original = 2 * xv - 3 * yv + bv; + const bool enabled = bv == static_cast(active); + const bool expected = (gv == static_cast(!enabled)) && + (!enabled || (original >= lower && original <= upper)); + assert(linear_rows(snapshot, values) == expected); + assert(validate(snapshot, values, 0.0, 0.0).valid == expected); + } + } +} + +void semi_domains_and_redundancy() { + for (auto type : {VariableType::SemiContinuous, VariableType::SemiInteger}) { + Model model; + auto b = model.add_binary(); + auto x = model.add_variable(type, 3, 6); + auto formulation = add_indicator(model, b, true, {{x, 1}}, 2, inf); + assert(formulation.lower_m && *formulation.lower_m >= 2); + auto snapshot = model.snapshot(); + assert(snapshot.indicators[0].domains[0].lower == 0.0); + for (int bv = 0; bv <= 1; ++bv) + for (int xv : {0, 3, 4, 5, 6}) { + std::vector values(snapshot.variables.size()); + values[b.id] = bv; values[x.id] = xv; + values[formulation.inactive_gate->id] = 1 - bv; + assert(validate(snapshot, values, 0, 0).valid == (bv == 0 || xv >= 2)); + } + } + Model model; + auto b = model.add_binary(); + auto x = model.add_continuous(0, inf); + // An unbounded upper endpoint is irrelevant to this lower-side relaxation. + auto redundant = add_indicator(model, b, true, {{x, 1}}, 0, inf); + assert(redundant.lower_m == 0.0 && !redundant.inactive_gate); + auto tautology = add_indicator(model, b, false, {{x, 1}}, -inf, inf); + assert(tautology.rows.empty() && !tautology.inactive_gate); + auto snapshot = model.snapshot(); + validate_structure(snapshot); + assert(validate(snapshot, {1, 1e100}, 0, 0).valid); + + Model constants; + auto c = constants.add_binary(); + auto impossible = add_indicator(constants, c, true, {}, 1, inf); + auto empty = constants.snapshot(); + assert(validate(empty, {0, 1}, 0, 0).valid); + assert(!validate(empty, {1, 0}, 0, 0).valid); + assert(impossible.inactive_gate); +} + +void preserve_active_arithmetic_and_check_original_logic() { + Model model; + auto b = model.add_binary(); + auto x = model.add_continuous(-1e12, 1e12); + const double tiny = 1e-9; + auto formulation = add_indicator(model, b, true, {{b, tiny}, {x, 1}}, tiny, inf); + const auto snapshot = model.snapshot(); + const auto& generated = model.row(formulation.rows.back()); + assert(generated.lower == tiny); + assert(generated.terms[0].variable == b && generated.terms[0].coefficient == tiny); + assert(*formulation.lower_m >= 1e12); + assert(validate(snapshot, {1, 0, 0}, 0, 0).valid); + assert(!validate(snapshot, {1, -1e-10, 0}, 0, 0).valid); + + // A near-zero gate can satisfy numerical M rows while violating the logic. + // Original metadata must reject it for either activation value. + for (bool active : {false, true}) { + Model guarded; + auto enabled = guarded.add_binary(); + auto amount = guarded.add_continuous(0, 1e12); + auto condition = add_indicator(guarded, enabled, active, {{amount, 1}}, 1e12, inf); + const auto original = guarded.snapshot(); + std::vector values(original.variables.size()); + values[enabled.id] = active ? 1.0 - 5e-8 : 5e-8; + values[amount.id] = 1e12 - 10000; + values[condition.inactive_gate->id] = 5e-8; + assert(linear_rows(original, values, 1e-7L)); + const auto checked = validate(original, values, 1e-7, 1e-6); + assert(!checked.valid && checked.model_valid && checked.max_indicator_violation >= 10000); + } +} + +void mutation_guards_and_snapshot_validation() { + Model model; + auto b = model.add_binary(); + auto x = model.add_continuous(-5, 5); + auto condition = add_indicator(model, b, true, {{x, 1}}, 2, 3); + const auto original = model.snapshot(); + model.set_bounds(x, -3, 4); + model.set_bounds(x, -5, 5); + const auto revision = model.revision(); + rejects([&] { model.set_bounds(x, -6, 5); }); + rejects([&] { model.set_bounds(x, -5, 6); }); + rejects([&] { model.set_bounds(*condition.inactive_gate, 0, 0); }); + rejects([&] { model.set_bounds(condition.rows.back(), 0, 9); }); + rejects([&] { model.set_coefficient(condition.rows.front(), b, 2); }); + rejects([&] { model.remove(condition.rows.front()); }); + rejects([&] { model.remove(b); }); + assert(model.revision() == revision); + auto bad = original; + bad.variables[x.id].lower = -6; + rejects([&] { validate_structure(bad); }); + bad = original; + bad.rows[condition.rows.back().id].upper = 4; + rejects([&] { validate_structure(bad); }); + bad = original; + bad.indicators[0].lower_m = 0; + rejects([&] { validate_structure(bad); }); + bad = original; + bad.indicators[0].domains[0].lower = -100; + rejects([&] { validate_structure(bad); }); + bad = original; + bad.variables[b.id].type = VariableType::Integer; + rejects([&] { validate_structure(bad); }); + bad = original; + bad.indicators[0].active = false; + rejects([&] { validate_structure(bad); }); + bad = original; + bad.indicators.clear(); + rejects([&] { validate_structure(bad); }); + assert(validate(original, {1, 2, 0}, 0, 0).valid); + remove_indicator(model, condition.indicator); + assert(model.revision() == revision + 1); + assert(!model.snapshot().indicators[0].active); + model.set_bounds(x, -20, 20); + model.remove(*condition.inactive_gate); + model.remove(x); + validate_structure(model.snapshot()); + rejects([&] { remove_indicator(model, condition.indicator); }); + Model moved(std::move(model)); + validate_structure(moved.snapshot()); + assert(moved.snapshot().indicators.size() == 1); +} + +void errors_are_atomic() { + Model model, other; + auto b = model.add_binary(); + auto x = model.add_continuous(-inf, inf); + auto integer = model.add_integer(0, 1); + auto foreign = other.add_binary(); + const auto before = model.snapshot(); + rejects([&] { add_indicator(model, b, true, {{x, 1}}, 0, inf); }); + rejects([&] { add_indicator(model, b, true, {{x, -1}}, -inf, 0); }); + rejects([&] { add_indicator(model, integer, true, {}, 0, 1); }); + rejects([&] { add_indicator(model, b, true, {{foreign, 0}}, 0, 1); }); + rejects([&] { add_indicator(model, foreign, true, {}, 0, 1); }); + rejects([&] { add_indicator(model, b, true, {}, 2, 1); }); + rejects([&] { add_indicator(model, b, true, {}, std::nan(""), 1); }); + rejects([&] { add_boolean_and(model, b, {foreign}); }); + rejects([&] { add_boolean_or(model, integer, {}); }); + rejects([&] { remove_indicator(model, Indicator{other.id(), 0}); }); + assert(model.revision() == before.revision); + auto after = model.snapshot(); + assert(after.variables.size() == before.variables.size() && after.rows.empty() && after.indicators.empty()); + model.set_bounds(x, -2, 2); + const auto revision = model.revision(); + rejects([&] { + add_indicator(model, b, true, {{x, std::numeric_limits::max()}}, -1, 1); + }); + assert(model.revision() == revision && model.snapshot().rows.empty()); +} + +void boolean_truth_tables() { + for (bool conjunction : {false, true}) + for (int count = 0; count <= 4; ++count) + for (bool alias_result : {false, true}) { + Model model; + auto result = model.add_binary("result"); + std::vector inputs; + for (int i = 0; i < count; ++i) + inputs.push_back(alias_result && i == 0 ? result : model.add_binary()); + if (count > 1) inputs.push_back(inputs.back()); // Duplicate is idempotent. + const auto revision = model.revision(); + const auto rows = conjunction ? add_boolean_and(model, result, inputs) + : add_boolean_or(model, result, inputs); + assert(!rows.empty() && model.revision() == revision + 1); + const auto snapshot = model.snapshot(); + validate_structure(snapshot); + for (unsigned mask = 0; mask < (1U << snapshot.variables.size()); ++mask) { + std::vector values(snapshot.variables.size()); + for (std::size_t i = 0; i < values.size(); ++i) values[i] = (mask >> i) & 1U; + bool expected = conjunction; + for (auto input : inputs) + expected = conjunction ? expected && values[input.id] != 0.0 + : expected || values[input.id] != 0.0; + const bool correct = values[result.id] == static_cast(expected); + assert(linear_rows(snapshot, values) == correct); + assert(validate(snapshot, values, 0, 0).valid == correct); + } + } +} +} + +int main() { + indicator_equivalence(); + semi_domains_and_redundancy(); + preserve_active_arithmetic_and_check_original_logic(); + mutation_guards_and_snapshot_validation(); + errors_are_atomic(); + boolean_truth_tables(); +} diff --git a/test/optimize/consumer/CMakeLists.txt b/test/optimize/consumer/CMakeLists.txt new file mode 100644 index 0000000000..c81544e6fa --- /dev/null +++ b/test/optimize/consumer/CMakeLists.txt @@ -0,0 +1,56 @@ +cmake_minimum_required(VERSION 3.21) +project(OptimizeInstalledConsumer LANGUAGES C CXX) +option(EXPECT_BACKEND_DISABLED "Installed package intentionally has no HiGHS adapter" OFF) +set(PACKAGE_MODE "standalone" CACHE STRING "standalone, combined, or native-only") +if(PACKAGE_MODE STREQUAL "standalone") + find_package(GecodeOptimize CONFIG REQUIRED) + add_executable(consumer main.cpp) + target_link_libraries(consumer PRIVATE Gecode::optimize) +elseif(PACKAGE_MODE STREQUAL "combined") + find_package(Gecode CONFIG REQUIRED COMPONENTS int optimize) + add_executable(consumer main.cpp) + target_link_libraries(consumer PRIVATE Gecode::gecodeint Gecode::optimize) + target_compile_definitions(consumer PRIVATE CHECK_NATIVE=1) +elseif(PACKAGE_MODE STREQUAL "native-only") + # This must configure even if the installed producer enabled HiGHS. + set(CMAKE_DISABLE_FIND_PACKAGE_highs TRUE) + find_package(Gecode CONFIG REQUIRED COMPONENTS int) + add_executable(consumer main.cpp) + target_link_libraries(consumer PRIVATE Gecode::gecodeint) + target_compile_definitions(consumer PRIVATE CHECK_NATIVE=1 NATIVE_ONLY=1) +else() + message(FATAL_ERROR "Unknown PACKAGE_MODE") +endif() +if(EXPECT_BACKEND_DISABLED) + target_compile_definitions(consumer PRIVATE EXPECT_BACKEND_DISABLED=1) +endif() +enable_testing() +add_test(NAME installed-consumer COMMAND consumer) +if(NOT PACKAGE_MODE STREQUAL "native-only") + add_executable(c-consumer ../c_api.c) + set_target_properties(c-consumer PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + target_link_libraries(c-consumer PRIVATE Gecode::optimize_c) + add_test(NAME installed-c-consumer COMMAND c-consumer) + add_executable(lp-c-consumer ../lp_observations_c.c) + set_target_properties(lp-c-consumer PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + target_link_libraries(lp-c-consumer PRIVATE Gecode::optimize_c) + add_test(NAME installed-lp-c-consumer COMMAND lp-c-consumer) + add_executable(scenarios-c-consumer ../scenarios_c.c) + set_target_properties(scenarios-c-consumer PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + target_link_libraries(scenarios-c-consumer PRIVATE Gecode::optimize_c) + add_test(NAME installed-scenarios-c-consumer COMMAND scenarios-c-consumer) + add_executable(lp-evidence-c-consumer ../lp_evidence_c.c) + set_target_properties(lp-evidence-c-consumer PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + target_link_libraries(lp-evidence-c-consumer PRIVATE Gecode::optimize_c) + add_test(NAME installed-lp-evidence-c-consumer COMMAND lp-evidence-c-consumer) + add_executable(lp-sensitivity-c-consumer ../lp_sensitivity_c.c) + set_target_properties(lp-sensitivity-c-consumer PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + target_link_libraries(lp-sensitivity-c-consumer PRIVATE Gecode::optimize_c) + add_test(NAME installed-lp-sensitivity-c-consumer COMMAND lp-sensitivity-c-consumer) + foreach(c_test lp_basis regular) + add_executable(${c_test}-c-consumer ../${c_test}_c_api.c) + set_target_properties(${c_test}-c-consumer PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + target_link_libraries(${c_test}-c-consumer PRIVATE Gecode::optimize_c) + add_test(NAME installed-${c_test}-c-consumer COMMAND ${c_test}-c-consumer) + endforeach() +endif() diff --git a/test/optimize/consumer/main.cpp b/test/optimize/consumer/main.cpp new file mode 100644 index 0000000000..4e255c85bb --- /dev/null +++ b/test/optimize/consumer/main.cpp @@ -0,0 +1,182 @@ +#ifdef CHECK_NATIVE +#include +class NativeModel : public Gecode::Space { +public: + Gecode::IntVar x; + NativeModel() : x(*this,0,1) {} + NativeModel(NativeModel& other) : Gecode::Space(other) { x.update(*this,other.x); } + Gecode::Space* copy() override { return new NativeModel(*this); } +}; +#endif +#ifndef NATIVE_ONLY +#include +#include +#include +#endif + +int main() { +#ifdef CHECK_NATIVE + NativeModel native; + Gecode::rel(native,native.x,Gecode::IRT_EQ,1); + if (native.status()==Gecode::SS_FAILED || !native.x.assigned() || native.x.val()!=1) return 1; +#endif +#ifndef NATIVE_ONLY + Gecode::Optimize::Model model; + auto x=model.add_variables({{Gecode::Optimize::VariableType::Integer,2,5,"x"}}).front(); + model.add_rows({{{{x,1}},2,5,"bounds"}}); + Gecode::Optimize::SparseRowBatch sparse; + sparse.columns={x};sparse.row_start={0,1};sparse.column={0};sparse.coefficient={1};sparse.lower={2};sparse.upper={5}; + model.add_rows_sparse(sparse); + model.minimize({{x,3}}); + auto prepared=Gecode::Optimize::presolve_integer(model); + if (!prepared.model || prepared.status!=Gecode::Optimize::PresolveStatus::Fixpoint + || prepared.model->original().model_id!=model.id()) return 7; + auto result=Gecode::Optimize::solve(model); +#ifdef EXPECT_BACKEND_DISABLED + if (result.termination!=Gecode::Optimize::Termination::Unsupported || result.has_solution()) return 2; +#else + if (!result.has_solution() || result.termination!=Gecode::Optimize::Termination::Optimal + || result.value(x)!=2 || *result.objective!=6) return 2; + Gecode::Optimize::SolveSession session; + if (!session.solve(model).has_solution()) return 3; + model.set_bounds(x,3,5); + auto edited=session.solve(model); + if (!edited.has_solution() || *edited.objective!=9 || session.statistics().model_loads!=1) return 4; + auto conflict=Gecode::Optimize::analyze_conflict(model); + if (conflict.status!=Gecode::Optimize::ConflictStatus::Feasible) return 5; + auto reduced=Gecode::Optimize::solve(prepared.model->reduced()); + auto recovered=prepared.model->postsolve(reduced); + if (!recovered.exact_witness_validated || recovered.solution.value(x)!=2 + || recovered.solution.termination!=Gecode::Optimize::Termination::Unknown) return 8; + Gecode::Optimize::PoolOptions pool_options; pool_options.max_solutions=4; + auto pool=Gecode::Optimize::solve_pool(model,pool_options); + if (!pool.exhausted() || pool.entries.size()!=3 || pool.ranked_prefix!=3 + || pool.entries[0].solution.objective!=9 || pool.entries[2].solution.objective!=15) return 9; +#endif +#ifdef CHECK_NATIVE + Gecode::Optimize::SolveOptions native_options; + native_options.backend=Gecode::Optimize::Backend::Native; + native_options.guarantee=Gecode::Optimize::Guarantee::Exact; + native_options.primal_start={{x,5}}; + auto native_result=Gecode::Optimize::solve(model,native_options); + if (!native_result.has_solution() || !native_result.start_submitted || native_result.guarantee!=Gecode::Optimize::Guarantee::Exact) return 6; + Gecode::Optimize::Model regular; + auto letter=regular.add_integer(-1,1); + Gecode::Optimize::add_regular(regular,{letter},2,0,{{0,-1,1}},{1},"signed word"); + regular.minimize({{letter,1}}); + native_options.primal_start.clear(); + auto accepted=Gecode::Optimize::solve(regular,native_options); + if (!accepted.has_solution() || accepted.objective!=-1 || accepted.best_bound!=accepted.objective) return 17; +#endif + auto hybrid=Gecode::Optimize::solve_native_lp(model); + if (Gecode::Optimize::native_lp_capabilities().available) { + if (!hybrid.result.has_solution() || hybrid.result.termination!=Gecode::Optimize::Termination::Optimal) return 10; + } else if (hybrid.result.termination!=Gecode::Optimize::Termination::Unsupported) return 11; + auto frontier=Gecode::Optimize::solve_native_search(model); +#ifdef CHECK_NATIVE + if (!frontier.result.has_solution() || frontier.result.termination!=Gecode::Optimize::Termination::Optimal + || frontier.result.best_bound!=frontier.result.objective) return 12; +#else + if (frontier.result.termination!=Gecode::Optimize::Termination::Unsupported) return 12; +#endif + auto neighborhood=Gecode::Optimize::solve_native_neighborhoods(model); + if (!neighborhood.neighborhood.requested) return 22; +#ifdef CHECK_NATIVE + if (!neighborhood.search.result.has_solution() + || neighborhood.search.result.termination!=Gecode::Optimize::Termination::Optimal + || neighborhood.neighborhood.budget_nodes!=neighborhood.search.frontier.admitted_nodes + +neighborhood.search.branching.probe_status_calls+neighborhood.neighborhood.status_attempts) return 22; +#else + if (neighborhood.search.result.termination!=Gecode::Optimize::Termination::Unsupported) return 22; +#endif + Gecode::Optimize::NativeSearchOptions branching_options; + branching_options.branching=Gecode::Optimize::NativeBranchingSettings{}; + auto branching=Gecode::Optimize::solve_native_search(model,branching_options); + if (!branching.branching.requested) return 14; +#ifdef CHECK_NATIVE + if (!branching.result.has_solution() || branching.result.termination!=Gecode::Optimize::Termination::Optimal + || branching.branching.budget_nodes!=branching.frontier.admitted_nodes+branching.branching.probe_status_calls) return 14; +#else + if (branching.result.termination!=Gecode::Optimize::Termination::Unsupported) return 14; +#endif + Gecode::Optimize::Model lp; + auto lx=lp.add_continuous(),ly=lp.add_continuous(); + auto demand=lp.add_row({{lx,1},{ly,1}},4,std::numeric_limits::infinity()); + lp.minimize({{lx,2},{ly,3}},7); + auto observed=Gecode::Optimize::solve_lp_observed(lp); +#ifdef EXPECT_BACKEND_DISABLED + if (Gecode::Optimize::lp_observation_capabilities().available + || observed.result.termination!=Gecode::Optimize::Termination::Unsupported) return 15; +#else + if (!observed.observations || observed.result.termination!=Gecode::Optimize::Termination::Optimal + || observed.result.objective!=15 || !observed.observations->checks().accepted + || observed.observations->row(demand).dual!=2 + || observed.observations->column(ly).reduced_cost!=1) return 15; + Gecode::Optimize::LpBasisSolveOptions basis_options; + basis_options.basis=Gecode::Optimize::make_lp_basis(*observed.observations); + auto basis_result=Gecode::Optimize::solve_lp_with_basis(lp,basis_options); + if (basis_result.observed.result.termination!=Gecode::Optimize::Termination::Optimal + || basis_result.observed.result.objective!=15 + || !basis_result.submission.backend_attempted + || (basis_result.submission.state!=Gecode::Optimize::LpBasisSubmissionState::Accepted + && basis_result.submission.state!=Gecode::Optimize::LpBasisSubmissionState::Repaired)) return 16; + Gecode::Optimize::LpSensitivityOptions sensitivity_options; + sensitivity_options.parameters={Gecode::Optimize::LpObjectiveParameter{lx}}; + auto sensitivity=Gecode::Optimize::analyze_lp_sensitivity(observed,sensitivity_options); + if (sensitivity.completion!=Gecode::Optimize::LpSensitivityCompletion::Complete + || !sensitivity.sensitivity) return 21; + const auto* range=sensitivity.sensitivity->objective(lx); + if (!range || range->group.state!=Gecode::Optimize::LpSensitivityState::Available + || !range->interval || range->interval->lower.value!=0 + || range->interval->upper.value!=3 || range->interval->objective_slope!=4 + || sensitivity.sensitivity->original().result.objective!=15) return 21; +#endif + std::vector scenarios(2); + scenarios[1].objective_offset=-5; + auto batch=Gecode::Optimize::solve_scenarios(lp,scenarios); +#ifdef EXPECT_BACKEND_DISABLED + if (batch.stop_reason!=Gecode::Optimize::Termination::Unsupported || batch.all_resolved()) return 18; +#else + if (!batch.all_resolved() || !batch.batch || batch.outcomes.size()!=2 + || !batch.outcomes[0].result || !batch.outcomes[1].result + || batch.outcomes[0].result->objective!=15 || batch.outcomes[1].result->objective!=3 + || batch.value(batch.batch->scenario(1),lx)!=4) return 18; +#endif + Gecode::Optimize::ScenarioBatchOptions certified_scenarios; + certified_scenarios.solve.guarantee=Gecode::Optimize::Guarantee::Certified; + auto unsupported_batch=Gecode::Optimize::solve_scenarios(lp,scenarios,certified_scenarios); + if (unsupported_batch.stop_reason!=Gecode::Optimize::Termination::Unsupported + || unsupported_batch.batch || unsupported_batch.attempted!=0) return 19; + Gecode::Optimize::QuadraticModel quadratic; + Gecode::Optimize::Model infeasible_lp; + auto bounded=infeasible_lp.add_continuous(0,1); + auto impossible=infeasible_lp.add_row({{bounded,1}},2,std::numeric_limits::infinity()); + Gecode::Optimize::LpEvidenceOptions evidence_options; + evidence_options.request=Gecode::Optimize::LpEvidenceRequest::Farkas; + auto evidence=Gecode::Optimize::analyze_lp_evidence(infeasible_lp,evidence_options); +#ifdef EXPECT_BACKEND_DISABLED + if (evidence.stop_reason!=Gecode::Optimize::Termination::Unsupported || evidence.attempted_calls) return 20; +#else + if (evidence.completion!=Gecode::Optimize::LpEvidenceCompletion::Complete || !evidence.evidence + || evidence.evidence->farkas().state!=Gecode::Optimize::LpEvidenceState::Available + || evidence.evidence->row_multiplier(impossible).multiplier!=1 + || evidence.evidence->column_multiplier(bounded).multiplier!=-1 + || evidence.evidence->farkas_data().contradiction_margin!=1 || evidence.attempted_calls!=1) return 20; +#endif + auto qx=quadratic.add_continuous(-2,3,"qx"); + quadratic.minimize_squares({{{{qx,1}},-1,2,"square"}}, {}, 3); + auto qp=Gecode::Optimize::solve_quadratic(quadratic); +#ifdef EXPECT_BACKEND_DISABLED + if (Gecode::Optimize::quadratic_capabilities().available + || qp.result.termination!=Gecode::Optimize::Termination::Unsupported + || qp.result.has_solution()) return 13; +#else + if (!Gecode::Optimize::quadratic_capabilities().available || !qp.result.has_solution() + || qp.result.termination!=Gecode::Optimize::Termination::Optimal + || std::abs(qp.result.value(qx)-1)>1e-7 || !qp.result.objective + || std::abs(*qp.result.objective-3)>1e-7 || !qp.checks.bound_valid + || !qp.checks.gap_upper_bound || *qp.checks.gap_upper_bound>1e-6) return 13; +#endif +#endif + return 0; +} diff --git a/test/optimize/cut_loop.cpp b/test/optimize/cut_loop.cpp new file mode 100644 index 0000000000..c2b05a17bf --- /dev/null +++ b/test/optimize/cut_loop.cpp @@ -0,0 +1,336 @@ +// Independent finite original-model oracle and actual numerical LP selection. +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +#include +#ifdef GECODE_LP_CUT_LOOP_TEST_NATIVE +#include +#endif + +namespace LP=Gecode::Experimental::LpRelaxation; +namespace Cuts=LP::Cuts; +using I=std::int64_t; +using Model=LP::BoundedIntegerModel; +static std::size_t runs=0,points=0; +#ifdef GECODE_LP_CUT_LOOP_TEST_HOOKS +static thread_local std::map events; +static thread_local std::string failed_event; +static thread_local unsigned failed_occurrence=0,event_occurrence=0; +static thread_local bool allocation_failure=false; +static thread_local bool cancel_on_event=false,cancelled_on_event=false; +namespace Gecode { namespace Experimental { namespace LpRelaxation { namespace Cuts { +void root_cut_loop_test_event(const char* name) { + ++events[name]; + if(failed_event!=name || ++event_occurrence!=failed_occurrence)return; + if(cancel_on_event){cancelled_on_event=true;return;} + if(allocation_failure)throw std::bad_alloc(); + throw std::runtime_error("injected backend/augmentation error"); +} +}}}} +#endif + +static Model model(LP::LinearModel dense,std::vector lower,std::vector upper) { + return {LP::sparse_model(dense),std::move(lower),std::move(upper)}; +} +static Model fixture() {return model({{-3,-3},{-5},{-2,-2}},{0,0},{1,1});} +static bool same(const Model& a,const Model& b) { + return a.lower==b.lower && a.upper==b.upper && a.linear.a==b.linear.a && + a.linear.b==b.linear.b && a.linear.c==b.linear.c && + a.linear.column==b.linear.column && a.linear.row_start==b.linear.row_start; +} +static bool feasible(const Model& m,const std::vector& x) { + for (std::size_t j=0;jm.upper[j]) return false; + for (std::size_t i=0;i& x) { + I sum=0;for(std::size_t j=0;j& cuts) { + const auto& original=source.model();const auto& m=augmented.model(); + assert(m.lower==original.lower && m.upper==original.upper && m.linear.c==original.linear.c); + const auto rows=original.linear.b.size(),nnz=original.linear.a.size(); + assert(m.linear.b.size()==rows+cuts.size()); + assert(std::equal(original.linear.row_start.begin(),original.linear.row_start.end(),m.linear.row_start.begin())); + assert(std::equal(original.linear.a.begin(),original.linear.a.end(),m.linear.a.begin())); + assert(std::equal(original.linear.column.begin(),original.linear.column.end(),m.linear.column.begin())); + assert(std::equal(original.linear.b.begin(),original.linear.b.end(),m.linear.b.begin())); + std::size_t start=nnz; + for (std::size_t i=0;imodel,result.model.model())); + if(result.best_bound) { + const auto& best=*result.best_bound; + assert(best.bound.valid && best.bound.certificate && !best.bound.primal_suggestion); + provenance(source,best.model,best.cuts); + I checked=0; + assert(best.bound.certificate->lower_bound_integer(source.model().lower,source.model().upper,checked)); + assert(checked==best.bound.lower_bound); + } + const auto& original=source.model();std::vector x(original.lower.size()); + std::optional optimum; + const auto visit=[&](const auto& self,std::size_t j)->void { + if(j==x.size()) { + ++points;const bool valid=feasible(original,x); + assert(valid==feasible(result.model.model(),x)); + if(result.best_bound) assert(valid==feasible(result.best_bound->model.model(),x)); + if(valid) {const auto value=objective(original,x);if(!optimum || value<*optimum) optimum=value;} + return; + } + for(I value=original.lower[j];value<=original.upper[j];++value){x[j]=value;self(self,j+1);} + }; + visit(visit,0); + if(optimum && result.best_bound) assert(result.best_bound->bound.lower_bound<=*optimum); +} +static void actual_lp_and_ownership() { + auto input=fixture();Cuts::SourceModel source(input); + auto old=std::make_shared(input); + const auto old_bound=old->bound(input.lower,input.upper,true,true); + assert(old_bound.valid && old_bound.lower_bound==-3 && old_bound.primal_suggestion && + std::abs(old_bound.lp_objective+10.0/3.0)<1e-8); + assert(!old->bound(input.lower,input.upper).primal_suggestion); + auto copied=old_bound;(*copied.primal_suggestion)[0]=999; + assert((*old_bound.primal_suggestion)[0]<=1); + input.linear.a[0]=11;input.upper[0]=0; + assert(source.model().linear.a[0]==-3 && old->model.upper[0]==1); + Cuts::RootLoopOptions options; + auto result=Cuts::root_cover_loop(source,options);evidence(source,options,result); + assert(result.completion==Cuts::RootLoopCompletion::NoNewCuts); + assert(result.stats.lp_calls>=2 && result.stats.augmentations==1 && result.cuts.size()==1); + assert(result.best_bound && result.best_bound->bound.lower_bound==-2); + assert(result.best_bound->model.model().linear.b.size()==2); + assert(old->model.linear.b.size()==1 && old->bound({0,0},{1,1},true).lower_bound==-3); + I checked=0;assert(old_bound.certificate->lower_bound_integer({0,0},{1,1},checked) && checked==-3); + auto historical=result;result.cuts.clear();result.backend.reset(); + assert(historical.cuts.size()==1 && historical.backend->model.linear.b.size()==2); + LP::SparseBackend binary(source.model().linear); + assert(binary.bound({0,0},{1,1},true,true).primal_suggestion); + assert(!binary.bound({0,0},{1,1},true).primal_suggestion); +} +static void rationalization() { + Cuts::SourceModel source(model({{},{},{0,0,0}},{-2,0,2},{2,1,2})); + const auto projected=Cuts::rational_selection_point(source,{-3,1.0001,99},8); + assert(projected.projected_coordinates==3 && projected.point.numerator==std::vector({-16,8,16})); + const auto rounded=Cuts::rational_selection_point(source,{-0.0625,0.0625,2},8); + assert(!rounded.projected_coordinates && rounded.point.numerator==std::vector({-1,1,16})); + for(I d:{I(0),I(-1),I(3),I(2097152)}) { + bool rejected=false;try{(void)Cuts::rational_selection_point(source,{0,0,2},d);} + catch(const std::invalid_argument&){rejected=true;}assert(rejected); + } + for(double bad:{std::numeric_limits::infinity(),std::numeric_limits::quiet_NaN()}) { + bool rejected=false;try{(void)Cuts::rational_selection_point(source,{bad,0,2});} + catch(const std::invalid_argument&){rejected=true;}assert(rejected); + } + bool rejected=false;try{(void)Cuts::rational_selection_point(source,{0});} + catch(const std::invalid_argument&){rejected=true;}assert(rejected); + const auto limit=Gecode::Int::Limits::max; + Cuts::SourceModel endpoints(model({{},{},{0,0}},{-limit,limit},{-limit,limit})); + const auto exact=Cuts::rational_selection_point(endpoints,{-double(limit),double(limit)}); + assert(exact.point.numerator==std::vector({-I(limit)*1048576,I(limit)*1048576})); +} +static void budgets() { + Cuts::SourceModel source(fixture());Cuts::RootLoopOptions options; + options.max_rounds=0;auto result=Cuts::root_cover_loop(source,options);evidence(source,options,result); + assert(result.completion==Cuts::RootLoopCompletion::RoundLimit && !result.backend && !result.best_bound); + options={};options.max_rounds=1;result=Cuts::root_cover_loop(source,options);evidence(source,options,result); + assert(result.completion==Cuts::RootLoopCompletion::RoundLimit && result.cuts.size()==1); + assert(result.best_bound->bound.lower_bound==-3 && result.best_bound->cuts.empty()); + assert(result.model.model().linear.b.size()==2 && result.best_bound->model.model().linear.b.size()==1); + for(std::size_t work=0;work<650;++work) { + options={};options.max_work=work; + const auto limited=Cuts::root_cover_loop(source,options);evidence(source,options,limited); + assert(limited.completion==Cuts::RootLoopCompletion::WorkLimit || + limited.completion==Cuts::RootLoopCompletion::NoNewCuts); + if(limited.completion==Cuts::RootLoopCompletion::WorkLimit && limited.best_bound) + assert(limited.best_bound->bound.lower_bound<=-2); + } + for(unsigned kind=0;kind<6;++kind) { + options={}; + if(kind==0)options.max_columns=1; + if(kind==1)options.max_rows=1; + if(kind==2)options.max_nonzeros=3; + if(kind==3)options.pool.max_cuts=0; + if(kind==4)options.pool.max_nonzeros=1; + if(kind==5)options.max_rows=0; + result=Cuts::root_cover_loop(source,options);evidence(source,options,result); + assert(result.completion==Cuts::RootLoopCompletion::StorageLimit && result.cuts.empty()); + } + options={};options.separation.max_rows=0;result=Cuts::root_cover_loop(source,options);evidence(source,options,result); + assert(result.completion==Cuts::RootLoopCompletion::SeparationLimit); + options={};options.separation.max_work=0;result=Cuts::root_cover_loop(source,options);evidence(source,options,result); + assert(result.completion==Cuts::RootLoopCompletion::WorkLimit); + options={};options.separation.max_cuts=0;result=Cuts::root_cover_loop(source,options);evidence(source,options,result); + assert(result.completion==Cuts::RootLoopCompletion::SeparationLimit); + options={};options.deadline=std::chrono::steady_clock::now();result=Cuts::root_cover_loop(source,options);evidence(source,options,result); + assert(result.completion==Cuts::RootLoopCompletion::TimeLimit && !result.backend); + options={};options.stop_requested=[](){return true;};result=Cuts::root_cover_loop(source,options);evidence(source,options,result); + assert(result.completion==Cuts::RootLoopCompletion::Cancelled && !result.backend); + std::size_t checkpoints=0; + options={};options.stop_requested=[&](){++checkpoints;return false;}; + result=Cuts::root_cover_loop(source,options);evidence(source,options,result); + const auto total=checkpoints; + for(std::size_t stop=1;stop<=total;++stop) for(unsigned mode=0;mode<4;++mode) { + checkpoints=0;options={}; + options.stop_requested=[&]() { + if(++checkpoints!=stop)return false; + if(mode==1)throw std::runtime_error("callback failure"); + if(mode==2)throw std::bad_alloc(); + if(mode==3){options.deadline=std::chrono::steady_clock::now();return false;} + return true; + }; + const auto partial=Cuts::root_cover_loop(source,options);evidence(source,options,partial); + assert(checkpoints==stop); + assert(partial.completion==(mode==3?Cuts::RootLoopCompletion::TimeLimit: + mode?Cuts::RootLoopCompletion::CallbackError:Cuts::RootLoopCompletion::Cancelled)); + } +} +static void edge_models() { + for(const auto& value:{ + model({{1},{0},{1}},{0},{1}), // Integral LP; no violated cover. + model({{-1,-1},{-3},{-1,-1}},{0,0},{3,3}), // Unsupported free general terms. + model({{},{},{0}},{-1},{1}), // No matrix, no raw LP suggestion. + model({{},{1},{}},{},{}), // Exact contradiction, no floating inference. + model({{1,-1},{-1},{0,0}},{-2,0},{-2,1}), // Fixed negative integer substitution. + model({{1},{2},{0}},{0},{1}) // Floating infeasible status must not prune. + }) { + Cuts::SourceModel source(value);Cuts::RootLoopOptions options; + const auto result=Cuts::root_cover_loop(source,options);evidence(source,options,result); + if(value.upper==std::vector({3,3})) assert(result.stats.unsupported_rows>0 && result.cuts.empty()); + if(value.linear.a.empty()) assert(result.completion==Cuts::RootLoopCompletion::NoPrimalSuggestion); + } + std::mt19937 random(419); + for(unsigned example=0;example<180;++example) { + const std::size_t n=2+random()%2,m=1+random()%3;LP::LinearModel dense; + for(std::size_t k=0;k lo(n,0),hi(n,1); + if(example%5==0)lo[0]=hi[0]=-2; + Cuts::SourceModel source(model(dense,lo,hi)); + std::optional prior; + for(std::size_t rounds=1;rounds<=3;++rounds) { + Cuts::RootLoopOptions options;options.max_rounds=rounds; + const auto result=Cuts::root_cover_loop(source,options);evidence(source,options,result); + if(result.best_bound) { + if(prior)assert(result.best_bound->bound.lower_bound>=*prior); + prior=result.best_bound->bound.lower_bound; + } + } + } +} +static void concurrency() { + Cuts::SourceModel source(fixture());Cuts::RootLoopOptions options; + std::optional a,b; + std::thread one([&](){a=Cuts::root_cover_loop(source,options);}); + std::thread two([&](){b=Cuts::root_cover_loop(source,options);}); + one.join();two.join();evidence(source,options,*a);evidence(source,options,*b); + assert(a->best_bound->bound.lower_bound==-2 && b->best_bound->bound.lower_bound==-2); + assert(!a->model.same_identity(b->model) && a->cuts[0].source().same_identity(b->cuts[0].source())); + auto backend=a->backend;bool first=true,second=true; + std::thread low([&](){for(unsigned i=0;i<10;++i){const auto r=backend->bound({0,0},{0,1},true,true);first&=r.valid && r.lower_bound==-2;}}); + std::thread high([&](){for(unsigned i=0;i<10;++i){const auto r=backend->bound({1,0},{1,0},true,true);second&=r.valid && r.lower_bound==-2;}}); + low.join();high.join();assert(first && second); +} +#ifdef GECODE_LP_CUT_LOOP_TEST_HOOKS +static void faults() { + Cuts::SourceModel source(fixture());Cuts::RootLoopOptions options; + events.clear();(void)Cuts::root_cover_loop(source,options);const auto counts=events; + for(const auto& event:counts)for(unsigned occurrence=1;occurrence<=event.second;++occurrence) + for(unsigned mode=0;mode<3;++mode) { + failed_event=event.first;failed_occurrence=occurrence;event_occurrence=0;allocation_failure=mode==1; + cancel_on_event=mode==2;cancelled_on_event=false; + options.stop_requested=[](){return cancelled_on_event;}; + auto result=Cuts::root_cover_loop(source,options);evidence(source,options,result); + assert(event_occurrence==occurrence); + assert(result.completion==(mode==2?Cuts::RootLoopCompletion::Cancelled: + mode==1?Cuts::RootLoopCompletion::AllocationFailure:Cuts::RootLoopCompletion::BackendError)); + if(event.first=="augmentation_publication")assert(result.cuts.empty() && result.best_bound->bound.lower_bound==-3); + if(event.first=="after_augmentation_publication")assert(result.cuts.size()==1 && result.best_bound->bound.lower_bound==-3); + if(event.first=="bound_publication" && occurrence==2)assert(result.cuts.size()==1 && result.best_bound->bound.lower_bound==-3); + } + // Explicit last-round interruption: retain its already-published augmentation. + failed_event="after_augmentation_publication";failed_occurrence=1;event_occurrence=0; + cancel_on_event=true;cancelled_on_event=false;options.max_rounds=1; + const auto last=Cuts::root_cover_loop(source,options);evidence(source,options,last); + assert(last.completion==Cuts::RootLoopCompletion::Cancelled && last.cuts.size()==1); + failed_event.clear();cancel_on_event=false;cancelled_on_event=false; +} +#endif +#ifdef GECODE_LP_CUT_LOOP_TEST_NATIVE +class NativeModel final:public Gecode::Space { +public: + Gecode::IntVarArray x; + Gecode::IntVar cost; + explicit NativeModel(const Model& input):x(*this,static_cast(input.lower.size())), + cost(*this,Gecode::Int::Limits::min,Gecode::Int::Limits::max) { + for(int j=0;j(input.lower[j]),static_cast(input.upper[j])); + LP::post_native_integer(*this,x,cost,input); + Gecode::branch(*this,x,Gecode::INT_VAR_SIZE_MIN(),Gecode::INT_VAL_MIN()); + } + NativeModel(NativeModel& other):Gecode::Space(other){x.update(*this,other.x);cost.update(*this,other.cost);} + Gecode::Space* copy() override{return new NativeModel(*this);} +}; +static void native_consumption() { + for(const auto& input:{fixture(),model({{2,-3,-3},{-7},{0,-2,-2}},{-1,0,0},{-1,1,1}), + model({{},{1},{}},{},{})}) { + Cuts::SourceModel source(input);Cuts::RootLoopOptions options; + auto result=Cuts::root_cover_loop(source,options);evidence(source,options,result); + auto root=std::make_unique(result.model.model()); + Gecode::DFS search(root.get());root.reset(); + std::size_t count=0; + while(auto* raw=search.next()) { + std::unique_ptr solution(raw);std::vector x; + for(int j=0;jx.size();++j)x.push_back(solution->x[j].val()); + assert(feasible(input,x) && solution->cost.val()==objective(input,x));++count; + } + std::size_t expected=0;std::vector x(input.lower.size()); + const auto visit=[&](const auto& self,std::size_t j)->void { + if(j==x.size()){expected+=feasible(input,x);return;} + for(I value=input.lower[j];value<=input.upper[j];++value){x[j]=value;self(self,j+1);} + }; + visit(visit,0);assert(count==expected); + } +} +#endif +int main() { + actual_lp_and_ownership();rationalization();budgets();edge_models(); +#ifdef GECODE_LP_CUT_LOOP_TEST_HOOKS + faults(); // Hook instrumentation is coordinator-only; concurrent runs follow. +#endif + concurrency(); +#ifdef GECODE_LP_CUT_LOOP_TEST_NATIVE + native_consumption(); +#endif + std::cout<<"PASS root cover loop: "< +#include +#include +#include +#include +#include + +namespace LP=Gecode::Experimental::LpRelaxation; +namespace C=LP::Cuts; +using I=std::int64_t; +using Point=std::vector; +static void check(bool okay,const char* message) {if(!okay)throw std::runtime_error(message);} +template static void rejected(Function function,const char* message) { + bool failed=false; + try {function();}catch(const std::invalid_argument&){failed=true;}catch(const std::overflow_error&){failed=true;} + check(failed,message); +} +static LP::BoundedIntegerModel model(LP::LinearModel dense,Point lower,Point upper) { + return {LP::sparse_model(dense),std::move(lower),std::move(upper)}; +} +static C::Box box(const LP::BoundedIntegerModel& source) {return {source.lower,source.upper};} +static bool in_box(const Point& point,const C::Box& bounds) { + for(std::size_t j=0;jbounds.upper[j])return false; + return true; +} +static I activity(const C::SparseCut& cut,const Point& point) { + I value=0;for(std::size_t k=0;k enumerate(const LP::BoundedIntegerModel& source) { + const auto& m=source.linear;Point point(m.c.size());std::vector found; + const auto visit=[&](auto&& self,std::size_t column)->void { + if(column& points) { + std::size_t excluded_siblings=0; + const auto& inequality=cut.inequality(); + check(inequality.column.size()==inequality.coefficient.size(),"cut term dimensions"); + for(std::size_t i=0;iinequality.column[i-1],"cut is not canonical"); + check(inequality.coefficient[i]==-1 || inequality.coefficient[i]==1,"cover coefficients are not literal signs"); + } + for(const auto& point:points) { + const bool inside=cut.proof().scope.kind==C::ScopeKind::Global || in_box(point,cut.proof().scope.box); + check(cut.applies_to(source,{point,point})==inside,"cut scope differs from independent box containment"); + if(inside)check(activity(inequality,point)<=inequality.upper,"cover removed a feasible original integer assignment"); + else if(activity(inequality,point)>inequality.upper)++excluded_siblings; + } + const auto rechecked=C::verify_cover(source,cut.proof()); + check(rechecked.inequality().column==inequality.column && rechecked.inequality().coefficient==inequality.coefficient && + rechecked.inequality().upper==inequality.upper && rechecked.capacity()==cut.capacity() && + rechecked.cover_weight()==cut.cover_weight(),"stored cover proof does not reconstruct its cut"); + return excluded_siblings; +} +static std::string fingerprint(const C::SeparationResult& result) { + std::ostringstream text; + for(const auto& cut:result.cuts) { + text<(cut.proof().scope.kind)<<':'<::value,"unverified default cut must not exist"); + auto input=model({{-3,-3,0},{-5},{0,0,0}},{0,0,0},{1,1,1}); + C::SourceModel source(input),identical(input);auto shared=source; + auto cut=C::verify_cover(source,{0,{1,0},C::Scope::global()}); + check(cut.capacity()==5 && cut.cover_weight()==6 && cut.inequality().upper==1 && + cut.inequality().column==std::vector({0,1}),"strict cover or canonical ordering"); + check(cut.applies_to(shared,box(input)) && !cut.applies_to(identical,box(input)),"source identity was structural or lost on copy"); + input.linear.a[0]=-1;input.upper[0]=3; + check(source.model().linear.a[0]==-3 && source.model().upper[0]==1,"source snapshot changed after input mutation"); + auto moved=std::move(source); + check(cut.applies_to(moved,box(moved.model())) && !cut.applies_to(source,box(moved.model())),"source move identity"); + rejected([&]{(void)source.model();},"moved source accepted"); + assert_valid(cut,moved,enumerate(moved.model())); + auto row_copy=cut.inequality();row_copy.upper=100; + check(cut.inequality().upper==1,"mutable inequality copy modified the verified record"); + const auto retained=[] { + C::SourceModel temporary(model({{-3,-3},{-5},{0,0}},{0,0},{1,1})); + return C::verify_cover(temporary,{0,{0,1},{}}); + }(); + assert_valid(retained,retained.source(),enumerate(retained.source().model())); + for(const auto& claim:std::vector{ + {0,{0},{}},{0,{0,0},{}},{0,{0,2},{}},{0,{99},{}},{99,{},{}}, + {0,{0,1},{C::ScopeKind::Global,{{0,0,0},{1,1,0}}}}, + {0,{0,1},C::Scope::local({{0,0},{1,1}})}, + {0,{0,1},C::Scope::local({{-1,0,0},{1,1,1}})}, + {0,{0,1},C::Scope::local({{1,0,0},{0,1,1}})}, + {0,{0,1},{static_cast(-1),{}}}}) + rejected([&]{(void)C::verify_cover(moved,claim);},"forged or insufficient cover proof accepted"); + check(!cut.applies_to(moved,{{0},{1}}) && !cut.applies_to(moved,{{0,0,0},{2,1,1}}),"invalid application box accepted"); + + C::SourceModel signed_row(model({{3,-3,3},{1},{0,0,0}},{0,0,0},{1,1,1})); + auto signed_cut=C::verify_cover(signed_row,{0,{0,1},{}}); + check(signed_cut.inequality().coefficient==Point({-1,1}) && signed_cut.inequality().upper==0, + "signed-literal translation lost a complement constant"); + assert_valid(signed_cut,signed_row,enumerate(signed_row.model())); + for(I coefficient:{I(2),I(-2)}) { + const I fixed=coefficient>0?2:-2; + C::SourceModel fixed_row(model({{-3,-3,coefficient},{-1},{0,0,0}},{0,0,fixed},{1,1,fixed})); + const auto fixed_cut=C::verify_cover(fixed_row,{0,{0,1},{}}); + check(fixed_cut.capacity()==5 && fixed_cut.inequality().upper==1,"fixed signed integer contribution"); + assert_valid(fixed_cut,fixed_row,enumerate(fixed_row.model())); + rejected([&]{(void)C::verify_cover(fixed_row,{0,{0,2},{}});},"fixed variable accepted as free cover literal"); + } + C::SourceModel fixed_zero(model({{3,-3},{-1},{0,0}},{0,0},{0,1})); + auto zero_cut=C::verify_cover(fixed_zero,{0,{1},{}}); + check(zero_cut.capacity()==1 && zero_cut.inequality().upper==0,"fixed positive coefficient included twice"); + assert_valid(zero_cut,fixed_zero,enumerate(fixed_zero.model())); + C::SourceModel contradiction(model({{},{1},{}},{},{})); + auto impossible=C::verify_cover(contradiction,{0,{},{}}); + check(impossible.inequality().column.empty() && impossible.inequality().upper==-1,"empty cover contradiction"); + for(I rhs:{I(0),I(-1)}) { + C::SourceModel constant(model({{},{rhs},{}},{},{})); + rejected([&]{(void)C::verify_cover(constant,{0,{},{}});},"nonnegative capacity accepted an empty cover"); + } + auto corrupt=model({{-3,-3},{-5},{0,0}},{0,0},{1,1});corrupt.linear.column[1]=0; + rejected([&]{C::SourceModel invalid(corrupt);},"malformed sparse source accepted"); + corrupt=model({{-3},{-1},{0}},{0},{1});corrupt.linear.a[0]=std::numeric_limits::min(); + rejected([&]{C::SourceModel invalid(corrupt);},"unsupported source arithmetic accepted"); +} + +static void scope_and_pool() { + // In z=1, x+y<=1 is valid. In z=0, x=y=1 is feasible and violates it. + C::SourceModel source(model({{-2,-2,-2,0},{-4},{0,0,0,0}},{0,0,0,0},{1,1,1,1})); + auto parent=box(source.model());parent.lower[2]=1; + auto left=parent,right=parent;left.upper[3]=0;right.lower[3]=1; + const auto a=C::verify_cover(source,{0,{0,1},C::Scope::local(left)}); + const auto b=C::verify_cover(source,{0,{1,0},C::Scope::local(right)}); + const auto broad=C::verify_cover(source,{0,{0,1},C::Scope::local(parent)}); + check(assert_valid(a,source,enumerate(source.model()))>0,"local fixture cannot expose a sibling leak"); + auto sibling=box(source.model());sibling.upper[2]=0; + check(!a.applies_to(source,right) && !a.applies_to(source,sibling) && broad.applies_to(source,left),"local cut leaked a sibling"); + rejected([&]{(void)C::verify_cover(source,{0,{0,1},{}});},"local proof silently became global"); + C::CutPool pool(source); + check(pool.insert(a)==C::InsertStatus::Inserted && pool.insert(b)==C::InsertStatus::Inserted && + pool.records().size()==2,"incomparable scopes were merged"); + check(pool.applicable(parent).empty() && pool.applicable(left).size()==1 && pool.applicable(sibling).empty(), + "pool applied a union of unproved scopes"); + auto historical=pool.applicable(left); + check(pool.insert(broad)==C::InsertStatus::Replaced && pool.records().size()==1 && + pool.scope_values()==8 && pool.nonzeros()==2,"independently proved broader local scope did not replace narrower cuts"); + check(pool.insert(a)==C::InsertStatus::Duplicate && !historical[0].applies_to(source,right), + "duplicate insertion widened a historical record"); + C::SourceModel foreign(source.model()); + auto foreign_cut=C::verify_cover(foreign,{0,{0,1},C::Scope::local(parent)}); + check(pool.insert(foreign_cut)==C::InsertStatus::ForeignSource && pool.records().size()==1,"pool accepted a foreign model"); + for(const auto limits:std::vector{{0,100,100},{2,1,100},{2,100,7}}) { + C::CutPool limited(source,limits); + check(limited.insert(broad)==C::InsertStatus::Capacity && limited.records().empty() && + limited.nonzeros()==0 && limited.scope_values()==0,"capacity rejection partially mutated pool"); + } + C::SourceModel global_source(model({{-3,-3,0},{-5},{0,0,0}},{0,0,0},{1,1,1})); + auto local=box(global_source.model());local.upper[2]=0; + auto global=C::verify_cover(global_source,{0,{0,1},{}}); + C::CutPool replacement(global_source,{1,2,6}); + check(replacement.insert(C::verify_cover(global_source,{0,{0,1},C::Scope::local(local)}))==C::InsertStatus::Inserted, + "local initial record insertion"); + check(replacement.insert(global)==C::InsertStatus::Replaced && replacement.scope_values()==0, + "global proof replacement failed at pool capacity"); + check(replacement.insert(global)==C::InsertStatus::Duplicate,"global canonical duplicate"); +} + +static void separation_edges() { + C::SourceModel source(model({{-3,-3,0,-3,0,-3},{-5,-5},{0,0,0}},{0,0,0},{1,1,1})); + const C::FractionalPoint point{{3,3,3},4}; + // Each original packing row has activity 4.5<=5, but its cover has 1.5>1. + const auto complete=C::separate_covers(source,point); + check(complete.cuts.size()==2 && !complete.stats.work_limit,"fractional cover point was not separated"); + for(const auto& cut:complete.cuts) { + check(activity(cut.inequality(),point.numerator)>cut.inequality().upper*point.denominator, + "separator returned an unviolated cut"); + assert_valid(cut,source,enumerate(source.model())); + } + // Verification accepts a valid cut independently of whether this point violates it. + const auto valid=C::verify_cover(source,{0,{0,1},{}}); + check(C::separate_covers(source,{{0,0,0},1}).cuts.empty() && valid.inequality().upper==1, + "validity was confused with candidate-point violation"); + for(std::size_t budget=0;budget<300;++budget) { + C::SeparationOptions options;options.max_work=budget; + const auto a=C::separate_covers(source,point,{},options),b=C::separate_covers(source,point,{},options); + check(a.stats.work<=budget && fingerprint(a)==fingerprint(b),"separation work budget or deterministic prefix"); + for(const auto& cut:a.cuts)assert_valid(cut,source,enumerate(source.model())); + } + C::SeparationOptions options;options.max_rows=0; + check(C::separate_covers(source,point,{},options).stats.row_limit,"row limit ignored"); + options={};options.max_cuts=0; + check(C::separate_covers(source,point,{},options).stats.cut_limit,"zero cut limit ignored"); + options={};options.max_cuts=1; + const auto one=C::separate_covers(source,point,{},options); + check(one.cuts.size()==1 && one.stats.cut_limit,"output cut limit ignored"); + options={};options.max_terms_per_row=1; + const auto skipped=C::separate_covers(source,point,{},options); + check(skipped.cuts.empty() && skipped.stats.oversized_rows==2,"row-term storage cap ignored"); + options={};options.max_cuts_per_row=0; + check(C::separate_covers(source,point,{},options).cuts.empty(),"per-row zero cut limit ignored"); + options={};options.max_starts_per_row=0; + check(C::separate_covers(source,point,{},options).cuts.empty(),"zero greedy starts ignored"); + C::SourceModel general(model({{-3,-3,2},{-1},{0,0,0}},{0,0,-2},{1,1,2})); + rejected([&]{(void)C::verify_cover(general,{0,{0,1},{}});},"free general integer row treated as binary"); + const auto unsupported=C::separate_covers(general,{{1,1,0},2}); + check(unsupported.cuts.empty() && unsupported.stats.unsupported_rows==1,"unsupported row not reported"); + auto local=box(general.model());local.lower[2]=local.upper[2]=2; + const auto localized=C::separate_covers(general,{{3,3,8},4},C::Scope::local(local)); + check(!localized.cuts.empty(),"fixed general integer substitution did not permit binary separation"); + for(const auto& cut:localized.cuts)assert_valid(cut,general,enumerate(general.model())); + for(const auto& bad:std::vector{{{0,0,0},0},{{0,0},1},{{-1,0,0},1},{{2,0,0},1}}) + rejected([&]{(void)C::separate_covers(source,bad);},"invalid selection point accepted"); + const I maximum=std::numeric_limits::max(); + const auto overflow=C::separate_covers(source,{{maximum,maximum,maximum},maximum}); + check(overflow.cuts.empty() && overflow.stats.arithmetic_rejections>0,"overflowing rational separation was not rejected"); + C::SourceModel impossible(model({{},{1},{}},{},{})); + const auto empty=C::separate_covers(impossible,{{},1}); + check(empty.cuts.size()==1 && empty.cuts[0].inequality().upper==-1,"constant contradiction not separated"); + + // The source and query remain sparse; even coordinate validation obeys work. + const std::size_t n=16384; + LP::BoundedIntegerModel large; + large.linear.row_start={0,2};large.linear.column={0,n-1};large.linear.a={-3,-3};large.linear.b={-5}; + large.linear.c.resize(n);large.lower.resize(n);large.upper.assign(n,1); + C::SourceModel big(large);options={};options.max_work=100; + const auto bounded=C::separate_covers(big,{Point(n,1),2},{},options); + check(bounded.stats.work==100 && bounded.stats.work_limit && bounded.stats.rows==0 && bounded.cuts.empty(), + "large source coordinate scan ignored separator budget"); + auto global=C::verify_cover(big,{0,{0,n-1},{}}); + C::CutPool sparse_pool(big); + check(sparse_pool.insert(global)==C::InsertStatus::Inserted && sparse_pool.nonzeros()==2, + "sparse cut pool retained dense coefficients"); +} + +static void exhaustive_oracles() { + std::mt19937 random(975522); + std::size_t verified=0,rejected_proofs=0,separated=0,sibling_witnesses=0; + for(unsigned trial=0;trial<750;++trial) { + const auto n=1+random()%4,m=1+random()%3; + LP::LinearModel dense;dense.c.resize(n);dense.a.resize(n*m);dense.b.resize(m); + Point lo(n),hi(n); + for(std::size_t j=0;j(random()%5)-2;} + else {lo[j]=-2;hi[j]=2;} + } + for(auto& a:dense.a)a=random()%3?static_cast(random()%11)-5:0; + for(auto& b:dense.b)b=static_cast(random()%15)-7; + C::SourceModel source(model(dense,lo,hi));const auto feasible=enumerate(source.model()); + auto local=box(source.model()); + for(std::size_t j=0;j{{},C::Scope::local(local)}) { + for(std::size_t row=0;row2*cut.inequality().upper,"random cut was not exactly violated"); + } + } + } + check(verified>100 && rejected_proofs>100 && separated>100 && sibling_witnesses>0,"cover oracle coverage missing"); + std::cout<<"PASS 750 scoped integer models; "< +#include +#include + +#include +#include +#include +#include +#include + +namespace O = Gecode::Optimize; +constexpr double inf = std::numeric_limits::infinity(); + +#ifdef GECODE_DIAGNOSTICS_TEST_FAKE_SOLVER +namespace { +int calls = 0, interrupt_at = 0, corrupt_at = 0, contradict_at = 0, cancel_at = 0; +O::Termination interrupted_reason = O::Termination::TimeLimit; +std::vector limits; +void reset() { + calls = interrupt_at = corrupt_at = contradict_at = cancel_at = 0; + interrupted_reason = O::Termination::TimeLimit; + limits.clear(); +} +} +namespace Gecode { namespace Optimize { +SolveResult solve(const ModelSnapshot& model, const SolveOptions& options) { + ++calls; + assert(model.objective.terms.empty() && model.objective.offset == 0); + assert(options.primal_start.empty() && options.relative_gap == 0 && options.absolute_gap == 0); + limits.push_back(options.time_limit_seconds); + if (limits.size() > 1) assert(limits.back() <= limits[limits.size()-2]); + SolveResult result; + result.model_id = model.model_id; result.revision = model.revision; + result.backend = "deterministic oracle"; result.backend_version = "test"; + for (const auto& variable : model.variables) result.active_variables.push_back(variable.active); + result.termination = Termination::Infeasible; + assert(model.variables.size() == 1); + for (double value : {-10.0, -3.0, -2.0, -1.0, 0.0, 0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 10.0}) { + std::vector witness{value}; + if (!validate(model, witness).valid) continue; + result.values = std::move(witness); + result.solution_validated = true; + result.objective = 0; + result.termination = Termination::Optimal; + break; + } + if (calls == corrupt_at || calls == contradict_at) { + result.solution_validated = true; result.values = {3}; result.objective = 0; + result.termination = calls == contradict_at ? Termination::Infeasible : Termination::Optimal; + } + if (calls == interrupt_at) result.termination = interrupted_reason; + if (calls == cancel_at) options.cancellation->cancel(); + return result; +} +}} +#endif + +namespace { +// Independent reconstruction from the attribution contract: begin with a free +// continuous system and restore only reported groups, rather than using the +// implementation's deletion operation. +O::ModelSnapshot rebuild(const O::ModelSnapshot& original, + const std::vector& groups, + std::size_t omit = std::numeric_limits::max()) { + auto model = original; + model.objective = {}; + for (auto& variable : model.variables) if (variable.active) { + variable.type = O::VariableType::Continuous; + variable.lower = -inf; variable.upper = inf; + } + for (auto& row : model.rows) if (row.active) { row.active = false; row.terms.clear(); } + for (auto& indicator : model.indicators) indicator.active = false; + for (std::size_t i = 0; i < groups.size(); ++i) if (i != omit) { + const auto& group = groups[i]; + switch (group.kind) { + case O::ConflictGroupKind::Row: model.rows[group.row->id] = original.rows[group.row->id]; break; + case O::ConflictGroupKind::LowerBound: + model.variables[group.variable->id].lower = original.variables[group.variable->id].lower; break; + case O::ConflictGroupKind::UpperBound: + model.variables[group.variable->id].upper = original.variables[group.variable->id].upper; break; + case O::ConflictGroupKind::Integrality: + model.variables[group.variable->id].type = O::VariableType::Integer; break; + case O::ConflictGroupKind::VariableDomain: + model.variables[group.variable->id] = original.variables[group.variable->id]; break; + case O::ConflictGroupKind::IndicatorComponent: + for (auto handle : group.grouped_variables) model.variables[handle.id] = original.variables[handle.id]; + for (auto handle : group.generated_rows) model.rows[handle.id] = original.rows[handle.id]; + for (auto handle : group.indicators) model.indicators[handle.id] = original.indicators[handle.id]; + break; + } + } + return model; +} + +O::ConflictOptions evidence_options() { + O::ConflictOptions options; + options.retain_deletion_witnesses = true; + return options; +} + +void verify(const O::ModelSnapshot& original, const O::ConflictResult& result) { + assert(result.irreducible() && result.infeasibility_established); + assert(result.termination == O::Termination::Infeasible && result.guarantee == O::Guarantee::Numerical); + assert(result.model_id == original.model_id && result.revision == original.revision); + const auto conflict = rebuild(original, result.groups); + O::validate_structure(conflict); +#ifndef GECODE_DIAGNOSTICS_TEST_FAKE_SOLVER + assert(O::solve(conflict).termination == O::Termination::Infeasible); +#endif + for (std::size_t i = 0; i < result.groups.size(); ++i) { + assert(result.groups[i].necessity_verified); + auto deleted = rebuild(original, result.groups, i); + O::validate_structure(deleted); + assert(O::validate(deleted, result.groups[i].deletion_witness).valid); + } +} + +void preflight() { + O::Model model; + auto x = model.add_integer(0, 4); + model.add_row({{x, 1}}, 5, inf); + auto options = evidence_options(); + options.solve.time_limit_seconds = 0; + auto result = O::analyze_conflict(model, options); + assert(result.status == O::ConflictStatus::Unknown && result.termination == O::Termination::TimeLimit); + assert(!result.infeasibility_established && result.oracle_calls == 0 && result.groups.empty()); + options.solve.time_limit_seconds = inf; + options.solve.cancellation = std::make_shared(); + options.solve.cancellation->cancel(); + result = O::analyze_conflict(model, options); + assert(result.termination == O::Termination::Cancelled && result.oracle_calls == 0); + options = {}; options.solve.node_limit = 2; + assert(O::analyze_conflict(model, options).status == O::ConflictStatus::Unsupported); + options = {}; options.solve.guarantee = O::Guarantee::Exact; + assert(O::analyze_conflict(model, options).status == O::ConflictStatus::Unsupported); + options.solve.guarantee = O::Guarantee::Certified; + assert(O::analyze_conflict(model, options).status == O::ConflictStatus::Unsupported); + options = {}; options.solve.feasibility_tolerance = -1; + assert(O::analyze_conflict(model, options).status == O::ConflictStatus::InvalidModel); + auto malformed = model.snapshot(); malformed.variables[x.id].variable.model_id = 0; + assert(O::analyze_conflict(malformed).status == O::ConflictStatus::InvalidModel); +} + +#ifndef GECODE_DIAGNOSTICS_TEST_FAKE_SOLVER +void actual_oracles() { + const auto options = evidence_options(); + O::Model rows; + auto x = rows.add_continuous(-inf, inf, "quantity"); + auto lower = rows.add_row({{x, 1}}, 2, inf, "demand"); + auto upper = rows.add_row({{x, 1}}, -inf, 1, "capacity"); + rows.add_row({{x, 1}}, -inf, 10, "redundant"); + rows.maximize({{x, 7}}, -8); + const auto original = rows.snapshot(); + auto ignored_start = options; ignored_start.solve.primal_start = {{x, 0}}; + auto result = O::analyze_conflict(rows, ignored_start); + verify(original, result); + assert(result.groups.size() == 2 && result.groups[0].row->id == lower.id && result.groups[1].row->id == upper.id); + assert(rows.revision() == original.revision && rows.row(lower).active && rows.row(upper).active); + assert(rows.snapshot().objective.offset == -8 && rows.snapshot().objective.sense == O::ObjectiveSense::Maximize); + auto without_witnesses = O::analyze_conflict(rows); + assert(without_witnesses.irreducible()); + for (const auto& group : without_witnesses.groups) assert(group.deletion_witness.empty()); + + O::Model bound; + auto y = bound.add_continuous(0, 10, "stock"); + bound.add_row({{y, 1}}, 11, inf, "required"); + result = O::analyze_conflict(bound, options); verify(bound.snapshot(), result); + assert(result.groups.size() == 2 && result.groups[0].kind == O::ConflictGroupKind::Row && + result.groups[1].kind == O::ConflictGroupKind::UpperBound && *result.groups[1].variable == y); + + O::Model integer; + auto n = integer.add_integer(0.25, 0.75, "count"); + result = O::analyze_conflict(integer, options); verify(integer.snapshot(), result); + assert(result.groups.size() == 3); + assert(result.groups[0].kind == O::ConflictGroupKind::LowerBound && + result.groups[1].kind == O::ConflictGroupKind::UpperBound && + result.groups[2].kind == O::ConflictGroupKind::Integrality); + assert(integer.variable(n).type == O::VariableType::Integer && integer.variable(n).lower == 0.25); + + O::Model binary; + auto bit = binary.add_binary(); binary.set_bounds(bit, 0.25, 0.75); + result = O::analyze_conflict(binary, options); verify(binary.snapshot(), result); + assert(result.groups.size() == 1 && result.groups[0].kind == O::ConflictGroupKind::VariableDomain); + + for (auto type : {O::VariableType::SemiContinuous, O::VariableType::SemiInteger}) { + O::Model semi; + auto s = semi.add_variable(type, 3, 6); + semi.add_row({{s, 1}}, type == O::VariableType::SemiContinuous ? 1 : 3.25, + type == O::VariableType::SemiContinuous ? 2 : 3.75); + result = O::analyze_conflict(semi, options); verify(semi.snapshot(), result); + assert(result.groups.size() == 2 && result.groups[1].kind == O::ConflictGroupKind::VariableDomain); + } + + O::Model constant; + constant.add_row({}, 1, inf, "constant contradiction"); + result = O::analyze_conflict(constant, options); verify(constant.snapshot(), result); + assert(result.groups.size() == 1 && result.groups[0].kind == O::ConflictGroupKind::Row); + O::Model empty; + auto dead = empty.add_binary(); empty.remove(dead); + result = O::analyze_conflict(empty, options); + assert(result.status == O::ConflictStatus::Feasible && !result.infeasibility_established); + assert(result.feasible_witness.size() == 1 && std::isnan(result.feasible_witness[dead.id])); + + O::Model unbounded; + auto free = unbounded.add_continuous(-inf, inf); + unbounded.minimize({{free, -1}}); + result = O::analyze_conflict(unbounded, options); + assert(result.status == O::ConflictStatus::Feasible && result.oracle_calls == 1); + assert(O::validate(unbounded.snapshot(), result.feasible_witness).valid); + + O::Model indicators; + auto b = indicators.add_binary("enabled"); auto c = indicators.add_binary("other"); + auto amount = indicators.add_continuous(0, 5); + indicators.set_bounds(b, 1, 1); indicators.set_bounds(c, 1, 1); + auto first = O::add_indicator(indicators, b, true, {{amount, 1}}, 4, inf); + auto second = O::add_indicator(indicators, c, true, {{amount, 1}}, -inf, 3); + indicators.add_row({}, -inf, 1, "irrelevant"); + const auto indicator_snapshot = indicators.snapshot(); + result = O::analyze_conflict(indicators, options); verify(indicator_snapshot, result); + assert(result.groups.size() == 1 && result.groups[0].kind == O::ConflictGroupKind::IndicatorComponent); + assert(result.groups[0].indicators.size() == 2 && result.groups[0].grouped_variables.size() == 5); + assert(indicators.revision() == indicator_snapshot.revision && indicators.row(first.rows.front()).active && + indicators.row(second.rows.front()).active); + // Removed indicator tombstones must not impose a hidden nonrelaxable domain. + O::remove_indicator(indicators, first.indicator); O::remove_indicator(indicators, second.indicator); + result = O::analyze_conflict(indicators, options); + assert(result.status == O::ConflictStatus::Feasible); + + O::Model chained; + auto enable = chained.add_binary(); chained.set_bounds(enable, 0, 0); + auto first_amount = chained.add_continuous(0, 4); + auto second_amount = chained.add_continuous(0, 2); + auto gate_owner = O::add_indicator(chained, enable, true, {{first_amount, 1}}, 2, inf); + O::add_indicator(chained, *gate_owner.inactive_gate, true, {{second_amount, 1}}, 3, inf); + // An independent, harmless component must disappear from the conflict. + auto unrelated = chained.add_binary(); + O::add_indicator(chained, unrelated, true, {}, -inf, inf); + result = O::analyze_conflict(chained, options); verify(chained.snapshot(), result); + assert(result.groups.size() == 1 && result.groups[0].indicators.size() == 2); + assert(result.groups[0].grouped_variables.size() == 5); +} +#else +void coordinator() { + O::Model model; + auto x = model.add_continuous(-10, 10); + model.add_row({{x, 1}}, 2, inf); model.add_row({{x, 1}}, -inf, 1); + auto options = evidence_options(); options.solve.time_limit_seconds = 100; + options.solve.primal_start = {{x, 0}}; + reset(); + auto result = O::analyze_conflict(model, options); verify(model.snapshot(), result); + assert(result.oracle_calls == 5 && result.groups.size() == 2); + reset(); interrupt_at = 2; + result = O::analyze_conflict(model, options); + assert(result.status == O::ConflictStatus::Incomplete && result.infeasibility_established && !result.irreducible()); + assert(result.termination == O::Termination::TimeLimit && result.groups.size() == 4 && calls == 2); + reset(); interrupt_at = 5; + result = O::analyze_conflict(model, options); + assert(result.status == O::ConflictStatus::Incomplete && result.groups.size() == 3 && calls == 5); + assert(result.groups.back().kind == O::ConflictGroupKind::UpperBound); + assert(result.groups[0].necessity_verified && result.groups[1].necessity_verified); + reset(); interrupt_at = 1; interrupted_reason = O::Termination::InfeasibleOrUnbounded; + result = O::analyze_conflict(model, options); + assert(result.status == O::ConflictStatus::Unknown && !result.infeasibility_established && result.groups.empty()); + reset(); corrupt_at = 2; + result = O::analyze_conflict(model, options); + assert(result.status == O::ConflictStatus::Incomplete && result.termination == O::Termination::NumericalFailure); + assert(result.groups.size() == 4); + reset(); contradict_at = 1; + result = O::analyze_conflict(model, options); + assert(result.status == O::ConflictStatus::Error && result.termination == O::Termination::NumericalFailure); + assert(!result.infeasibility_established && result.groups.empty()); + reset(); cancel_at = 2; + options.solve.cancellation = std::make_shared(); + result = O::analyze_conflict(model, options); + assert(result.status == O::ConflictStatus::Incomplete && result.termination == O::Termination::Cancelled); + assert(result.groups.size() == 4 && calls == 2); +} +#endif +} + +int main() { + preflight(); +#ifdef GECODE_DIAGNOSTICS_TEST_FAKE_SOLVER + coordinator(); +#else + if (!O::capabilities().available) { + O::Model model; model.add_integer(0.25, 0.75); + auto result = O::analyze_conflict(model); + assert(result.status == O::ConflictStatus::Unsupported && !result.infeasibility_established); + std::cout << "Unavailable diagnostic backend contract passed\n"; + return 0; + } + actual_oracles(); +#endif + std::cout << "Numerical conflict groups, irreducibility witnesses and budgets passed\n"; +} diff --git a/test/optimize/flatzinc-fixtures/boolean-channel.fzn b/test/optimize/flatzinc-fixtures/boolean-channel.fzn new file mode 100644 index 0000000000..f5144af1cf --- /dev/null +++ b/test/optimize/flatzinc-fixtures/boolean-channel.fzn @@ -0,0 +1,5 @@ +var 0..1: x :: output_var; +var bool: b :: output_var; +constraint bool2int(b,x); +constraint bool_eq(b,true); +solve minimize x; diff --git a/test/optimize/flatzinc-fixtures/cli-v1-constant-objective.fzn b/test/optimize/flatzinc-fixtures/cli-v1-constant-objective.fzn new file mode 100644 index 0000000000..9233f09d28 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v1-constant-objective.fzn @@ -0,0 +1,3 @@ +% CLI fixture v1: the only assignment is w=5; every feasible point has objective -3. +var 5..5: w :: output_var; +solve maximize -3; diff --git a/test/optimize/flatzinc-fixtures/cli-v1-empty-output.fzn b/test/optimize/flatzinc-fixtures/cli-v1-empty-output.fzn new file mode 100644 index 0000000000..fb945b2c9e --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v1-empty-output.fzn @@ -0,0 +1,3 @@ +% CLI fixture v1: the sole array has no elements and there are no variable slots. +array [1..0] of var 0..1: a :: output_array([1..0]); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v1-global-distinct.fzn b/test/optimize/flatzinc-fixtures/cli-v1-global-distinct.fzn new file mode 100644 index 0000000000..65a9644837 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v1-global-distinct.fzn @@ -0,0 +1,9 @@ +% CLI fixture v1: distinct x,y,z in 1..3, y=2 and x (x<=1) means x>=2, so the minimum is x=2. +% Testing the false branch distinguishes equivalence from one-way implication. +var -2..3: x :: output_var; +var bool: b :: output_var; +constraint int_le_reif(x,1,b); +constraint bool_eq(b,false); +solve minimize x; diff --git a/test/optimize/flatzinc-fixtures/cli-v1-reified-signed-alias.fzn b/test/optimize/flatzinc-fixtures/cli-v1-reified-signed-alias.fzn new file mode 100644 index 0000000000..d6dfa5b649 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v1-reified-signed-alias.fzn @@ -0,0 +1,8 @@ +% CLI fixture v1: false <-> (-2*x+y-x<=-1) means y>=3*x. +% Enumerating x in -1..2 and y in -2..2 gives the unique minimum (x,y)=(-1,-2). +var -1..2: x :: output_var; +var -2..2: y :: output_var; +var bool: b :: output_var; +constraint int_lin_le_reif([-2,1,-1],[x,y,x],-1,b); +constraint bool_eq(b,false); +solve minimize y; diff --git a/test/optimize/flatzinc-fixtures/cli-v1-satisfy-hidden.fzn b/test/optimize/flatzinc-fixtures/cli-v1-satisfy-hidden.fzn new file mode 100644 index 0000000000..5ebffe92e1 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v1-satisfy-hidden.fzn @@ -0,0 +1,6 @@ +% CLI fixture v1: hidden=2 and hidden+x=3 imply x=1; hidden must not be printed. +var 0..2: hidden; +var 0..2: x :: output_var; +constraint int_plus(hidden,x,3); +constraint int_eq(hidden,2); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v1-unbounded-domain.fzn b/test/optimize/flatzinc-fixtures/cli-v1-unbounded-domain.fzn new file mode 100644 index 0000000000..1ae55f6c06 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v1-unbounded-domain.fzn @@ -0,0 +1,4 @@ +% CLI fixture v1: missing integer bounds are outside this finite-domain compiler. +% A frontend rejection must never be reported as a mathematical UNSAT proof. +var int: x :: output_var; +solve minimize x; diff --git a/test/optimize/flatzinc-fixtures/cli-v1-unknown-predicate.fzn b/test/optimize/flatzinc-fixtures/cli-v1-unknown-predicate.fzn new file mode 100644 index 0000000000..75abd5cd18 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v1-unknown-predicate.fzn @@ -0,0 +1,4 @@ +% CLI fixture v1: an intentionally unknown predicate must be rejected completely. +var 0..2: x :: output_var; +constraint cli_v1_unknown_predicate(x); +solve minimize x; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-circuit-empty.fzn b/test/optimize/flatzinc-fixtures/cli-v2-circuit-empty.fzn new file mode 100644 index 0000000000..3305c92cf1 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-circuit-empty.fzn @@ -0,0 +1,3 @@ +% This low-level predicate requires a nonempty successor array. +constraint gecode_circuit(0,[]); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-circuit-offset.fzn b/test/optimize/flatzinc-fixtures/cli-v2-circuit-offset.fzn new file mode 100644 index 0000000000..d0e1d119e0 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-circuit-offset.fzn @@ -0,0 +1,6 @@ +% Exactly one cycle on labels 3,4,5. Minimizing the first successor is unique. +var 3..5: x :: output_var; +var 3..5: y :: output_var; +var 3..5: z :: output_var; +constraint gecode_circuit(3,[x,y,z]); +solve minimize x; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-circuit-singleton.fzn b/test/optimize/flatzinc-fixtures/cli-v2-circuit-singleton.fzn new file mode 100644 index 0000000000..52d4194c02 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-circuit-singleton.fzn @@ -0,0 +1,3 @@ +% A singleton full circuit returns to its sole explicit label. +constraint gecode_circuit(5,[5]); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-circuit-subtours.fzn b/test/optimize/flatzinc-fixtures/cli-v2-circuit-subtours.fzn new file mode 100644 index 0000000000..2911855406 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-circuit-subtours.fzn @@ -0,0 +1,3 @@ +% Two disjoint two-cycles are not a full circuit. +constraint gecode_circuit(0,[1,0,3,2]); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-cumulative-fixed-alias.fzn b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-fixed-alias.fzn new file mode 100644 index 0000000000..50efb27d3d --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-fixed-alias.fzn @@ -0,0 +1,12 @@ +% Original alias-domain intersections fix duration=2 and capacity=2; h is assigned. +% Task [-1,1) uses one unit, so the two-unit task has earliest start 1. +var {0,2}: d :: output_var; +var int: duration_alias :: output_var = d; +var 0..3: h :: output_var = 1; +var {1,2}: capacity :: output_var; +var int: capacity_alias :: output_var = capacity; +var -1..3: s :: output_var; +constraint int_in(duration_alias,1..3); +constraint int_in(capacity_alias,{2}); +constraint cumulatives([-1,s],[duration_alias,1],[h,2],capacity_alias); +solve minimize s; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-cumulative-half-open.fzn b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-half-open.fzn new file mode 100644 index 0000000000..27d9b1fa20 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-half-open.fzn @@ -0,0 +1,5 @@ +% a occupies [-1,0); b may start exactly at 0, which is its unique optimum. +var -1..-1: a :: output_var; +var -1..2: b :: output_var; +constraint gecode_cumulatives([a,b],[1,2],[2,2],2); +solve minimize b; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-cumulative-malformed-four.fzn b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-malformed-four.fzn new file mode 100644 index 0000000000..480b1470ac --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-malformed-four.fzn @@ -0,0 +1,4 @@ +% Four arguments with mismatched task-array lengths are invalid input. +var 0..1: s; +constraint gecode_cumulatives([s],[1,1],[1],1); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-cumulative-overlap-unsat.fzn b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-overlap-unsat.fzn new file mode 100644 index 0000000000..317341f27f --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-overlap-unsat.fzn @@ -0,0 +1,4 @@ +% The legacy four-argument spelling denotes the same single resource. +% Two tasks overlap on [-1,0), where demand 3 exceeds capacity 2. +constraint cumulatives([-1,-1],[2,1],[2,1],2); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-cumulative-positive-duration-unsat.fzn b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-positive-duration-unsat.fzn new file mode 100644 index 0000000000..0c45f81d7a --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-positive-duration-unsat.fzn @@ -0,0 +1,4 @@ +% Positive duration makes the same excess demand impossible at every start. +var -2..2: s :: output_var; +constraint gecode_cumulatives([s],[1],[2],1); +solve minimize s; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-cumulative-unfixed-parameter.fzn b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-unfixed-parameter.fzn new file mode 100644 index 0000000000..929e25eb11 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-unfixed-parameter.fzn @@ -0,0 +1,7 @@ +% A normal equality does not establish an original singleton declaration/domain. +% The compiler must not substitute the lower bound or run propagation to fix d. +var 0..1: s; +var 1..2: d; +constraint int_eq(d,1); +constraint gecode_cumulatives([s],[d],[1],1); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-cumulative-unsupported-seven.fzn b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-unsupported-seven.fzn new file mode 100644 index 0000000000..313f553379 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-unsupported-seven.fzn @@ -0,0 +1,4 @@ +% The seven-argument MiniZinc wrapper is distinct from the four-argument poster. +var 0..1: s; +constraint fzn_cumulatives([s],[1],[1],[0],[1],true,0); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-cumulative-unsupported-six.fzn b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-unsupported-six.fzn new file mode 100644 index 0000000000..66bb334431 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-unsupported-six.fzn @@ -0,0 +1,4 @@ +% Machine assignments, per-machine bounds and polarity must never be truncated. +var 0..1: s; +constraint gecode_cumulatives([s],[1],[1],[0],[1],true); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-cumulative-wrong-arity.fzn b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-wrong-arity.fzn new file mode 100644 index 0000000000..9a92675fcb --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-wrong-arity.fzn @@ -0,0 +1,4 @@ +% The admitted legacy spelling still requires exactly four arguments. +var 0..1: s; +constraint cumulatives([s],[1],[1]); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-cumulative-zero-duration.fzn b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-zero-duration.fzn new file mode 100644 index 0000000000..426b6786b6 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-cumulative-zero-duration.fzn @@ -0,0 +1,5 @@ +% Half-open semantics: a zero-duration task consumes no capacity. +% The legacy p_cumulatives singleton shortcut incorrectly rejects height 2 > 1. +var -2..2: s :: output_var; +constraint gecode_cumulatives([s],[0],[2],1); +solve minimize s; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-domain-contiguous.fzn b/test/optimize/flatzinc-fixtures/cli-v2-domain-contiguous.fzn new file mode 100644 index 0000000000..a47d94290a --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-domain-contiguous.fzn @@ -0,0 +1,4 @@ +% Intersecting finite sets proves the exact interval [0,1], with no global left. +var {0,1,3}: x :: output_var; +constraint int_in(x,{0,1,2}); +solve minimize x; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-holey-alias.fzn b/test/optimize/flatzinc-fixtures/cli-v2-holey-alias.fzn new file mode 100644 index 0000000000..6fd789a1aa --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-holey-alias.fzn @@ -0,0 +1,7 @@ +% Alias membership intersection leaves {-1,1}; the row excludes -1. +% Replacing this domain with its hull would incorrectly admit the optimum 0. +var {-3,-1,1,4}: x :: output_var; +var int: y :: output_var = x; +constraint int_in(y,{-1,1,3}); +constraint int_le(0,y); +solve minimize x; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-table-alias-unsat.fzn b/test/optimize/flatzinc-fixtures/cli-v2-table-alias-unsat.fzn new file mode 100644 index 0000000000..f65b487cf9 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-table-alias-unsat.fzn @@ -0,0 +1,4 @@ +% Repeated positions are the same decision, so the only tuple cannot match. +var 0..1: x :: output_var; +constraint gecode_table_int([x,x],[0,1]); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-table-empty.fzn b/test/optimize/flatzinc-fixtures/cli-v2-table-empty.fzn new file mode 100644 index 0000000000..dd3879c8ef --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-table-empty.fzn @@ -0,0 +1,4 @@ +% A positive-arity relation with no tuples is false. +var 0..1: x :: output_var; +constraint gecode_table_int([x],[]); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-table-holes.fzn b/test/optimize/flatzinc-fixtures/cli-v2-table-holes.fzn new file mode 100644 index 0000000000..66f42f85d3 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-table-holes.fzn @@ -0,0 +1,5 @@ +% Row-major relation and a declared finite set both preserve original values. +var {-3,1,3}: x :: output_var; +var -2..2: y :: output_var; +constraint gecode_table_int([x,y],[-3,2,1,-2,3,1]); +solve minimize y; diff --git a/test/optimize/flatzinc-fixtures/cli-v2-table-zero-arity.fzn b/test/optimize/flatzinc-fixtures/cli-v2-table-zero-arity.fzn new file mode 100644 index 0000000000..06d5c024db --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v2-table-zero-arity.fzn @@ -0,0 +1,3 @@ +% Flattening lost whether this meant zero tuples or one empty tuple. +constraint gecode_table_int([],[]); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-alias-repeat.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-alias-repeat.fzn new file mode 100644 index 0000000000..1061c47cdc --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-alias-repeat.fzn @@ -0,0 +1,5 @@ +% Three source positions share one variable; repeated 2 is accepted. +var 1..2: x :: output_var; +var 1..2: y :: output_var = x; +constraint gecode_regular([x,y,x],2,2,[1,2,2,1],1,{2}); +solve minimize x; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-bad-count.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-bad-count.fzn new file mode 100644 index 0000000000..dd4886b15a --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-bad-count.fzn @@ -0,0 +1,3 @@ +% No initial state can belong to a zero-state automaton. +constraint gecode_regular([],0,1,[],1,{}); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-bad-initial.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-bad-initial.fzn new file mode 100644 index 0000000000..0810aff04c --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-bad-initial.fzn @@ -0,0 +1,3 @@ +% Initial state is outside 1..Q; empty words do not bypass admission. +constraint gecode_regular([],2,2,[1,2,2,1],3,{1}); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-bad-target.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-bad-target.fzn new file mode 100644 index 0000000000..eaae4e56b2 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-bad-target.fzn @@ -0,0 +1,3 @@ +% Every target must be in 0..Q, including unreachable transitions. +constraint gecode_regular([],2,2,[1,2,3,1],1,{1}); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-dead-transition.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-dead-transition.fzn new file mode 100644 index 0000000000..47ff7e4c1b --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-dead-transition.fzn @@ -0,0 +1,3 @@ +% Source target 0 is always failing, not the typed API's ordinary state 0. +constraint gecode_regular([1],1,1,[0],1,{1}); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-empty-accept.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-empty-accept.fzn new file mode 100644 index 0000000000..eb36700451 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-empty-accept.fzn @@ -0,0 +1,4 @@ +% No transitions are needed when the empty word starts in a final state. +array [1..0] of var 1..2: word :: output_array([1..0]) = []; +constraint gecode_regular(word,2,2,[0,0,0,0],2,{2}); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-empty-finals.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-empty-finals.fzn new file mode 100644 index 0000000000..eda129d2c5 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-empty-finals.fzn @@ -0,0 +1,3 @@ +% An empty final set rejects even an empty word. +constraint gecode_regular([],1,1,[1],1,{}); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-empty-reject.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-empty-reject.fzn new file mode 100644 index 0000000000..dc701c50a9 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-empty-reject.fzn @@ -0,0 +1,3 @@ +% Empty word does not visit another final state. +constraint gecode_regular([],2,2,[0,0,0,0],2,{1}); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-final-interval.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-final-interval.fzn new file mode 100644 index 0000000000..9f569ccb6a --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-final-interval.fzn @@ -0,0 +1,4 @@ +% Both destinations are final after expanding the parameter interval. +var 0..3: x :: output_var; +constraint gecode_regular([x],3,2,[2,3,0,0,0,0],1,2..3); +solve minimize x; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-malformed-matrix.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-malformed-matrix.fzn new file mode 100644 index 0000000000..7030793da7 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-malformed-matrix.fzn @@ -0,0 +1,3 @@ +% Q*S is 4, but only 3 cells are present. +constraint gecode_regular([1],2,2,[1,2,1],1,{1}); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-nonliteral-parameter.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-nonliteral-parameter.fzn new file mode 100644 index 0000000000..53fb3a47bc --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-nonliteral-parameter.fzn @@ -0,0 +1,5 @@ +% Q is a var reference even though its original domain is singleton. +% The explicit literal-parameter schema rejects it without propagation. +var 1..1: q; +constraint gecode_regular([1],q,1,[1],1,{1}); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-nonunit-finals.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-nonunit-finals.fzn new file mode 100644 index 0000000000..5acbc9a069 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-nonunit-finals.fzn @@ -0,0 +1,4 @@ +% The literal final set is noncontiguous and contains a harmless duplicate. +var 0..3: x :: output_var; +constraint gecode_regular([x],3,2,[2,3,0,0,0,0],1,{3,1,3}); +solve maximize x; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-rejected-word.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-rejected-word.fzn new file mode 100644 index 0000000000..d77967c2ff --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-rejected-word.fzn @@ -0,0 +1,3 @@ +% Word [1,1] ends in a nonfinal state, without a missing transition. +constraint gecode_regular([1,1],2,2,[1,2,2,1],1,{2}); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-unsupported-set.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-unsupported-set.fzn new file mode 100644 index 0000000000..3d6768cdc3 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-unsupported-set.fzn @@ -0,0 +1,3 @@ +% Seven-argument set-alphabet spelling is a separate unsupported interface. +constraint gecode_regular_set([1],1,1,1,[1],1,{1}); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-word.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-word.fzn new file mode 100644 index 0000000000..6876d0fe37 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-word.fzn @@ -0,0 +1,5 @@ +% Exactly the words [1,2] and [2,1] are accepted; min x is unique. +var 0..3: x :: output_var; +var 0..3: y :: output_var; +constraint gecode_regular([x,y],3,2,[2,3,0,3,3,0],1,{3}); +solve minimize x; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-wrong-arity.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-wrong-arity.fzn new file mode 100644 index 0000000000..4bca50814b --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-wrong-arity.fzn @@ -0,0 +1,3 @@ +% Six arguments are required; a missing final set is malformed. +constraint gecode_regular([1],1,1,[1],1); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/cli-v3-regular-zero-symbol.fzn b/test/optimize/flatzinc-fixtures/cli-v3-regular-zero-symbol.fzn new file mode 100644 index 0000000000..2d4395f815 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/cli-v3-regular-zero-symbol.fzn @@ -0,0 +1,3 @@ +% Source alphabet is 1..S; input zero must never become a valid symbol. +constraint gecode_regular([0],1,1,[1],1,{1}); +solve satisfy; diff --git a/test/optimize/flatzinc-fixtures/linear-alias.fzn b/test/optimize/flatzinc-fixtures/linear-alias.fzn new file mode 100644 index 0000000000..81990d17f4 --- /dev/null +++ b/test/optimize/flatzinc-fixtures/linear-alias.fzn @@ -0,0 +1,4 @@ +var 0..3: x; +var 1..2: y :: output_var = x; +array [1..4] of var 0..3: a :: output_array([-1..0,2..3]) = [x,x,2,3]; +solve minimize y; diff --git a/test/optimize/flatzinc-fixtures/unsupported.fzn b/test/optimize/flatzinc-fixtures/unsupported.fzn new file mode 100644 index 0000000000..f1cdf18add --- /dev/null +++ b/test/optimize/flatzinc-fixtures/unsupported.fzn @@ -0,0 +1,3 @@ +var 0..2: x :: output_var; +constraint int_times(x,x,x); +solve minimize x; diff --git a/test/optimize/flatzinc.cpp b/test/optimize/flatzinc.cpp new file mode 100644 index 0000000000..a2e675fd96 --- /dev/null +++ b/test/optimize/flatzinc.cpp @@ -0,0 +1,732 @@ +/* Independent source-language truth tables; no native parser/backend required. */ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace O=Gecode::Optimize; +namespace F=Gecode::FlatZinc::Capture; +using I=std::int64_t; +using Point=std::vector; +static std::size_t configurations=0,assignments=0; +F::Value integer(I x){F::Value v;v.integer=x;return v;} +F::Value boolean(bool x){F::Value v;v.kind=F::ValueKind::Boolean;v.boolean=x;return v;} +F::Value ref(std::size_t index,F::Type type=F::Type::Integer){F::Value v;v.kind=F::ValueKind::Reference;v.reference={type,index};return v;} +F::Value array(std::vector x){F::Value v;v.kind=F::ValueKind::Array;v.elements=std::move(x);return v;} +F::Value domain(I lo,I hi){F::Value v;v.kind=F::ValueKind::Set;v.set.interval=true;v.set.lower=lo;v.set.upper=hi;return v;} +F::Value members(std::vector values){auto v=domain(0,0);v.set.interval=false;v.set.values=std::move(values);return v;} +F::Value atom(const std::string& x){F::Value v;v.kind=F::ValueKind::Atom;v.text=x;return v;} +F::Value call(const std::string& x,std::vector args){auto v=atom(x);v.kind=F::ValueKind::Call;v.elements=std::move(args);return v;} +F::Variable variable(std::size_t index,I lo,I hi,F::Type type=F::Type::Integer){F::Variable v;v.reference={type,index};v.name="v"+std::to_string(index);v.domain.present=true;v.domain.integers=domain(lo,hi).set;return v;} +F::Constraint row(const std::string& name,std::vector args){F::Constraint c;c.id=name;c.arguments=std::move(args);c.location={"fixture.fzn",3,1};return c;} +O::CompiledFlatZinc compiled(const F::Records& source){auto result=O::compile_flatzinc(source);if(!result.compiled)std::cerr<<"compile failed: "<> mapped(const O::CompiledFlatZinc& c,const Point& point){ + std::vector values(c.model().variables.size());std::vector assigned(values.size());assert(point.size()==c.variables().size()); + for(std::size_t i=0;ibool { + if(slot==values.size())return O::validate(c.model(),values,0,0).valid; + if(assigned[slot])return self(self,slot+1); + const auto& v=c.model().variables[slot];assert(v.active&&v.upper-v.lower<=2); + for(I x=static_cast(v.lower);x<=static_cast(v.upper);++x){values[slot]=static_cast(x);if(self(self,slot+1))return true;} + return false; + }; + if(complete(complete,0))return values;return {}; +} +O::SolveResult witness(const O::CompiledFlatZinc& c,const Point& point,double objective=0){ + O::SolveResult result;result.model_id=c.model().model_id;result.revision=c.model().revision; + result.values=*mapped(c,point);result.active_variables.assign(result.values.size(),true);result.objective=objective; + result.solution_validated=true;result.termination=O::Termination::Optimal;result.guarantee=O::Guarantee::Exact;return result; +} +void oracle(const F::Records& source,const std::vector>& ranges,const std::function& predicate){ + const auto c=compiled(source);Point point(ranges.size()); + const auto visit=[&](const auto& self,std::size_t j)->void{ + if(j(status)<<" actual "<(r.status)<<"\n";assert(r.status==status&&!r.compiled);++configurations;} +void comparisons(){ + for(const auto& id:{"int_eq","int_le","int_lt","int_ge","int_gt"})for(bool left_literal:{false,true})for(bool right_literal:{false,true}){ + F::Records r;r.raw_variables={variable(0,-2,2),variable(1,-2,2)}; + r.raw_constraints={row(id,{left_literal?integer(-1):ref(0),right_literal?integer(1):ref(1)})}; + oracle(r,{{-2,2},{-2,2}},[&](const Point& p){auto a=left_literal?-1:p[0],b=right_literal?1:p[1];std::string s=id;return s=="int_eq"?a==b:s=="int_le"?a<=b:s=="int_lt"?a=b:a>b;}); + } + for(const auto& id:{"int_plus","int_minus"}){F::Records r;r.raw_variables={variable(0,-2,2),variable(1,-2,2),variable(2,-2,2)}; + r.raw_constraints={row(id,{ref(0),ref(1),ref(2)})};oracle(r,{{-2,2},{-2,2},{-2,2}},[&](const Point& p){return (std::string(id)=="int_plus"?p[0]+p[1]:p[0]-p[1])==p[2];});} + for(int a=-3;a<=3;++a)for(int b=-3;b<=3;++b)for(bool equality:{false,true}){ + F::Records r;r.raw_variables={variable(0,-2,2),variable(1,-2,2)}; + r.raw_constraints={row(equality?"int_lin_eq":"int_lin_le",{array({integer(a),integer(b),integer(-1)}),array({ref(0),ref(1),ref(0)}),integer(1)})}; + oracle(r,{{-2,2},{-2,2}},[&](const Point& p){auto sum=(a-1)*p[0]+b*p[1];return equality?sum==1:sum<=1;}); + } +} +void booleans(){ + const auto B=F::Type::Boolean; + for(const auto& id:{"bool_eq","bool_le","bool_not","bool_and","bool_or"}){ + F::Records r;r.raw_variables={variable(0,0,1,B),variable(1,0,1,B),variable(2,0,1,B)}; + std::vector args{ref(0,B),ref(1,B)};if(std::string(id)=="bool_and"||std::string(id)=="bool_or")args.push_back(ref(2,B)); + r.raw_constraints={row(id,args)};oracle(r,{{0,1},{0,1},{0,1}},[&](const Point& p){std::string s=id;return s=="bool_eq"?p[0]==p[1]:s=="bool_le"?p[0]<=p[1]:s=="bool_not"?p[0]!=p[1]:s=="bool_and"?((p[0]&&p[1])==p[2]):((p[0]||p[1])==p[2]);}); + } + for(bool conjunction:{false,true})for(int size=0;size<=4;++size)for(bool literal_result:{false,true}){ + F::Records r;r.raw_variables={variable(0,0,1,B),variable(1,0,1,B)};std::vector args; + for(int i=0;i a(n,ref(0,B)),b(k,ref(1,B));r.raw_constraints={row("bool_clause",{array(a),array(b)})};oracle(r,{{0,1},{0,1}},[&](const Point& p){return(n&&p[0])||(k&&!p[1]);});} + F::Records channel;channel.raw_variables={variable(0,0,1,B),variable(0,-1,2)};channel.raw_constraints={row("bool2int",{ref(0,B),ref(0)})}; + oracle(channel,{{0,1},{-1,2}},[](const Point& p){return p[0]==p[1];}); + channel.raw_constraints={row("bool_lin_eq",{array({integer(2),integer(-1)}),array({ref(0,B),boolean(true)}),ref(0)})}; + oracle(channel,{{0,1},{-1,2}},[](const Point& p){return 2*p[0]-1==p[1];}); +} +void aliases_domains(){ + // Domain-based binary recognition must not change source typing, aliases, + // singleton restrictions, or acceptance of assignments outside the domain. + F::Records binary;binary.raw_variables={variable(0,0,1),variable(1,-2,3),variable(2,1,1),variable(3,0,2)}; + binary.raw_variables[1].alias=true;binary.raw_variables[1].target={F::Type::Integer,0}; + binary.output={{"integer_decision",ref(0)}}; + const auto classified=compiled(binary); + assert(classified.model().variables[0].type==O::VariableType::Binary); + assert(classified.variables()[0].variable==classified.variables()[1].variable); + assert(classified.model().variables[1].type==O::VariableType::Binary); + assert(classified.model().variables[2].type==O::VariableType::Integer); + assert(O::format_flatzinc_solution(classified,witness(classified,{1,1,1,2}))=="integer_decision = 1;\n----------\n"); + oracle(binary,{{-1,2},{-1,2},{0,2},{-1,3}},[](const Point& p){ + return p[0]>=0&&p[0]<=1&&p[0]==p[1]&&p[2]==1&&p[3]>=0&&p[3]<=2; + }); + F::Records r;r.raw_variables={variable(0,-2,3),variable(1,-9,9),variable(2,-9,9)}; + r.raw_variables[1].alias=true;r.raw_variables[1].target={F::Type::Integer,0};r.raw_variables[2].alias=true;r.raw_variables[2].target={F::Type::Integer,1}; + r.raw_domains={row("int_in",{ref(1),domain(0,2)})}; + // Normalized audit data must never replace a raw relation. + r.constraints={row("unsupported_audit_only",{})}; + r.raw_constraints={row("int_le",{ref(2),integer(1)})}; + oracle(r,{{-2,3},{-2,3},{-2,3}},[](const Point& p){return p[0]==p[1]&&p[1]==p[2]&&p[0]>=0&&p[0]<=1;}); + r.raw_domains.push_back(row("int_in",{ref(2),domain(3,4)}));oracle(r,{{-2,3},{-2,3},{-2,3}},[](const Point&){return false;}); + F::Records empty;empty.raw_variables={variable(0,1,0)};oracle(empty,{{-1,1}},[](const Point&){return false;}); + empty.raw_variables[0].domain.integers=domain(0,1).set;empty.raw_variables[0].assigned=true;empty.raw_variables[0].value=integer(1); + oracle(empty,{{-1,2}},[](const Point& p){return p[0]==1;}); + empty.raw_variables[0].assigned=false;empty.raw_variables[0].domain.present=false;empty.raw_domains={row("int_in",{ref(0),domain(-1,1)})}; + oracle(empty,{{-2,2}},[](const Point& p){return p[0]>=-1&&p[0]<=1;}); + auto set=domain(0,0);set.set.interval=false;set.set.values={2,0,1,1};empty.raw_domains={row("int_in",{ref(0),set})}; + oracle(empty,{{-1,3}},[](const Point& p){return p[0]>=0&&p[0]<=2;}); + set.set.values={0,2};empty.raw_domains={row("int_in",{ref(0),set})};oracle(empty,{{-1,3}},[](const Point& p){return p[0]==0||p[0]==2;}); +} +void failures(){ + F::Records r;r.raw_variables={variable(0,0,2)}; + for(const auto& id:{"unknown_plugin","int_eq_reif","int_lin_eq_reif","int_times","regular","gecode_int_le","array_bool_element"}){r.raw_constraints={row(id,{})};rejected(r);} + r.raw_constraints={row("cumulatives",{})};rejected(r,O::FlatZincCompileStatus::InvalidInput); + r.raw_constraints={row("int_eq",{})};rejected(r,O::FlatZincCompileStatus::InvalidInput); + r.raw_constraints={row("int_eq",{ref(3),integer(0)})};rejected(r,O::FlatZincCompileStatus::InvalidInput); + r.raw_constraints={row("int_eq",{boolean(false),integer(0)})};rejected(r,O::FlatZincCompileStatus::InvalidInput); + r.raw_constraints.clear();r.raw_variables[0].alias=true;r.raw_variables[0].target={F::Type::Integer,0};rejected(r,O::FlatZincCompileStatus::InvalidInput); + r.raw_variables={variable(0,0,1),variable(0,0,1)};rejected(r,O::FlatZincCompileStatus::InvalidInput); + r.raw_variables={variable(0,0,1,F::Type::Float)};rejected(r); + r.raw_variables={variable(0,0,1)};r.solve.annotations={call("int_search",{})};rejected(r); + r.solve.annotations.clear();r.raw_constraints={row("int_le",{ref(0),integer(0)})};r.raw_constraints[0].annotations={atom("unknown_annotation")};rejected(r); + for(const auto& name:{"ctx_pos","ctx_neg","ctx_mix"}) { + r.raw_constraints[0].annotations={atom(name)}; + oracle(r,{{0,1}},[](const Point& p){return p[0]<=0;}); + for(const auto& args:{std::vector{},std::vector{integer(0)}}) { + r.raw_constraints[0].annotations={call(name,args)};rejected(r,O::FlatZincCompileStatus::InvalidInput); + } + auto malformed=atom(name);malformed.elements={integer(0)}; + r.raw_constraints[0].annotations={malformed};rejected(r,O::FlatZincCompileStatus::InvalidInput); + } + r.raw_constraints[0].annotations={atom("bounds")};assert(O::compile_flatzinc(r).compiled); + r.raw_constraints[0].annotations={call("defines_var",{ref(99)})};rejected(r,O::FlatZincCompileStatus::InvalidInput); + r.raw_constraints[0].annotations={atom("bounds")}; + r.declaration_annotations={{"missing",{atom("output_var")},{}}};rejected(r,O::FlatZincCompileStatus::InvalidInput);r.declaration_annotations.clear(); + for(int mode=0;mode<5;++mode){O::FlatZincCompileOptions opts; + if(mode==0)opts.max_work=0;if(mode==1)opts.max_variables=0;if(mode==2)opts.max_constraints=0;if(mode==3)opts.max_nonzeros=0;if(mode==4)opts.max_value_depth=0; + if(mode==4)r.raw_constraints[0].arguments[0]=array({ref(0)}); + auto result=O::compile_flatzinc(r,opts);assert(result.status==O::FlatZincCompileStatus::ResourceLimit&&!result.compiled); + } + r.raw_constraints.clear();O::FlatZincCompileOptions opts;opts.time_limit_seconds=0;assert(O::compile_flatzinc(r,opts).status==O::FlatZincCompileStatus::TimeLimit); + opts.time_limit_seconds=10;opts.cancellation=std::make_shared();opts.cancellation->cancel();assert(O::compile_flatzinc(r,opts).status==O::FlatZincCompileStatus::Cancelled); +} +void history_output(){ + F::Records r;r.raw_variables={variable(0,-2,3),variable(0,0,1,F::Type::Boolean)}; + r.output={{"x",ref(0)},{"b",ref(0,F::Type::Boolean)},{"a",array({ref(0),ref(0),integer(7),integer(-3)})}}; + r.declaration_annotations={{"a",{call("output_array",{array({domain(-1,0),domain(2,3)})})},{}}}; + for(auto method:{F::Method::Minimize,F::Method::Maximize})for(bool constant:{false,true}) { + r.solve.method=method;r.solve.has_objective=true;r.solve.objective=constant?integer(7):ref(0); + auto c=compiled(r);auto result=witness(c,{2,1},constant?7:2);auto checked=O::validate_flatzinc(c,result);assert(checked.valid&&checked.original_objective==(constant?7:2)); + assert(O::format_flatzinc_solution(c,result)=="x = 2;\nb = true;\na = array2d(-1..0, 2..3, [2, 2, 7, -3]);\n----------\n"); + result.revision++;assert(!O::validate_flatzinc(c,result).valid);result.revision--; + result.objective=99;assert(!O::validate_flatzinc(c,result).valid);result.objective=constant?7:2; + result.values[0]=2.0000001;assert(O::validate_flatzinc(c,result).valid);result.values[0]=2.1;assert(!O::validate_flatzinc(c,result).valid); + result.values[0]=2;result.active_variables[0]=false;assert(!O::validate_flatzinc(c,result).valid); + bool threw=false;try{O::format_flatzinc_solution(c,result);}catch(const O::ModelError&){threw=true;}assert(threw); + } + auto c=compiled(r);r.raw_variables.clear();r.output.clear();assert(c.source().raw_variables.size()==2&&c.source().output.size()==3); + F::Records empty;auto e=compiled(empty);assert(O::format_flatzinc_solution(e,witness(e,{}))=="----------\n"); +} +void actual_backends(){ + F::Records r;r.raw_variables={variable(0,-2,4),variable(1,-2,4)};r.raw_constraints={row("int_lin_le",{array({integer(-2),integer(-1)}),array({ref(0),ref(1)}),integer(-5)})}; + r.solve.method=F::Method::Minimize;r.solve.has_objective=true;r.solve.objective=ref(0);auto c=compiled(r); + for(auto backend:{O::Backend::Highs,O::Backend::Native})if(O::capabilities(backend).available){O::SolveOptions opts;opts.backend=backend;opts.relative_gap=0;opts.absolute_gap=0;opts.guarantee=backend==O::Backend::Native?O::Guarantee::Exact:O::Guarantee::Numerical;auto solved=O::solve(c.model(),opts);assert(solved.termination==O::Termination::Optimal&&solved.objective==1&&O::validate_flatzinc(c,solved).valid);} +} +void guarded(){ + const auto B=F::Type::Boolean; + for(const auto& id:{"int_le_reif","int_le_imp","int_eq_imp"})for(int guard=0;guard<4;++guard)for(int operands=0;operands<4;++operands){ + F::Records r;r.raw_variables={variable(0,-2,2),variable(1,-2,2),variable(0,0,1,B)}; + if(guard==3){r.raw_variables[2].assigned=true;r.raw_variables[2].value=boolean(false);} + r.raw_constraints={row(id,{operands&1?integer(-1):ref(0),operands&2?integer(1):ref(1),guard==1?boolean(false):guard==2?boolean(true):ref(0,B)})}; + oracle(r,{{-2,2},{-2,2},{0,1}},[&](const Point& p){const auto a=operands&1?-1:p[0],b=operands&2?1:p[1];const bool g=guard==1?false:guard==2?true:p[2]; + const bool condition=std::string(id)=="int_eq_imp"?a==b:a<=b; + return(guard!=3||p[2]==0)&&(std::string(id)=="int_le_reif"?condition==g:!g||condition);}); + } + for(bool equivalence:{false,true})for(int a=-3;a<=3;++a)for(int b=-3;b<=3;++b)for(int guard=0;guard<3;++guard){ + F::Records r;r.raw_variables={variable(0,-2,2),variable(1,-2,2),variable(0,0,1,B)}; + r.raw_constraints={row(equivalence?"int_lin_le_reif":"int_lin_le_imp",{array({integer(a),integer(b),integer(-1)}),array({ref(0),ref(1),ref(0)}),integer(1),guard==1?boolean(false):guard==2?boolean(true):ref(0,B)})}; + oracle(r,{{-2,2},{-2,2},{0,1}},[&](const Point& p){const bool g=guard==1?false:guard==2?true:p[2],condition=(a-1)*p[0]+b*p[1]<=1;return equivalence?condition==g:!g||condition;}); + } + for(I rhs=-1;rhs<=1;++rhs){F::Records r;r.raw_variables={variable(0,0,1,B)};r.raw_constraints={row("int_lin_le_reif",{array({}),array({}),integer(rhs),ref(0,B)})};oracle(r,{{0,1}},[&](const Point& p){return(0<=rhs)==bool(p[0]);});} + F::Records literals;literals.raw_variables={variable(0,-2,2),variable(0,0,1,B)}; + literals.raw_constraints={row("int_lin_le_reif",{array({integer(-2),integer(3)}),array({ref(0),integer(-1)}),integer(-2),ref(0,B)})}; + oracle(literals,{{-2,2},{0,1}},[](const Point& p){return(-2*p[0]-3<=-2)==bool(p[1]);}); + O::FlatZincCompileOptions no_helpers;no_helpers.max_variables=2; + assert(O::compile_flatzinc(literals,no_helpers).status==O::FlatZincCompileStatus::ResourceLimit); + F::Records alias;alias.raw_variables={variable(0,-1,1),variable(1,-1,1),variable(0,0,1,B),variable(1,0,1,B)}; + alias.raw_variables[1].alias=true;alias.raw_variables[1].target={F::Type::Integer,0};alias.raw_variables[3].alias=true;alias.raw_variables[3].target={B,0}; + alias.raw_constraints={row("int_le_reif",{ref(0),ref(1),ref(1,B)})}; + oracle(alias,{{-1,1},{-1,1},{0,1},{0,1}},[](const Point& p){return p[0]==p[1]&&p[2]==1&&p[3]==1;}); + auto c=compiled(alias);auto proof=witness(c,{0,0,1,1}); + std::set visible;for(const auto& m:c.variables())visible.insert(m.variable.id); + for(std::size_t i=0;i values=variant==0?std::vector{}:variant==1?std::vector{ref(0)}:variant==2?std::vector{ref(0),ref(0)}:variant==3?std::vector{ref(0),ref(1),integer(1)}:std::vector{integer(1),integer(1)}; + r.raw_constraints={row("all_different_int",{array(values)})}; + oracle(r,{{-1,2},{-1,2}},[&](const Point& p){return variant<2?true:variant==2||variant==4?false:p[0]!=p[1]&&p[0]!=1&&p[1]!=1;}); + } + for(const auto& id:{"array_int_element","array_var_int_element"})for(int variant=0;variant<5;++variant){ + if(std::string(id)=="array_int_element"&&variant>=2)continue; + F::Records r;r.raw_variables={variable(0,0,3),variable(1,-1,2),variable(2,-1,2),variable(3,-1,2)}; + const std::vector elements=variant==0?std::vector{}:variant==1?std::vector{integer(-1),integer(2)}:variant==2?std::vector{ref(2),ref(2)}:std::vector{ref(2),integer(1),ref(3)}; + r.raw_constraints={row(id,{variant==4?integer(2):ref(0),array(elements),variant==3?ref(0):ref(1)})}; + oracle(r,{{0,3},{-1,2},{-1,2},{-1,2}},[&](const Point& p){const I index=variant==4?2:p[0],result=variant==3?p[0]:p[1]; + if(index<1||static_cast(index)>elements.size())return false; + const I selected=variant==1?(index==1?-1:2):variant==2?p[2]:index==1?p[2]:index==2?1:p[3];return selected==result;}); + } + F::Records aliases;aliases.raw_variables={variable(0,-1,2),variable(1,-1,2)};aliases.raw_variables[1].alias=true;aliases.raw_variables[1].target={F::Type::Integer,0}; + aliases.raw_constraints={row("all_different_int",{array({ref(0),ref(1)})})};oracle(aliases,{{-1,2},{-1,2}},[](const Point&){return false;}); + aliases.raw_constraints={row("array_int_element",{integer(1),array({ref(0)}),ref(1)})};rejected(aliases,O::FlatZincCompileStatus::InvalidInput); + F::Records limits;limits.raw_variables={variable(0,0,2)};limits.raw_constraints={row("all_different_int",{array({ref(0),integer(1)})})}; + O::FlatZincCompileOptions cap;cap.max_variables=1;assert(O::compile_flatzinc(limits,cap).status==O::FlatZincCompileStatus::ResourceLimit); + cap={};cap.max_nonzeros=1;assert(O::compile_flatzinc(limits,cap).status==O::FlatZincCompileStatus::ResourceLimit); + auto c=compiled(limits); + if(O::capabilities(O::Backend::Highs).available){O::SolveOptions opt;opt.backend=O::Backend::Highs;auto r=O::solve(c.model(),opt);assert(r.termination==O::Termination::Unsupported&&!r.has_solution());} + if(O::capabilities(O::Backend::Native).available){O::SolveOptions opt;opt.backend=O::Backend::Native;opt.guarantee=O::Guarantee::Exact;auto r=O::solve(c.model(),opt);assert(r.termination==O::Termination::Optimal&&O::validate_flatzinc(c,r,0).valid);} + F::Records combined;combined.raw_variables={variable(0,1,3),variable(1,1,3),variable(0,0,1,F::Type::Boolean)}; + combined.raw_constraints={row("all_different_int",{array({ref(0),ref(1)})}),row("array_var_int_element",{integer(1),array({ref(0),integer(3)}),ref(1)}),row("int_le_reif",{ref(0),ref(1),ref(0,F::Type::Boolean)})}; + oracle(combined,{{1,3},{1,3},{0,1}},[](const Point&){return false;}); + if(O::capabilities(O::Backend::Native).available){ + O::SolveOptions opt;opt.backend=O::Backend::Native;opt.guarantee=O::Guarantee::Exact; + auto impossible=compiled(combined);assert(O::solve(impossible.model(),opt).termination==O::Termination::Infeasible); + combined.raw_constraints[1]=row("array_var_int_element",{integer(2),array({ref(0),ref(1)}),ref(1)}); + auto possible=compiled(combined);auto r=O::solve(possible.model(),opt); + assert(r.termination==O::Termination::Optimal&&O::validate_flatzinc(possible,r,0).valid); + } +} + +void holey_domains(){ + // Every pair of subsets of a signed five-point universe: intersection is + // checked through bit membership, independently of normalization/lowering. + for(unsigned first=0;first<32;++first)for(unsigned second=0;second<32;++second){ + F::Records r;r.raw_variables={variable(7,-2,2)};std::vector a,b; + for(int bit=4;bit>=0;--bit){if(first&(1u<{2,1}:std::vector{-2,0})})}; + oracle(r,{},[&](const Point&){return allowed;}); + } + F::Records empty;empty.raw_variables={variable(0,0,1)};empty.raw_variables[0].domain.present=false; + empty.raw_domains={row("int_in",{ref(0),members({})})};oracle(empty,{{-1,1}},[](const Point&){return false;}); + empty.raw_variables[0].domain.integers=members({0,2,3,4,6}).set;empty.raw_variables[0].domain.present=true; + empty.raw_domains={row("int_in",{ref(0),domain(2,4)})}; + auto contiguous=compiled(empty);assert(contiguous.model().globals.empty()); + oracle(empty,{{0,6}},[](const Point& p){return p[0]>=2&&p[0]<=4;}); + empty.raw_domains={row("int_in",{ref(0),members({3})})};assert(compiled(empty).model().globals.empty()); + // Sparse gaps stay symbolic even at the limits of exact model representation. + const I large=9007199254740992LL; + F::Records sparse;sparse.raw_variables={variable(8,-large,large)}; + sparse.raw_variables[0].domain.integers=members({large,-large,large}).set; + auto c=compiled(sparse);assert(c.model().variables.size()==1&&c.model().globals.size()==1); + assert(c.model().variables[0].lower==-large&&c.model().variables[0].upper==large); + assert(mapped(c,{-large})&&mapped(c,{large})&&!mapped(c,{0})); + auto historical=witness(c,{large});assert(O::validate_flatzinc(c,historical,0).valid); + sparse.raw_variables.clear();assert(O::validate_flatzinc(c,historical,0).valid); + historical.values[0]=0;assert(!O::validate_flatzinc(c,historical,0).valid); + historical.values[0]=large;historical.active_variables[0]=false;assert(!O::validate_flatzinc(c,historical,0).valid); + empty.raw_domains={row("int_in",{ref(0),members({large+1})})};rejected(empty); + // A contradictory earlier domain must not hide an inadmissible later value. + empty.raw_variables[0].domain.integers=members({}).set;rejected(empty); + F::Records boolean_domain;boolean_domain.raw_variables={variable(0,0,1,F::Type::Boolean)}; + boolean_domain.raw_domains={row("int_in",{ref(0,F::Type::Boolean),members({3,1,-1})})}; + oracle(boolean_domain,{{0,1}},[](const Point& p){return p[0]==1;}); +} +void tables(){ + // All relations of arities one through three on {0,1}; order and duplicate + // rows are deliberately perturbed. The oracle interprets a relation bitset. + for(unsigned arity=1;arity<=3;++arity)for(unsigned relation=0;relation<(1u<<(1u< arguments,flat; + for(unsigned j=0;j=0;--tuple)if(relation&(1u<>j)&1u)); + r.raw_constraints={row("gecode_table_int",{array(arguments),array(flat)})}; + oracle(r,std::vector>(arity,{0,1}),[&](const Point& p){unsigned index=0;for(unsigned j=0;j{integer(-2),integer(allowed?-2:1)}:std::vector{})})}; + oracle(fixed,{},[&](const Point&){return nonempty&&allowed;}); + } + F::Records malformed;malformed.raw_variables={variable(0,0,1)}; + const auto reject=[&](std::vector args,O::FlatZincCompileStatus status=O::FlatZincCompileStatus::InvalidInput){malformed.raw_constraints={row("gecode_table_int",std::move(args))};rejected(malformed,status);}; + reject({});reject({array({ref(0)})});reject({ref(0),array({})}); + reject({array({ref(0),ref(0)}),array({integer(0)})}); + reject({array({ref(0)}),array({ref(0)})});reject({array({ref(0)}),array({boolean(true)})}); + reject({array({boolean(true)}),array({integer(1)})});reject({array({ref(9)}),array({integer(0)})}); + reject({array({}),array({})},O::FlatZincCompileStatus::Unsupported);reject({array({}),array({integer(0)})}); + reject({array({ref(0)}),array({integer(9007199254740993LL)})},O::FlatZincCompileStatus::Unsupported); + for(const auto& id:{"table_int","fzn_table_int","gecode_gecode_table_int","gecode_table_bool"}){malformed.raw_constraints={row(id,{array({ref(0)}),array({integer(0)})})};rejected(malformed);} +} +void table_domain_limits(){ + F::Records r;r.raw_variables={variable(0,-2,2)};r.raw_variables[0].domain.integers=members({2,-2,0,2}).set; + r.raw_constraints={row("gecode_table_int",{array({ref(0),integer(0)}),array({integer(-2),integer(0),integer(2),integer(0)})})}; + auto full=O::compile_flatzinc(r);assert(full.compiled&&full.work>0); + for(const auto cap:{std::size_t(0),std::size_t(1),full.work/2,full.work-1}){ + O::FlatZincCompileOptions options;options.max_work=cap;auto result=O::compile_flatzinc(r,options); + assert(result.status==O::FlatZincCompileStatus::ResourceLimit&&!result.compiled&&result.work<=cap); + } + O::FlatZincCompileOptions options;options.max_work=full.work;assert(O::compile_flatzinc(r,options).compiled); + for(int mode=0;mode<3;++mode){options={};if(mode==0)options.max_variables=1;if(mode==1)options.max_constraints=1;if(mode==2)options.max_nonzeros=9; + auto result=O::compile_flatzinc(r,options);assert(result.status==O::FlatZincCompileStatus::ResourceLimit&&!result.compiled); + } + // Unary domain: one operand plus three distinct values; binary table: two + // operands plus four cells. Both footprint limits are reserved before posting. + options={};options.max_variables=2;options.max_constraints=2;options.max_nonzeros=10;assert(O::compile_flatzinc(r,options).compiled); + F::Records duplicates;duplicates.raw_variables={variable(0,0,1)};duplicates.raw_variables[0].domain.integers=members({0,0,0,0}).set; + options={};options.max_nonzeros=3;auto result=O::compile_flatzinc(duplicates,options); + assert(result.status==O::FlatZincCompileStatus::ResourceLimit&&!result.compiled); + options={};options.time_limit_seconds=0;assert(O::compile_flatzinc(r,options).status==O::FlatZincCompileStatus::TimeLimit); + options={};options.cancellation=std::make_shared();options.cancellation->cancel();assert(O::compile_flatzinc(r,options).status==O::FlatZincCompileStatus::Cancelled); +} +void table_domain_backends(){ + F::Records r;r.raw_variables={variable(0,-3,3),variable(1,-3,3),variable(0,0,1,F::Type::Boolean)}; + r.raw_variables[0].domain.integers=members({3,-3,1}).set; + r.raw_constraints={row("gecode_table_int",{array({ref(0),ref(1)}),array({integer(-3),integer(2),integer(1),integer(-2),integer(3),integer(1)})}), + row("all_different_int",{array({ref(0),ref(1)})}), + row("array_var_int_element",{integer(1),array({ref(0),ref(1)}),ref(0)}), + row("int_le_reif",{ref(0),ref(1),ref(0,F::Type::Boolean)})}; + oracle(r,{{-3,3},{-3,3},{0,1}},[](const Point& p){return ((p[0]==-3&&p[1]==2)||(p[0]==1&&p[1]==-2)||(p[0]==3&&p[1]==1))&&bool(p[2])==(p[0]<=p[1]);}); + for(auto method:{F::Method::Minimize,F::Method::Maximize}){ + r.solve.method=method;r.solve.has_objective=true;r.solve.objective=ref(0);auto c=compiled(r); + O::SolveOptions options;options.backend=O::Backend::Highs; + auto unsupported=O::solve(c.model(),options);assert(unsupported.termination==O::Termination::Unsupported&&!unsupported.has_solution()); + for(auto backend:{O::Backend::Native,O::Backend::Auto}){options.backend=backend;options.guarantee=O::Guarantee::Exact; + auto solved=O::solve(c.model(),options); + if(O::capabilities(O::Backend::Native).available)assert(solved.termination==O::Termination::Optimal&&solved.objective==(method==F::Method::Minimize?-3:3)&&O::validate_flatzinc(c,solved,0).valid); + else assert(solved.termination==O::Termination::Unsupported&&!solved.has_solution()); + } + } + // Eliminating all holes restores the linear backend path without weakening + // the original list: only the exact interval intersection remains feasible. + F::Records contiguous;contiguous.raw_variables={variable(0,0,6)}; + contiguous.raw_variables[0].domain.integers=members({0,2,3,4,6}).set; + contiguous.raw_domains={row("int_in",{ref(0),domain(2,4)})}; + contiguous.solve.method=F::Method::Minimize;contiguous.solve.has_objective=true;contiguous.solve.objective=ref(0); + auto linear=compiled(contiguous);assert(linear.model().globals.empty()); + O::SolveOptions linear_options;linear_options.backend=O::Backend::Highs; + auto linear_result=O::solve(linear.model(),linear_options); + if(O::capabilities(O::Backend::Highs).available)assert(linear_result.termination==O::Termination::Optimal&&linear_result.objective==2&&O::validate_flatzinc(linear,linear_result,0).valid); + else assert(linear_result.termination==O::Termination::Unsupported&&!linear_result.has_solution()); + F::Records large;large.raw_variables={variable(0,-9007199254740992LL,9007199254740992LL)}; + large.raw_variables[0].domain.integers=members({-9007199254740992LL,9007199254740992LL}).set; + auto wide=compiled(large);O::SolveOptions native;native.backend=O::Backend::Native;native.guarantee=O::Guarantee::Exact; + auto native_result=O::solve(wide.model(),native);assert(native_result.termination==O::Termination::Unsupported&&!native_result.has_solution()); + F::Records empty;empty.raw_variables={variable(0,0,1)};empty.raw_constraints={row("gecode_table_int",{array({ref(0)}),array({})})}; + auto c=compiled(empty);O::SolveOptions options;options.backend=O::Backend::Native;options.guarantee=O::Guarantee::Exact; + const auto result=O::solve(c.model(),options);assert(result.termination==(O::capabilities(O::Backend::Native).available?O::Termination::Infeasible:O::Termination::Unsupported)); +} + +// Independent circuit oracle: generate Hamiltonian cycles from permutations of +// nodes 1..n-1, rather than reusing either checker's successor-walk algorithm. +std::set circuit_points(std::size_t size,I offset){ + assert(size>0);std::vector order; + for(std::size_t i=1;i points; + do{Point successor(size);std::size_t previous=0; + for(auto next:order){successor[previous]=offset+static_cast(next);previous=next;} + successor[previous]=offset;points.insert(std::move(successor)); + }while(std::next_permutation(order.begin(),order.end())); + return points; +} +void circuits(){ + for(I offset:{I(0),I(1),I(3)})for(std::size_t size=1;size<=5;++size){ + F::Records r;std::vector arguments; + for(std::size_t i=0;i(size)));arguments.push_back(ref(i));} + r.raw_constraints={row("gecode_circuit",{integer(offset),array(arguments)})}; + const auto allowed=circuit_points(size,offset); + oracle(r,std::vector>(size,{offset-1,offset+static_cast(size)}),[&](const Point& p){return allowed.count(p)!=0;}); + const auto artifact=compiled(r);const auto& data=std::get(artifact.model().globals.at(0).payload); + assert(data.index_base==offset&&data.successors.size()==size); + for(std::size_t i=0;i arguments{ref(0),variant==1?ref(0):ref(1),variant==0?ref(2):integer(3)}; + r.raw_constraints={row("gecode_circuit",{integer(3),array(arguments)})};const auto allowed=circuit_points(3,3); + oracle(r,{{2,5},{2,5},{2,5}},[&](const Point& p){ + return(variant!=2||p[0]==p[1])&&allowed.count({p[0],variant==1?p[0]:p[1],variant==0?p[2]:3}); + }); + } + for(const auto& tuple:std::vector{{3},{2},{4,3},{4,5,3},{4,3,5},{3,4,5}}){ + F::Records r;std::vector args;for(auto value:tuple)args.push_back(integer(value)); + r.raw_constraints={row("gecode_circuit",{integer(3),array(args)})};const auto allowed=circuit_points(tuple.size(),3); + oracle(r,{},[&](const Point&){return allowed.count(tuple)!=0;}); + } + // Owning source, hidden constant slots and output positions remain intact. + F::Records r;r.raw_variables={variable(0,3,5),variable(1,3,5)}; + r.raw_constraints={row("gecode_circuit",{integer(3),array({ref(0),ref(1),integer(3)})})};r.output={{"first",ref(0)}}; + auto saved=compiled(r);auto proof=witness(saved,{4,5});assert(proof.values.size()==3&&saved.variables().size()==2); + r.raw_variables.clear();r.raw_constraints.clear();r.output.clear(); + assert(O::validate_flatzinc(saved,proof,0).valid&&O::format_flatzinc_solution(saved,proof,0)=="first = 4;\n----------\n"); + proof.values[0]=3;assert(!O::validate_flatzinc(saved,proof,0).valid); + proof=witness(saved,{4,5});proof.values.back()=4;assert(!O::validate_flatzinc(saved,proof,0).valid); +} +void circuit_boundaries(){ + F::Records r;r.raw_variables={variable(0,0,1),variable(0,0,1,F::Type::Boolean)}; + const auto reject=[&](std::vector args,O::FlatZincCompileStatus status=O::FlatZincCompileStatus::InvalidInput){r.raw_constraints={row("gecode_circuit",std::move(args))};rejected(r,status);}; + reject({});reject({integer(0)});reject({integer(0),array({ref(0)}),integer(0)}); + reject({array({ref(0)}),integer(0)});reject({ref(0),array({ref(0)})}); + reject({boolean(false),array({ref(0)})});reject({integer(0),ref(0)});reject({integer(0),array({})}); + reject({integer(0),array({boolean(true)})});reject({integer(0),array({ref(0,F::Type::Boolean)})}); + reject({integer(0),array({ref(7)})});reject({integer(0),array({array({integer(0)})})}); + reject({integer(-1),array({integer(-1)})},O::FlatZincCompileStatus::Unsupported); + constexpr I large=9007199254740992LL; + reject({integer(large+1),array({integer(0)})},O::FlatZincCompileStatus::Unsupported); + reject({integer(large),array({integer(0),integer(0)})},O::FlatZincCompileStatus::Unsupported); + reject({integer(0),array({integer(large+1)})},O::FlatZincCompileStatus::Unsupported); + for(const auto& id:{"circuit","fzn_circuit","fzn_gecode_circuit","gecode_gecode_circuit","gecode_subcircuit","gecode_circuit_cost","gecode_circuit_reif"}){ + r.raw_constraints={row(id,{integer(0),array({ref(0)})})};rejected(r); + } + // Exact frontend range and native implementation range are separate gates. + for(const auto& tuple:std::vector>{{large,{large}},{large-1,{large,large-1}}}){ + F::Records wide;std::vector args;for(auto v:tuple.second)args.push_back(integer(v)); + wide.raw_constraints={row("gecode_circuit",{integer(tuple.first),array(args)})}; + oracle(wide,{},[](const Point&){return true;});auto c=compiled(wide); + O::SolveOptions options;options.backend=O::Backend::Native;options.guarantee=O::Guarantee::Exact; + assert(O::solve(c.model(),options).termination==O::Termination::Unsupported); + } + F::Records bounded;bounded.raw_variables={variable(0,0,1)}; + bounded.raw_constraints={row("gecode_circuit",{integer(0),array({ref(0),integer(0)})})}; + const auto full=O::compile_flatzinc(bounded);assert(full.compiled&&full.work>0); + for(std::size_t cap:{std::size_t(0),full.work/2,full.work-1}){ + O::FlatZincCompileOptions options;options.max_work=cap;auto result=O::compile_flatzinc(bounded,options); + assert(result.status==O::FlatZincCompileStatus::ResourceLimit&&!result.compiled&&result.work<=cap); + } + O::FlatZincCompileOptions options;options.max_work=full.work;assert(O::compile_flatzinc(bounded,options).compiled); + for(int mode=0;mode<3;++mode){options={};if(mode==0)options.max_variables=1;if(mode==1)options.max_constraints=0;if(mode==2)options.max_nonzeros=1; + auto result=O::compile_flatzinc(bounded,options);assert(result.status==O::FlatZincCompileStatus::ResourceLimit&&!result.compiled); + } + options={};options.max_variables=2;options.max_constraints=1;options.max_nonzeros=2;assert(O::compile_flatzinc(bounded,options).compiled); + options={};options.time_limit_seconds=0;auto stopped=O::compile_flatzinc(bounded,options);assert(stopped.status==O::FlatZincCompileStatus::TimeLimit&&!stopped.compiled); + options={};options.cancellation=std::make_shared();options.cancellation->cancel();stopped=O::compile_flatzinc(bounded,options);assert(stopped.status==O::FlatZincCompileStatus::Cancelled&&!stopped.compiled); + // An earlier contradiction does not excuse an unsupported later signature. + bounded.raw_variables[0].domain.integers=domain(1,0).set;bounded.raw_constraints[0].arguments[0]=integer(-1);rejected(bounded); +} +void circuit_combined(){ + F::Records r;r.raw_variables={variable(0,3,5),variable(1,3,5),variable(2,3,5),variable(0,0,1,F::Type::Boolean),variable(9,-4,-2)}; + r.raw_variables[1].domain.integers=members({5,3,5}).set; + r.raw_constraints={row("gecode_circuit",{integer(3),array({ref(0),ref(1),ref(2)})}), + row("gecode_table_int",{array({ref(0),ref(1),ref(2)}),array({integer(4),integer(5),integer(3),integer(5),integer(3),integer(4)})}), + row("all_different_int",{array({ref(0),ref(1),ref(2)})}), + row("array_var_int_element",{integer(1),array({ref(0),ref(1)}),ref(0)}), + row("int_le_reif",{ref(0),ref(1),ref(0,F::Type::Boolean)}), + row("int_plus",{ref(9),integer(7),ref(0)})}; + const auto allowed=circuit_points(3,3); + oracle(r,{{3,5},{3,5},{3,5},{0,1},{-4,-2}},[&](const Point& p){return allowed.count({p[0],p[1],p[2]})&&bool(p[3])==(p[0]<=p[1])&&p[4]+7==p[0];}); + for(auto method:{F::Method::Minimize,F::Method::Maximize}){ + r.solve.method=method;r.solve.has_objective=true;r.solve.objective=ref(9);auto c=compiled(r); + for(auto backend:{O::Backend::Native,O::Backend::Auto,O::Backend::Highs}){ + O::SolveOptions options;options.backend=backend;options.guarantee=backend==O::Backend::Highs?O::Guarantee::Numerical:O::Guarantee::Exact; + auto result=O::solve(c.model(),options); + if(backend!=O::Backend::Highs&&O::capabilities(O::Backend::Native).available) + assert(result.termination==O::Termination::Optimal&&result.objective==(method==F::Method::Minimize?-3:-2)&&O::validate_flatzinc(c,result,0).valid); + else assert(result.termination==O::Termination::Unsupported&&!result.has_solution()); + } + } +} + +// The tiny cumulative oracle samples every integer time in a bounded horizon. +// Integer endpoints make this exhaustive for half-open continuous-time usage, +// independently of both production event-sweep implementations. +bool cumulative_point(const Point& starts,const Point& durations,const Point& heights,I bound){ + assert(starts.size()==durations.size()&&starts.size()==heights.size()&&bound>=0); + I first=0,last=0; + for(std::size_t i=0;i=0&&heights[i]>=0);first=std::min(first,starts[i]);last=std::max(last,starts[i]+durations[i]);} + assert(last-first<100); + for(I time=first;timebound)return false; + } + return true; +} +void cumulatives(){ + for(const auto& id:{"gecode_cumulatives","cumulatives"})for(std::size_t size=0;size<=4;++size){ + std::size_t patterns=size==0?1:size==1?9:size==2?81:18; + for(std::size_t pattern=0;pattern starts,duration_values,height_values;Point durations,heights;auto code=pattern; + for(std::size_t i=0;i>(size,{-2,2}),[&](const Point& p){return cumulative_point(p,durations,heights,bound);}); + } + } + // Repeated/aliased starts remain separate demands; literals get fixed slots. + for(int variant=0;variant<4;++variant){F::Records r;r.raw_variables={variable(0,-2,2),variable(1,-2,2)}; + if(variant==1){r.raw_variables[1].alias=true;r.raw_variables[1].target={F::Type::Integer,0};} + r.raw_constraints={row("gecode_cumulatives",{array({ref(0),variant==0?ref(0):variant==2?integer(0):ref(1)}),array({integer(2),integer(1)}),array({integer(2),integer(1)}),integer(2)})}; + oracle(r,{{-2,2},{-2,2}},[&](const Point& p){return(variant!=1||p[0]==p[1])&&cumulative_point({p[0],variant==0?p[0]:variant==2?0:p[1]},{2,1},{2,1},2);}); + } + // Legacy p_cumulatives has a singleton height<=bound shortcut which rejects + // this zero-duration task. The independent half-open oracle and typed/native + // route must accept it; a positive duration must remain infeasible. + for(I duration:{I(0),I(1)})for(const auto& id:{"gecode_cumulatives","cumulatives"}){ + F::Records r;r.raw_variables={variable(0,-2,2)}; + r.raw_constraints={row(id,{array({ref(0)}),array({integer(duration)}),array({integer(2)}),integer(1)})}; + oracle(r,{{-2,2}},[&](const Point& p){return cumulative_point(p,{duration},{2},1);}); + auto c=compiled(r);O::SolveOptions options;options.backend=O::Backend::Native;options.guarantee=O::Guarantee::Exact;auto result=O::solve(c.model(),options); + if(O::capabilities(O::Backend::Native).available){ + assert(result.termination==(duration?O::Termination::Infeasible:O::Termination::Optimal)); + if(!duration)assert(O::validate_flatzinc(c,result,0).valid); + }else assert(result.termination==O::Termination::Unsupported); + } +} +void cumulative_parameters(){ + F::Records r;r.raw_variables={variable(0,-2,2),variable(1,-2,2),variable(10,0,3),variable(11,0,3),variable(12,0,3),variable(13,0,3)}; + r.raw_variables[2].domain.integers=members({2,0,2}).set; + r.raw_variables[3].assigned=true;r.raw_variables[3].value=integer(1); + r.raw_variables[5].alias=true;r.raw_variables[5].target={F::Type::Integer,12}; + r.raw_domains={row("int_in",{ref(10),domain(1,3)}),row("int_in",{ref(13),members({2,2})})}; + r.raw_constraints={row("gecode_cumulatives",{array({ref(0),ref(1)}),array({ref(10),integer(1)}),array({ref(11),ref(11)}),ref(13)})}; + oracle(r,{{-2,2},{-2,2},{0,3},{0,3},{0,3},{0,3}},[](const Point& p){return p[2]==2&&p[3]==1&&p[4]==2&&p[5]==2&&cumulative_point({p[0],p[1]},{2,1},{1,1},2);}); + auto c=compiled(r);const auto& payload=std::get(c.model().globals.back().payload); + assert(payload.durations==Point({2,1})&&payload.heights==Point({1,1})&&payload.capacity==2); + assert(c.variables().size()==6&&c.model().variables.size()==5); // alias only, no hidden parameter slots + r.output={{"duration",ref(10)},{"height",ref(11)},{"capacity",ref(13)}};auto output=compiled(r);auto historical=witness(output,{-1,0,2,1,2,2}); + r.raw_variables.clear();r.raw_constraints.clear();r.raw_domains.clear();r.output.clear(); + assert(O::validate_flatzinc(output,historical,0).valid&&O::format_flatzinc_solution(output,historical,0)=="duration = 2;\nheight = 1;\ncapacity = 2;\n----------\n"); + historical.values[output.variables()[2].variable.id]=0;assert(!O::validate_flatzinc(output,historical,0).valid); + // No inference from an ordinary equality row, a lower bound, or an empty + // original domain which happened to produce the compiler's dummy zero slot. + for(int parameter=0;parameter<3;++parameter){F::Records source;source.raw_variables={variable(0,-1,1),variable(1,0,2)}; + source.raw_constraints={row("gecode_cumulatives",{array({ref(0)}),array({parameter==0?ref(1):integer(1)}),array({parameter==1?ref(1):integer(1)}),parameter==2?ref(1):integer(1)}),row("int_eq",{ref(1),integer(1)})}; + rejected(source);source.raw_variables[1].domain.integers=members({}).set;rejected(source); + } +} +void cumulative_boundaries(){ + F::Records r;r.raw_variables={variable(0,-1,1),variable(1,0,1,F::Type::Boolean)}; + const auto valid=std::vector{array({ref(0)}),array({integer(1)}),array({integer(1)}),integer(1)}; + const auto reject=[&](std::vector args,O::FlatZincCompileStatus status=O::FlatZincCompileStatus::InvalidInput){r.raw_constraints={row("gecode_cumulatives",std::move(args))};rejected(r,status);}; + reject({});reject({array({}),array({}),array({})}); + auto changed=valid;changed.push_back(boolean(true));reject(changed); + changed.push_back(integer(0));reject(changed,O::FlatZincCompileStatus::Unsupported); + changed.push_back(integer(0));reject(changed,O::FlatZincCompileStatus::Unsupported); + changed.push_back(integer(0));reject(changed); + for(int index=0;index<4;++index){changed=valid;changed[index]=index==3?boolean(true):integer(0);reject(changed);} + for(int index=0;index<3;++index){changed=valid;changed[index]=array({});reject(changed);} + for(int index=0;index<3;++index){changed=valid;changed[index]=array({boolean(true)});reject(changed); + changed[index]=array({ref(1,F::Type::Boolean)});reject(changed);changed[index]=array({ref(9)});reject(changed);} + for(int index:{1,2}){changed=valid;changed[index]=array({integer(-1)});reject(changed);} + changed=valid;changed[3]=integer(-1);reject(changed,O::FlatZincCompileStatus::Unsupported); + constexpr I large=9007199254740992LL; + for(int index=0;index<4;++index){changed=valid;changed[index]=index==3?integer(large+1):array({integer(large+1)});reject(changed,O::FlatZincCompileStatus::Unsupported);} + for(const auto& id:{"fzn_cumulative","fzn_cumulatives","gecode_cumulative","gecode_gecode_cumulatives","gecode_schedule_cumulative_optional"}){r.raw_constraints={row(id,valid)};rejected(r);} + // Both selected names reject multi-machine six-/seven-argument forms rather + // than truncating machine/bound/polarity data to a single resource. + for(const auto& id:{"gecode_cumulatives","cumulatives"})for(int size:{6,7}){r.raw_constraints={row(id,std::vector(size,integer(0)))};rejected(r);} + F::Records wide;wide.raw_constraints={row("gecode_cumulatives",{array({integer(large)}),array({integer(large)}),array({integer(1)}),integer(1)})}; + auto c=compiled(wide);assert(O::validate_flatzinc(c,witness(c,{}),0).valid); + O::SolveOptions native;native.backend=O::Backend::Native;native.guarantee=O::Guarantee::Exact; + assert(O::solve(c.model(),native).termination==O::Termination::Unsupported); + // The existing native energy/activity envelope remains a separate gate. + F::Records energy;energy.raw_variables={variable(0,0,1073741823),variable(1,0,1073741823)}; + energy.raw_constraints={row("gecode_cumulatives",{array({ref(0),ref(1)}),array({integer(1),integer(1)}),array({integer(1073741823),integer(1073741823)}),integer(1073741823)})}; + auto guarded=compiled(energy);assert(O::validate_flatzinc(guarded,witness(guarded,{0,1}),0).valid); + assert(O::solve(guarded.model(),native).termination==O::Termination::Unsupported); + F::Records bounded;bounded.raw_variables={variable(0,-1,1)}; + bounded.raw_constraints={row("gecode_cumulatives",{array({ref(0),integer(0)}),array({integer(1),integer(2)}),array({integer(1),integer(1)}),integer(2)})}; + auto full=O::compile_flatzinc(bounded);assert(full.compiled&&full.work>0); + for(std::size_t cap:{std::size_t(0),full.work/2,full.work-1}){O::FlatZincCompileOptions options;options.max_work=cap;auto result=O::compile_flatzinc(bounded,options);assert(result.status==O::FlatZincCompileStatus::ResourceLimit&&!result.compiled&&result.work<=cap);} + O::FlatZincCompileOptions options;options.max_work=full.work;assert(O::compile_flatzinc(bounded,options).compiled); + for(int mode=0;mode<3;++mode){options={};if(mode==0)options.max_variables=1;if(mode==1)options.max_constraints=0;if(mode==2)options.max_nonzeros=6;auto result=O::compile_flatzinc(bounded,options);assert(result.status==O::FlatZincCompileStatus::ResourceLimit&&!result.compiled);} + options={};options.max_variables=2;options.max_constraints=1;options.max_nonzeros=7;assert(O::compile_flatzinc(bounded,options).compiled); + options={};options.time_limit_seconds=0;auto stopped=O::compile_flatzinc(bounded,options);assert(stopped.status==O::FlatZincCompileStatus::TimeLimit&&!stopped.compiled); + options={};options.cancellation=std::make_shared();options.cancellation->cancel();stopped=O::compile_flatzinc(bounded,options);assert(stopped.status==O::FlatZincCompileStatus::Cancelled&&!stopped.compiled); + F::Records empty;empty.raw_constraints={row("cumulatives",{array({}),array({}),array({}),integer(0)})};options={};options.max_nonzeros=0; + assert(O::compile_flatzinc(empty,options).status==O::FlatZincCompileStatus::ResourceLimit);options.max_nonzeros=1;assert(O::compile_flatzinc(empty,options).compiled); +} +void cumulative_combined(){ + F::Records r;r.raw_variables={variable(0,-2,2),variable(1,-2,2),variable(0,0,1,F::Type::Boolean)}; + r.raw_variables[0].domain.integers=members({-2,0,2}).set; + r.raw_constraints={row("gecode_cumulatives",{array({ref(0),ref(1)}),array({integer(2),integer(1)}),array({integer(2),integer(1)}),integer(2)}), + row("all_different_int",{array({ref(0),ref(1)})}),row("array_var_int_element",{integer(2),array({ref(0),ref(1)}),ref(1)}), + row("gecode_table_int",{array({ref(1)}),array({integer(-2),integer(0),integer(2)})}),row("int_le_reif",{ref(0),ref(1),ref(0,F::Type::Boolean)})}; + oracle(r,{{-2,2},{-2,2},{0,1}},[](const Point& p){return p[0]%2==0&&p[1]%2==0&&p[0]!=p[1]&&bool(p[2])==(p[0]<=p[1])&&cumulative_point({p[0],p[1]},{2,1},{2,1},2);}); + for(auto method:{F::Method::Minimize,F::Method::Maximize}){r.solve.method=method;r.solve.has_objective=true;r.solve.objective=ref(0);auto c=compiled(r); + for(auto backend:{O::Backend::Native,O::Backend::Auto,O::Backend::Highs}){O::SolveOptions options;options.backend=backend;options.guarantee=backend==O::Backend::Highs?O::Guarantee::Numerical:O::Guarantee::Exact;auto result=O::solve(c.model(),options); + if(backend!=O::Backend::Highs&&O::capabilities(O::Backend::Native).available)assert(result.termination==O::Termination::Optimal&&result.objective==(method==F::Method::Minimize?-2:2)&&O::validate_flatzinc(c,result,0).valid); + else assert(result.termination==O::Termination::Unsupported&&!result.has_solution()); + } + } +} + +// Generate accepted words by enumerating source-labeled paths, independently +// of the compiler's sparse zero-based graph and the source checker's indexing. +std::set regular_words(I states,I symbols,const Point& transitions,I initial, + const std::set& finals,std::size_t length){ + assert(transitions.size()==static_cast(states*symbols));std::set accepted;Point word; + const auto paths=[&](const auto& self,I state)->void { + if(word.size()==length){if(finals.count(state))accepted.insert(word);return;} + for(I symbol=1;symbol<=symbols;++symbol){I next=0;std::size_t cell=0; + for(I from=1;from<=states;++from)for(I label=1;label<=symbols;++label,++cell) + if(from==state&&label==symbol)next=transitions[cell]; + if(next){word.push_back(symbol);self(self,next);word.pop_back();} + } + };paths(paths,initial);return accepted; +} +void regulars(){ + for(unsigned pattern=0;pattern<81;++pattern)for(I initial=1;initial<=2;++initial)for(unsigned final=0;final<4;++final)for(std::size_t size=0;size<=3;++size){ + F::Records r;std::vector word,flat;Point edges,final_values;std::set finals;auto code=pattern; + for(unsigned i=0;i<4;++i){edges.push_back(code%3);flat.push_back(integer(code%3));code/=3;} + for(I state=1;state<=2;++state)if(final&(1u<<(state-1))){finals.insert(state);final_values.push_back(state);} + for(std::size_t i=0;i>(size,{0,3}),[&](const Point& p){return accepted.count(p)!=0;}); + if(pattern%19==0&&initial==2&&final==2){auto c=compiled(r);O::SolveOptions options;options.backend=O::Backend::Native;options.guarantee=O::Guarantee::Exact;auto result=O::solve(c.model(),options); + if(O::capabilities(O::Backend::Native).available){assert(result.termination==(accepted.empty()?O::Termination::Infeasible:O::Termination::Optimal));if(!accepted.empty())assert(O::validate_flatzinc(c,result,0).valid);} + else assert(result.termination==O::Termination::Unsupported); + } + } + // Every source position survives aliasing and fixed-literal lowering. + for(int mode=0;mode<4;++mode){F::Records r;r.raw_variables={variable(0,0,3),variable(1,0,3)}; + if(mode==1){r.raw_variables[1].alias=true;r.raw_variables[1].target={F::Type::Integer,0};} + r.raw_constraints={row("gecode_regular",{array({ref(0),mode==0?ref(0):mode==2?integer(2):ref(1)}),integer(2),integer(2),array({integer(1),integer(2),integer(2),integer(0)}),integer(1),members({2,2})})}; + const auto accepted=regular_words(2,2,{1,2,2,0},1,{2},2); + oracle(r,{{0,3},{0,3}},[&](const Point& p){return(mode!=1||p[0]==p[1])&&accepted.count({p[0],mode==0?p[0]:mode==2?2:p[1]});}); + } + for(const auto& finals:{members({}),members({2,1,2}),domain(1,2),domain(3,2)})for(std::size_t size:{0u,1u}){ + F::Records r;std::vector word;if(size){r.raw_variables={variable(0,0,3)};word={ref(0)};} + r.raw_constraints={row("gecode_regular",{array(word),integer(2),integer(2),array({integer(0),integer(0),integer(0),integer(0)}),integer(1),finals})}; + const bool initial_final=finals.set.interval?(finals.set.lower<=1&&1<=finals.set.upper):std::find(finals.set.values.begin(),finals.set.values.end(),1)!=finals.set.values.end(); + oracle(r,std::vector>(size,{0,3}),[&](const Point&){return!size&&initial_final;}); + } +} +void regular_boundaries(){ + F::Records r;r.raw_variables={variable(0,1,2),variable(1,1,1),variable(0,0,1,F::Type::Boolean)}; + const std::vector valid={array({ref(0)}),integer(2),integer(2),array({integer(1),integer(2),integer(2),integer(1)}),integer(1),members({2})}; + const auto reject=[&](std::vector args,O::FlatZincCompileStatus status=O::FlatZincCompileStatus::InvalidInput){r.raw_constraints={row("gecode_regular",std::move(args))};rejected(r,status);}; + for(std::size_t n=0;n<=8;++n)if(n!=6)reject(std::vector(n,integer(0))); + for(std::size_t i=0;i<6;++i){auto changed=valid;changed[i]=boolean(true);reject(changed);} + for(std::size_t i:{1u,2u,4u}){auto changed=valid;changed[i]=ref(1);reject(changed);} + for(std::size_t i:{1u,2u})for(I value:{I(-1),I(0)}){auto changed=valid;changed[i]=integer(value);reject(changed);} + for(I initial:{I(0),I(3)}){auto changed=valid;changed[4]=integer(initial);reject(changed);} + for(std::size_t n:{0u,3u,5u}){auto changed=valid;changed[3]=array(std::vector(n,integer(0)));reject(changed);} + for(const auto& cell:{integer(-1),integer(3),boolean(true),ref(1),ref(0,F::Type::Boolean)}){auto changed=valid;changed[3].elements[0]=cell;reject(changed);} + for(const auto& final:{members({0}),members({3}),domain(0,1),domain(1,3),ref(1)}){auto changed=valid;changed[5]=final;reject(changed);} + for(const auto& term:{boolean(true),ref(0,F::Type::Boolean),ref(9),array({integer(1)})}){auto changed=valid;changed[0]=array({term});reject(changed);} + constexpr I huge=9007199254740992LL; + for(std::size_t i:{1u,2u,4u}){auto changed=valid;changed[i]=integer(huge+1);reject(changed,O::FlatZincCompileStatus::Unsupported);} + auto changed=valid;changed[1]=integer(huge);changed[2]=integer(huge);reject(changed,O::FlatZincCompileStatus::Unsupported); + changed=valid;changed[3].elements[0]=integer(huge+1);reject(changed,O::FlatZincCompileStatus::Unsupported); + changed=valid;changed[5]=members({huge+1});reject(changed,O::FlatZincCompileStatus::Unsupported); + for(const auto& id:{"regular","fzn_regular","gecode_regular_set","gecode_regular_reif","gecode_gecode_regular","gecode_regular_nfa"}){r.raw_constraints={row(id,valid)};rejected(r);} + // Even an empty word or an earlier contradiction cannot bypass admission. + changed=valid;changed[0]=array({});changed[3].elements[0]=integer(-1);reject(changed); + r.raw_variables[0].domain.integers=domain(1,0).set;reject(changed);r.raw_variables[0].domain.integers=domain(1,2).set; + // Exact payload accounting: three scalar parameters, word positions, input + // matrix cells, three cells per nonzero transition and expanded final IDs. + F::Records bounded;bounded.raw_variables={variable(0,1,2)}; + bounded.raw_constraints={row("gecode_regular",{array({ref(0),integer(1)}),integer(2),integer(2),array({integer(1),integer(2),integer(0),integer(0)}),integer(1),domain(1,2)})}; + auto full=O::compile_flatzinc(bounded);assert(full.compiled&&full.work>0); + for(auto cap:{std::size_t(0),full.work/2,full.work-1}){O::FlatZincCompileOptions options;options.max_work=cap;auto result=O::compile_flatzinc(bounded,options);assert(result.status==O::FlatZincCompileStatus::ResourceLimit&&!result.compiled&&result.work<=cap);} + O::FlatZincCompileOptions options;options.max_work=full.work;assert(O::compile_flatzinc(bounded,options).compiled); + for(int mode=0;mode<3;++mode){options={};if(mode==0)options.max_variables=1;if(mode==1)options.max_constraints=0;if(mode==2)options.max_nonzeros=16;auto result=O::compile_flatzinc(bounded,options);assert(result.status==O::FlatZincCompileStatus::ResourceLimit&&!result.compiled);} + options={};options.max_variables=2;options.max_constraints=1;options.max_nonzeros=17;assert(O::compile_flatzinc(bounded,options).compiled); + options={};options.time_limit_seconds=0;auto stopped=O::compile_flatzinc(bounded,options);assert(stopped.status==O::FlatZincCompileStatus::TimeLimit&&!stopped.compiled); + options={};options.cancellation=std::make_shared();options.cancellation->cancel();stopped=O::compile_flatzinc(bounded,options);assert(stopped.status==O::FlatZincCompileStatus::Cancelled&&!stopped.compiled); + bounded.raw_constraints[0].arguments[5]=domain(1,2);options={};options.max_nonzeros=1;assert(O::compile_flatzinc(bounded,options).status==O::FlatZincCompileStatus::ResourceLimit); +} +void regular_combined(){ + F::Records r;r.raw_variables={variable(0,0,3),variable(1,0,3),variable(0,0,1,F::Type::Boolean),variable(9,-3,-2)}; + r.raw_variables[0].domain.integers=members({2,1,2}).set; + r.raw_constraints={row("gecode_regular",{array({ref(0),ref(1)}),integer(3),integer(2),array({integer(2),integer(3),integer(0),integer(3),integer(3),integer(0)}),integer(1),members({3})}), + row("gecode_table_int",{array({ref(1)}),array({integer(1),integer(2)})}),row("all_different_int",{array({ref(0),ref(1)})}), + row("int_le_reif",{ref(0),ref(1),ref(0,F::Type::Boolean)}),row("int_plus",{ref(9),integer(4),ref(0)})}; + oracle(r,{{0,3},{0,3},{0,1},{-3,-2}},[](const Point& p){return((p[0]==1&&p[1]==2)||(p[0]==2&&p[1]==1))&&bool(p[2])==(p[0]<=p[1])&&p[3]+4==p[0];}); + for(auto method:{F::Method::Minimize,F::Method::Maximize}){ + r.solve.method=method;r.solve.has_objective=true;r.solve.objective=ref(9);r.output={{"first",ref(0)},{"objective",ref(9)}};auto c=compiled(r); + for(auto backend:{O::Backend::Native,O::Backend::Auto,O::Backend::Highs}){O::SolveOptions options;options.backend=backend;options.guarantee=backend==O::Backend::Highs?O::Guarantee::Numerical:O::Guarantee::Exact;auto result=O::solve(c.model(),options); + if(backend!=O::Backend::Highs&&O::capabilities(O::Backend::Native).available){assert(result.termination==O::Termination::Optimal&&result.objective==(method==F::Method::Minimize?-3:-2)&&O::validate_flatzinc(c,result,0).valid);assert(!O::format_flatzinc_solution(c,result,0).empty());} + else assert(result.termination==O::Termination::Unsupported&&!result.has_solution()); + } + } + r.solve={};auto c=compiled(r);auto historical=witness(c,{1,2,1,-3});r.raw_variables.clear();r.raw_constraints.clear();assert(O::validate_flatzinc(c,historical,0).valid); + historical.values[c.variables()[1].variable.id]=0;assert(!O::validate_flatzinc(c,historical,0).valid); +} + +int main(){regulars();regular_boundaries();regular_combined();cumulatives();cumulative_parameters();cumulative_boundaries();cumulative_combined();circuits();circuit_boundaries();circuit_combined();holey_domains();tables();table_domain_limits();table_domain_backends();comparisons();booleans();aliases_domains();guarded();globals();failures();history_output();actual_backends();assert(configurations>500&&assignments>20000);std::cout<<"FlatZinc source compiler: "< +#include + +namespace FznOptimizeDriver { +int fake=0,calls=0; +SolveOutput test_solve(const O::ModelSnapshot& m,const Options& controls,const O::SolveOptions& options) { + ++calls;if(!fake)return dispatch(m,controls,options); + O::SolveResult r;r.model_id=m.model_id;r.revision=m.revision;r.guarantee=options.guarantee; + r.termination=O::Termination::Optimal;r.values.resize(m.variables.size(),0); + r.active_variables.resize(m.variables.size(),true);r.solution_validated=true;r.objective=0; + r.best_bound=0;r.absolute_gap=0;r.relative_gap=0; + const auto clear=[&] {r.values.clear();r.active_variables.clear();r.objective.reset();r.best_bound.reset();r.absolute_gap.reset();r.relative_gap.reset();r.solution_validated=false;}; + switch(fake) { + case 2:r.termination=O::Termination::NodeLimit;break; + case 3:r.termination=O::Termination::NodeLimit;clear();break; + case 4:r.values[0]=3;break; + case 5:++r.model_id;break; + case 6:r.active_variables[0]=false;break; + case 7:clear();break; + case 8:r.termination=O::Termination::Infeasible;break; + case 9:r.termination=O::Termination::Infeasible;clear();break; + case 10:r.guarantee=O::Guarantee::Certified;break; + case 11:r.termination=O::Termination::BackendError;break; + case 12:r.termination=O::Termination::Unsupported;break; + case 13:r.objective=std::numeric_limits::quiet_NaN();break; + case 14:r.termination=O::Termination::Unbounded;clear();break; + case 15:r.termination=O::Termination::Infeasible;r.solution_validated=false;r.objective.reset();r.best_bound.reset();r.absolute_gap.reset();r.relative_gap.reset();break; + case 16:r.best_bound=1;break; + case 17:r.best_bound=std::numeric_limits::quiet_NaN();break; + case 18:r.best_bound.reset();r.absolute_gap.reset();r.relative_gap.reset();break; + case 19:r.best_bound=-1;r.absolute_gap=1;r.relative_gap=1;break; + case 20:r.absolute_gap=std::numeric_limits::quiet_NaN();break; + case 21:r.relative_gap=1;break; + case 22:r.termination=O::Termination::Infeasible;clear();r.objective=std::numeric_limits::quiet_NaN();break; + case 23:r.backend="native\r\n=====UNSATISFIABLE=====";r.message="policy\n==========\tmessage";break; + default:break; + } + return {r,fake==23?"counter\n=====UNKNOWN=====":""}; +} +} +using namespace FznOptimizeDriver; +namespace { +int checks=0; +const std::string basic="var 0..2: x :: output_var; solve minimize x;"; +std::pair run(const std::string& source,Options options={}) { + std::istringstream input(source);std::ostringstream out,err; + const int code=execute(options,input,out,err);++checks; + if(code==2) {assert(out.str().empty());assert(!err.str().empty());} + return {code,out.str()}; +} +void rejected_arguments() { + for(const auto& args:std::vector>{ + {},{"-a"},{"m","-a","1"},{"m","--backend","auto"},{"m","--backend"}, + {"m","--node-limit","-1"},{"m","--node-limit","1.5"}, + {"m","--node-limit","18446744073709551616"},{"m","--time-limit","nan"}, + {"m","--time-limit","-1"},{"m","--time-limit"," 2"}, + {"m","--time-limit","2x"},{"m","--max-input-bytes","2147483648"}, + {"m","--backend","native","--backend","highs"}}) { + bool threw=false;try{arguments(args);}catch(const std::invalid_argument&){threw=true;}assert(threw);++checks; + } + auto valid=arguments({"-","--backend","highs","--time-limit","1e2","--node-limit","18446744073709551615"}); + assert(valid.solve.time_limit_seconds==100&&valid.solve.node_limit==std::numeric_limits::max());++checks; +} +void native_arguments() { + const std::vector> invalid={ + {"--native-mode","unknown"},{"--native-mode"}, + {"--native-diagnostics","true"},{"--native-unknown","on"}, + {"--native-mode","auto","--native-mode","auto"}, + {"--native-node-limit","1","--node-limit","2"}, + {"--node-limit","1","--native-node-limit","2"}, + {"--backend","highs","--native-mode","auto"}, + {"--backend","highs","--native-diagnostics","off"}, + {"--native-mode","plain","--native-auto-knapsack","off"}, + {"--native-auto-presolve","true"},{"--native-race-seconds","2"}, + {"--native-mode","race","--native-race-seconds","nan"}, + {"--native-mode","race","--native-race-seconds","-1"}, + {"--native-mode","race","--native-race-nodes","0"}, + {"--native-mode","race","--native-race-nodes","18446744073709551616"}, + {"--native-lp","root"},{"--native-mode","configured","--native-lp","bad"}, + {"--native-mode","configured","--native-root-cuts","on"}, + {"--native-mode","configured","--native-bound-tightening","off"}, + {"--native-mode","configured","--native-lp","root","--native-lp-interval","2"}, + {"--native-mode","configured","--native-lp","updated","--native-lp-interval","0"}, + {"--native-mode","configured","--native-lp","updated","--native-lp-interval","4294967296"}, + {"--native-mode","configured","--native-search","bab","--native-branching","default"}, + {"--native-mode","configured","--native-search","bab","--native-max-open-nodes","1"}, + {"--native-mode","configured","--native-search","bab","--native-neighborhood","off"}, + {"--native-mode","configured","--native-branching-probes","1"}, + {"--native-mode","configured","--native-neighborhood-radius","1"}, + {"--native-mode","configured","--native-neighborhood-nodes","1"}, + {"--native-mode","configured","--native-neighborhood-seconds","1"}, + {"--native-mode","configured","--native-neighborhood","hamming","--native-neighborhood-seconds","inf"}, + {"--native-mode","configured","--native-max-open-nodes","18446744073709551616"} + }; + for(auto args:invalid) { + args.insert(args.begin(),"m");bool threw=false; + try{arguments(args);}catch(const std::invalid_argument&){threw=true;}assert(threw);++checks; + // Native-prefixed controls use identical validation in the MiniZinc protocol. + args.insert(args.begin(),"--minizinc");threw=false; + try{arguments(args);}catch(const std::invalid_argument&){threw=true;}assert(threw);++checks; + } + auto automatic=arguments({"--minizinc","--native-auto-presolve","off","-t","1000","m", + "--native-auto-components","off","--native-auto-symmetry","off","--native-auto-knapsack","off"}); + assert(!automatic.automatic.presolve&&!automatic.automatic.components&&!automatic.automatic.symmetry&&!automatic.automatic.knapsack); + assert(automatic.solve.time_limit_seconds==1&&automatic.filename=="m");++checks; + auto race=arguments({"m","--native-mode","race","--native-race-seconds","0","--native-race-nodes","1"}); + assert(race.race.exploration_seconds==0&&race.race.probe_node_limit==1);++checks; + auto configured=arguments({"--minizinc","m","--native-mode","configured","--native-search","best-bound", + "--native-lp","updated","--native-lp-interval","7","--native-root-cuts","on","--native-bound-tightening","off", + "--native-branching","reliability","--native-branching-probes","19","--native-max-open-nodes","50", + "--native-neighborhood","hamming","--native-neighborhood-radius","2","--native-neighborhood-nodes","17", + "--native-neighborhood-seconds","0.25","--native-node-limit","99","--native-diagnostics","on"}); + const auto search=search_options(configured,configured.solve); + assert(search.order==O::NativeSearchOrder::BestBound&&search.relaxation&&search.relaxation->root_cover_cuts); + assert(!search.relaxation->bound_tightening&&search.relaxation->bound_change_interval==7); + assert(search.relaxation->frequency==O::NativeLpFrequency::AfterBoundChanges&&search.branching&& + search.branching->max_probe_status_calls==19&&search.max_open_nodes==50); + assert(configured.neighborhood_settings.radius==2&&configured.neighborhood_settings.max_status_calls==17&& + configured.neighborhood_settings.time_limit_seconds==0.25&&configured.solve.node_limit==99&&configured.diagnostics);++checks; +} +void fake_contract() { + for(fake=1;fake<=22;++fake) { + auto result=run(basic); + if(fake==1)assert(result.first==0&&result.second.find("x = 0;\n----------\n==========\n")!=std::string::npos); + else if(fake==2)assert(result.first==1&&result.second.find("x = 0;")!=std::string::npos&&result.second.find("==========")==std::string::npos); + else if(fake==3)assert(result.first==1&&result.second.find("=====UNKNOWN=====")!=std::string::npos); + else if(fake==9)assert(result.first==0&&result.second.find("=====UNSATISFIABLE=====")!=std::string::npos); + else assert(result.first==2); + } + fake=1;auto sat=run("var 0..2: x :: output_var; solve satisfy;"); + assert(sat.first==0&&sat.second.find("x = 0;")!=std::string::npos&&sat.second.find("==========")==std::string::npos); + calls=0;Options timed;timed.solve.time_limit_seconds=0; + assert(run("not parsed",timed).first==1&&calls==0); + Options small;small.capture.max_input_bytes=1; + assert(run(basic,small).first==2&&calls==0); + for(const auto& source:{ + "var 0..2: x; constraint mystery(x); solve satisfy;", + "var 0..2: x; constraint int_times(x,x,x); solve satisfy;", + "var int: x; solve minimize x;", + "var 0.0..1.0: x; solve minimize x;", + "var 0..2: x; solve :: int_search([x],input_order,indomain_min,complete) satisfy;", + "var 0..2: x; constraint int_eq(x); solve satisfy;", + "var 0..2: x; solve minimize missing;"})assert(run(source).first==2&&calls==0); + // Finite holes are admitted now. A legal candidate reaches formatting, but + // a forged value inside the hull and outside the set remains invalid. + calls=0; + auto holes=run("var {0,2}: x :: output_var; solve minimize x;"); + assert(holes.first==0&&calls==1&&holes.second.find("x = 0;")!=std::string::npos); + calls=0; + assert(run("var {-1,1}: x :: output_var; solve minimize x;").first==2&&calls==1); + fake=23;Options diagnostic;diagnostic.diagnostics=true; + auto escaped=run(basic,diagnostic);assert(escaped.first==0); + assert(escaped.second.find("\n=====UNSATISFIABLE=====")==std::string::npos&& + escaped.second.find("\n=====UNKNOWN=====")==std::string::npos&& + escaped.second.find("% native-backend: native =====UNSATISFIABLE=====\n")!=std::string::npos&& + escaped.second.find("% native-policy: policy ========== message\n")!=std::string::npos); + fake=0; +} +void actual_native_controls() { + for(const auto& mode:{"auto","plain","race","configured"}) { + auto o=arguments({"m","--native-mode",mode,"--native-diagnostics","on"}); + auto solved=run(basic,o); + if(!O::native_capabilities().available) {assert(solved.first==2);continue;} + assert(solved.first==0&&solved.second.find(std::string("% native-mode: ")+mode+"\n")!=std::string::npos&& + solved.second.find("x = 0;\n----------\n==========\n")!=std::string::npos); + if(std::string(mode)=="configured")assert(solved.second.find("native frontier")!=std::string::npos); + } + for(const auto& order:{"bab","dfs","best-bound"})for(const auto& lp:{"off","root","updated"}) { + std::vector args={"m","--native-mode","configured","--native-search",order,"--native-lp",lp,"--native-diagnostics","on"}; + if(std::string(lp)!="off")args.insert(args.end(),{"--native-root-cuts","on"}); + if(std::string(order)!="bab")args.insert(args.end(),{"--native-branching","reliability","--native-neighborhood","hamming"}); + auto solved=run(basic,arguments(args)); + if(!O::native_capabilities().available||(std::string(lp)!="off"&&!O::native_lp_capabilities().available)) { + assert(solved.first==2);continue; + } + assert(solved.first==0&&solved.second.find("x = 0;")!=std::string::npos); + if(std::string(lp)!="off")assert(solved.second.find("checked LP")!=std::string::npos&&solved.second.find("lp-calls=")!=std::string::npos); + if(std::string(order)!="bab")assert(solved.second.find("branching-probes=")!=std::string::npos&&solved.second.find("neighborhood-attempts=")!=std::string::npos); + } + auto zero=arguments({"--minizinc","m","--native-mode","race","--native-node-limit","0","--native-diagnostics","on"}); + auto stopped=run(basic,zero); + if(O::native_capabilities().available)assert(stopped.first==0&&stopped.second.find("=====UNKNOWN=====")!=std::string::npos&&stopped.second.find("x =")==std::string::npos); +} +void actual_pipeline() { + int available=0; + for(auto backend:{O::Backend::Native,O::Backend::Highs}) { + Options o;o.solve.backend=backend;o.solve.guarantee=backend==O::Backend::Native?O::Guarantee::Exact:O::Guarantee::Numerical; + if(!O::capabilities(backend).available) {assert(run(basic,o).first==2);continue;} + ++available; + auto simple=run(basic,o);assert(simple.first==0&&simple.second.find("x = 0;")!=std::string::npos); + auto maximum=run("var -2..3: x :: output_var; solve maximize x;",o); + assert(maximum.first==0&&maximum.second.find("x = 3;")!=std::string::npos); + auto aliases=run("var 0..3: x; var 1..2: y :: output_var = x; array [1..4] of var 0..3: a :: output_array([-1..0,2..3]) = [x,x,2,3]; solve minimize y;",o); + assert(aliases.first==0&&aliases.second.find("a = array2d(-1..0, 2..3, [1, 1, 2, 3]);")!=std::string::npos&&aliases.second.find("y = 1;")!=std::string::npos); + auto channel=run("var 0..1: x :: output_var; var bool: b :: output_var; constraint bool2int(b,x); constraint bool_eq(b,true); solve minimize x;",o); + assert(channel.first==0&&channel.second.find("b = true;")!=std::string::npos&&channel.second.find("x = 1;")!=std::string::npos); + auto unsat=run("var 0..1: x; constraint int_ge(x,2); solve satisfy;",o); + assert(unsat.first==0&&unsat.second.find("=====UNSATISFIABLE=====")!=std::string::npos); + auto hidden=run("var 0..2: hidden; var 0..2: x :: output_var; constraint int_plus(hidden,x,3); constraint int_eq(hidden,2); solve minimize x;",o); + assert(hidden.first==0&&hidden.second.find("x = 1;")!=std::string::npos); + for(const auto& source:{"int: p = 7; solve minimize p;","solve maximize -3;"}) { + auto captured=F::parse_string(source);assert(captured.records); + auto compiled=O::compile_flatzinc(*captured.records);assert(compiled.compiled); + auto result=O::solve(compiled.compiled->model(),o.solve); + assert(result.termination==O::Termination::Optimal&&result.objective==(std::string(source).find('7')!=std::string::npos?7:-3)); + assert(run(source,o).first==0); + } + } +#ifdef GECODE_FLATZINC_DRIVER_EXPECT_BACKEND + assert(available>=1); +#else + assert(available==0); +#endif +} +} +int main() {rejected_arguments();native_arguments();fake_contract();actual_pipeline();actual_native_controls();assert(checks>=120);std::cout<<"FlatZinc complete frontend: "< None: + if not condition: + raise Failure(message) + + +def scalar(text: str) -> int | str: + text = text.strip() + if text in ("true", "false"): + return text # Keep Boolean true distinct from integer 1. + require(re.fullmatch(r"-?\d+", text) is not None, f"Noninteger source output: {text!r}") + return int(text) + + +def parse_output(text: str) -> Output: + assignments: dict[str, int | str | Array] = {} + markers: list[str] = [] + comments: list[str] = [] + for line in text.splitlines(): + if not line: + continue + if line.startswith("%"): + comments.append(line) + continue + if line in ("----------", "==========", "=====UNKNOWN=====", "=====UNSATISFIABLE====="): + markers.append(line) + continue + require(not markers, f"Assignment after a status/solution marker: {line!r}") + match = re.fullmatch(r"([A-Za-z_][A-Za-z0-9_]*) = (.*);", line) + require(match is not None, f"Unexpected stdout content: {line!r}") + name, raw = match.groups() + require(name not in assignments, f"Duplicate source output: {name}") + array = re.fullmatch(r"array(\d+)d\((.*), \[(.*)\]\)", raw) + if array: + arity, dimensions, contents = array.groups() + dims = [] + for dimension in dimensions.split(","): + bounds = re.fullmatch(r"\s*(-?\d+)\.\.(-?\d+)\s*", dimension) + require(bounds is not None, f"Unexpected array dimension: {dimension!r}") + dims.append(tuple(map(int, bounds.groups()))) + require(len(dims) == int(arity), "arrayNd dimension count mismatch") + values = tuple(scalar(item) for item in contents.split(",")) if contents else () + require(math.prod(max(0, hi - lo + 1) for lo, hi in dims) == len(values), + "Array output cardinality mismatch") + assignments[name] = Array(tuple(dims), values) + else: + assignments[name] = scalar(raw) + return Output(assignments, markers, comments) + + +class Suite: + def __init__(self, binary: Path, fixtures: Path, timeout: float) -> None: + self.binary, self.fixtures = binary, fixtures + self.deadline = time.monotonic() + timeout + self.results: list[dict[str, object]] = [] + self.sources: dict[str, str] = {} + + def fixture(self, name: str) -> Path: + path = self.fixtures / name + require(path.is_file(), f"Required fixture is missing: {path}") + self.sources[name] = hashlib.sha256(path.read_bytes()).hexdigest() + return path + + def invoke(self, name: str, args: list[str], data: bytes | None = None) -> tuple[int, str, str]: + remaining = self.deadline - time.monotonic() + require(remaining > 0, f"Aggregate CLI test deadline before required case {name}") + started = time.monotonic() + command = [str(self.binary), *args] + process = subprocess.Popen(command, stdin=subprocess.PIPE if data is not None else subprocess.DEVNULL, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + start_new_session=os.name == "posix") + try: + stdout, stderr = process.communicate(input=data, timeout=min(8.0, remaining)) + except subprocess.TimeoutExpired as error: + if os.name == "posix": + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + else: + process.kill() # The trusted driver creates solver threads, not child programs. + try: + process.communicate(timeout=1.0) + except subprocess.TimeoutExpired: + process.kill() + raise Failure(f"CLI child deadline: {name}, command={command!r}") from error + elapsed = time.monotonic() - started + require(len(stdout) <= 262144 and len(stderr) <= 262144, f"Excessive CLI output: {name}") + out, err = stdout.decode("utf-8", "strict"), stderr.decode("utf-8", "strict") + self.results.append({"case": name, "returncode": process.returncode, "elapsed_seconds": elapsed}) + require(time.monotonic() <= self.deadline, f"Aggregate CLI test deadline after {name}") + return process.returncode, out, err + + def solve(self, name: str, fixture: str, expected: dict[str, int | str | Array], + *, backend: str = "native", mode: str = "optimal", stdin: bool = False) -> None: + path = self.fixture(fixture) + args = ["-" if stdin else str(path), "--backend", backend] + code, stdout, stderr = self.invoke(name, args, path.read_bytes() if stdin else None) + require(code == 0 and not stderr, f"{name}: expected success, got {code}; stderr={stderr!r}; stdout={stdout!r}") + parsed = parse_output(stdout) + require(parsed.assignments == expected, f"{name}: wrong source assignment: {parsed.assignments!r}") + markers = {"optimal": ["----------", "=========="], "satisfy": ["----------"], + "unsat": ["=====UNSATISFIABLE====="]}[mode] + require(parsed.markers == markers, f"{name}: wrong completion markers: {parsed.markers!r}") + comment = "% guarantee: exact integer search" if backend == "native" else "% guarantee: numerical; requested MIP gaps: 0" + require(parsed.comments == [comment], f"{name}: missing/wrong guarantee attribution: {parsed.comments!r}") + + def error(self, name: str, args: list[str], text: str | None = None, contains: str = "") -> None: + code, stdout, stderr = self.invoke(name, args, None if text is None else text.encode()) + require(code == 2 and stdout == "" and bool(stderr), + f"{name}: rejection must emit only stderr, got code={code}, stdout={stdout!r}, stderr={stderr!r}") + require(contains.casefold() in stderr.casefold(), f"{name}: unexpected error: {stderr!r}") + + def native_controls(self, highs: str) -> None: + # Original source oracle: two binary items of weight 2 cannot both fit + # capacity 3; z counts chosen items. No reference result is used. + source = ("var 0..1: x; var 0..1: y; var 0..2: z :: output_var; " + "constraint int_lin_le([2,2],[x,y],3); " + "constraint int_lin_eq([1,1,-1],[x,y,z],0); solve maximize z;") + routes = [ + ("auto", []), ("plain", []), + ("race", ["--native-race-seconds", "0.02", "--native-race-nodes", "2"]), + ("auto", ["--native-auto-presolve", "off", "--native-auto-components", "off", + "--native-auto-symmetry", "off", "--native-auto-knapsack", "off"]), + ("configured", ["--native-search", "bab"]), + ("configured", ["--native-search", "dfs", "--native-branching", "reliability", + "--native-branching-probes", "8", "--native-neighborhood", "hamming", + "--native-neighborhood-radius", "1", "--native-neighborhood-nodes", "8", + "--native-neighborhood-seconds", "0.02", "--native-max-open-nodes", "100"]), + ("configured", ["--native-search", "best-bound"]), + ] + for order in ("bab", "dfs", "best-bound"): + for lp in ("root", "updated"): + extra = ["--native-search", order, "--native-lp", lp, "--native-root-cuts", "on", + "--native-bound-tightening", "off"] + if lp == "updated": + extra += ["--native-lp-interval", "2"] + routes.append(("configured", extra)) + for index, (mode, extra) in enumerate(routes): + args = ["--minizinc", "--native-mode", mode, "-", "--native-diagnostics", "on", *extra] + code, stdout, stderr = self.invoke(f"native-controls-{index}", args, source.encode()) + lp_requested = "--native-lp" in extra + if lp_requested and highs == "unavailable": + require(code == 2 and not stdout and "unsupported" in stderr.lower(), + "Requested unavailable checked LP must fail explicitly") + continue + parsed = parse_output(stdout) + require(code == 0 and not stderr and parsed.assignments == {"z": 1} and + parsed.markers == ["----------", "=========="], + f"Native route {index} failed its independent source oracle: {code}, {stdout!r}, {stderr!r}") + require(f"% native-mode: {mode}" in parsed.comments, "Requested native route missing from diagnostics") + backend = next((line for line in parsed.comments if line.startswith("% native-backend: ")), "") + require("Gecode native" in backend, "Actual native backend attribution missing") + if mode == "configured" and "bab" not in extra: + require("native frontier" in backend and "frontier-admitted=" in stdout, + "Configured frontier controls did not reach the frontier solver") + if lp_requested: + require("checked LP" in backend and re.search(r"lp-calls=[1-9]\d*", stdout), + "Checked LP controls did not perform an LP attempt") + if "--native-neighborhood" in extra: + require("neighborhood-attempts=" in stdout and "branching-probes=" in stdout, + "Requested branching/neighborhood work counters missing") + if "--native-auto-presolve" in extra: + require(all(f"auto-{feature}=off" in stdout for feature in ("presolve", "components", "symmetry", "knapsack")), + "Automatic transformation flags were not forwarded") + for mode in ("auto", "plain", "race", "configured"): + code, stdout, stderr = self.invoke(f"native-controls-zero-{mode}", + ["--minizinc", "-", "--native-mode", mode, "--native-node-limit", "0"], source.encode()) + parsed = parse_output(stdout) + require(code == 0 and not parsed.assignments and parsed.markers == ["=====UNKNOWN====="], + f"Native mode {mode} ignored the shared zero node budget") + invalid = [ + ["--native-mode", "race", "--native-race-nodes", "0"], + ["--native-mode", "race", "--native-race-seconds", "nan"], + ["--native-mode", "plain", "--native-auto-presolve", "off"], + ["--native-mode", "configured", "--native-root-cuts", "on"], + ["--native-mode", "configured", "--native-neighborhood-nodes", "1"], + ["--native-mode", "configured", "--native-search", "bab", "--native-branching", "reliability"], + ["--native-mode", "configured", "--native-lp", "updated", "--native-lp-interval", "4294967296"], + ["--native-node-limit", "18446744073709551616"], + ["--native-mode", "auto", "--native-mode", "auto"], + ["--native-diagnostics", "yes"], + ] + for index, flags in enumerate(invalid): + self.error(f"native-controls-reject-{index}", ["--minizinc", "-", *flags], source) + self.error("native-controls-highs-rejected", ["-", "--backend", "highs", "--native-mode", "auto"], source, + "require the native backend") + self.error("native-controls-node-alias-duplicate", ["-", "--node-limit", "2", "--native-node-limit", "3"], source, + "repeated") + + def cases(self, highs: str) -> None: + # Analytic source oracles, independent of any solver result. + alias = {"a": Array(((-1, 0), (2, 3)), (1, 1, 2, 3)), "y": 1} + channel = {"b": "true", "x": 1} + candidates = [(2*x+y, x, y) for x, y in itertools.product(range(-2, 5), range(-1, 4)) if x-y <= 2] + objective, x, y = max(candidates) + require(sum(candidate[0] == objective for candidate in candidates) == 1, "Oracle maximum is not unique") + maximum = {"x": x, "y": y, "z": objective} + for backend in ("native", "highs"): + if backend == "highs" and highs == "unavailable": + self.error("highs-unavailable", [str(self.fixture("linear-alias.fzn")), "--backend", backend], contains="unsupported") + continue + self.solve(f"{backend}-alias-file", "linear-alias.fzn", alias, backend=backend) + self.solve(f"{backend}-contiguous-domain", "cli-v2-domain-contiguous.fzn", {"x": 0}, backend=backend) + self.solve(f"{backend}-channel-stdin", "boolean-channel.fzn", channel, backend=backend, stdin=True) + self.solve(f"{backend}-maximum", "cli-v1-max-linear.fzn", maximum, backend=backend) + self.solve(f"{backend}-satisfaction", "cli-v1-satisfy-hidden.fzn", {"x": 1}, backend=backend, mode="satisfy") + self.solve(f"{backend}-infeasible-alias", "cli-v1-infeasible-alias.fzn", {}, backend=backend, mode="unsat") + self.solve(f"{backend}-constant-goal", "cli-v1-constant-objective.fzn", {"w": 5}, backend=backend) + self.solve(f"{backend}-empty-array", "cli-v1-empty-output.fzn", {"a": Array(((1, 0),), ())}, backend=backend, mode="satisfy") + self.solve(f"{backend}-reified-complement", "cli-v1-reified-complement.fzn", {"b": "false", "x": 2}, backend=backend) + feasible = [(yv, xv) for xv, yv in itertools.product(range(-1, 3), range(-2, 3)) if not (-2*xv+yv-xv <= -1)] + min_y, min_x = min(feasible) + require(sum(yv == min_y for yv, xv in feasible) == 1, "Reification oracle is not unique") + self.solve(f"{backend}-reified-signed-alias", "cli-v1-reified-signed-alias.fzn", {"b": "false", "x": min_x, "y": min_y}, backend=backend) + # Global source semantics remain present even when another backend lacks them. + table_candidates = [(yv, xv) for xv in (-3, 1, 3) for yv in range(-2, 3) + if (xv, yv) in ((-3, 2), (1, -2), (3, 1))] + table_y, table_x = min(table_candidates) + hole_candidates = sorted(set((-3, -1, 1, 4)) & set((-1, 1, 3))) + hole_min = min(v for v in hole_candidates if v >= 0) + # Enumerate original successor functions, then walk their labeled graph. + circuits = [] + for successors in itertools.product(range(3, 6), repeat=3): + seen, node = set(), 3 + for _ in range(3): + if node in seen: + break + seen.add(node) + node = successors[node-3] + if len(seen) == 3 and node == 3: + circuits.append(successors) + circuit = min(circuits) + require(sum(c[0] == circuit[0] for c in circuits) == 1, "Circuit optimum is not unique") + # Independent integer-time scheduling oracle. Integer endpoints make + # these checks exhaustive for half-open continuous-time task usage. + def cumulative(starts: tuple[int, ...], durations: tuple[int, ...], + heights: tuple[int, ...], capacity: int) -> bool: + require(len(starts) == len(durations) == len(heights), "Malformed scheduling oracle") + require(capacity >= 0 and all(v >= 0 for v in (*durations, *heights)), "Negative oracle task data") + first = min(starts, default=0) + last = max((s+d for s, d in zip(starts, durations)), default=first) + return all(sum(h for s, d, h in zip(starts, durations, heights) if s <= t < s+d) <= capacity + for t in range(first, last)) + zero_starts = [s for s in range(-2, 3) if cumulative((s,), (0,), (2,), 1)] + require(zero_starts == list(range(-2, 3)), "Zero-duration task incorrectly consumes capacity") + require(not any(cumulative((s,), (1,), (2,), 1) for s in range(-2, 3)), "Positive-duration excess demand is feasible") + touching_starts = [s for s in range(-1, 3) if cumulative((-1, s), (1, 2), (2, 2), 2)] + require(touching_starts == [0, 1, 2], "Shared start/end time has the wrong half-open usage") + require(not cumulative((-1, -1), (2, 1), (2, 1), 2), "Overlapping demand oracle is feasible") + fixed_durations = set((0, 2)) & set(range(1, 4)) + fixed_capacities = set((1, 2)) & {2} + require(fixed_durations == {2} and fixed_capacities == {2}, "Original alias parameter domains are not singleton") + duration, capacity, height = next(iter(fixed_durations)), next(iter(fixed_capacities)), 1 + fixed_starts = [s for s in range(-1, 4) if cumulative((-1, s), (duration, 1), (height, 2), capacity)] + require(fixed_starts == [1, 2, 3], "Fixed-alias scheduling oracle changed") + # Walk raw one-based FlatZinc transition tables. The oracle never uses + # compiled sparse IR, native DFA objects, or solver output as its input. + def regular(word: tuple[int, ...], states: int, symbols: int, + table: tuple[int, ...], initial: int, finals: tuple[int, ...]) -> bool: + require(states > 0 and symbols > 0 and len(table) == states*symbols, + "Malformed regular oracle dimensions") + require(1 <= initial <= states and all(0 <= target <= states for target in table) + and all(1 <= final <= states for final in finals), "Malformed regular oracle states") + current = initial + for symbol in word: + if not 1 <= symbol <= symbols: + return False + current = table[(current-1)*symbols+symbol-1] + if current == 0: + return False + return current in finals + regular_candidates = [word for word in itertools.product(range(4), repeat=2) + if regular(word, 3, 2, (2,3,0,3,3,0), 1, (3,))] + require(regular_candidates == [(1,2), (2,1)], "Regular accepted-word oracle changed") + regular_word = min(regular_candidates) + repeated_values = [v for v in (1,2) if regular((v,v,v), 2, 2, (1,2,2,1), 1, (2,))] + require(repeated_values == [2], "Regular repeated-alias oracle changed") + require(not regular((1,1), 2, 2, (1,2,2,1), 1, (2,)), "Nonfinal regular word was accepted") + require(regular((), 2, 2, (0,0,0,0), 2, (2,)) and + not regular((), 2, 2, (0,0,0,0), 2, (1,)), "Regular empty-word semantics changed") + require(not regular((1,), 1, 1, (0,), 1, (1,)) and + not regular((0,), 1, 1, (1,), 1, (1,)) and + not regular((), 1, 1, (1,), 1, ()), "Regular failure-state/alphabet/final-set semantics changed") + nonunit_words = [v for v in range(4) if regular((v,), 3, 2, (2,3,0,0,0,0), 1, (3,1,3))] + interval_words = [v for v in range(4) if regular((v,), 3, 2, (2,3,0,0,0,0), 1, tuple(range(2,4)))] + require(nonunit_words == [2] and interval_words == [1,2], "Regular literal final-set oracle changed") + globals_cases = ( + ("cli-v3-regular-word.fzn", dict(zip(("x", "y"), regular_word)), "optimal"), + ("cli-v3-regular-rejected-word.fzn", {}, "unsat"), + ("cli-v3-regular-alias-repeat.fzn", {"x": repeated_values[0], "y": repeated_values[0]}, "optimal"), + ("cli-v3-regular-empty-accept.fzn", {"word": Array(((1,0),), ())}, "satisfy"), + ("cli-v3-regular-empty-reject.fzn", {}, "unsat"), + ("cli-v3-regular-dead-transition.fzn", {}, "unsat"), + ("cli-v3-regular-nonunit-finals.fzn", {"x": max(nonunit_words)}, "optimal"), + ("cli-v3-regular-final-interval.fzn", {"x": min(interval_words)}, "optimal"), + ("cli-v3-regular-empty-finals.fzn", {}, "unsat"), + ("cli-v3-regular-zero-symbol.fzn", {}, "unsat"), + ("cli-v2-cumulative-zero-duration.fzn", {"s": min(zero_starts)}, "optimal"), + ("cli-v2-cumulative-positive-duration-unsat.fzn", {}, "unsat"), + ("cli-v2-cumulative-half-open.fzn", {"a": -1, "b": min(touching_starts)}, "optimal"), + ("cli-v2-cumulative-overlap-unsat.fzn", {}, "unsat"), + ("cli-v2-cumulative-fixed-alias.fzn", {"d": duration, "duration_alias": duration, "h": height, + "capacity": capacity, "capacity_alias": capacity, + "s": min(fixed_starts)}, "optimal"), + ("cli-v2-circuit-offset.fzn", dict(zip(("x", "y", "z"), circuit)), "optimal"), + ("cli-v2-circuit-subtours.fzn", {}, "unsat"), + ("cli-v2-circuit-singleton.fzn", {}, "satisfy"), + ("cli-v2-table-holes.fzn", {"x": table_x, "y": table_y}, "optimal"), + ("cli-v2-table-alias-unsat.fzn", {}, "unsat"), + ("cli-v2-table-empty.fzn", {}, "unsat"), + ("cli-v2-holey-alias.fzn", {"x": hole_min, "y": hole_min}, "optimal"), + ("cli-v1-global-element.fzn", {"i": 2, "v": -4}, "optimal"), + ("cli-v1-global-distinct.fzn", {"a": Array(((1, 3),), (1, 2, 3))}, "optimal"), + ("cli-v1-global-repeated-alias.fzn", {}, "unsat"), + ) + for fixture, expected, mode in globals_cases: + self.solve(f"native-{fixture}", fixture, expected, mode=mode) + self.error(f"highs-rejects-{fixture}", [str(self.fixture(fixture)), "--backend", "highs"], contains="unsupported") + # Default selection must be native/exact, without a fallback flag. + default = self.fixture("boolean-channel.fzn") + code, stdout, stderr = self.invoke("default-native", [str(default)]) + parsed = parse_output(stdout) + require(code == 0 and not stderr and parsed.assignments == channel and + parsed.comments == ["% guarantee: exact integer search"] and + parsed.markers == ["----------", "=========="], "Default backend/guarantee/output changed") + for fixture, fragment in (("cli-v3-regular-malformed-matrix.fzn", "Q*S"), + ("cli-v3-regular-bad-target.fzn", "0..Q"), + ("cli-v3-regular-bad-initial.fzn", "1..Q"), + ("cli-v3-regular-nonliteral-parameter.fzn", "expected an integer literal"), + ("cli-v3-regular-unsupported-set.fzn", "gecode_regular_set"), + ("cli-v3-regular-wrong-arity.fzn", "arity"), + ("cli-v3-regular-bad-count.fzn", "positive"), + ("cli-v2-circuit-empty.fzn", "nonempty"), + ("cli-v2-cumulative-malformed-four.fzn", "equal lengths"), + ("cli-v2-cumulative-wrong-arity.fzn", "arity"), + ("cli-v2-cumulative-unsupported-six.fzn", "multi-machine"), + ("cli-v2-cumulative-unsupported-seven.fzn", "fzn_cumulatives"), + ("cli-v2-cumulative-unfixed-parameter.fzn", "singleton"), + ("cli-v2-table-zero-arity.fzn", "zero-arity"), + ("cli-v1-malformed-arity.fzn", "argument"), + ("cli-v1-unknown-predicate.fzn", "cli_v1_unknown_predicate"), + ("cli-v1-unbounded-domain.fzn", "finite explicit domain"), + ("unsupported.fzn", "int_times")): + self.error(f"reject-{fixture}", [str(self.fixture(fixture))], contains=fragment) + self.error("malformed-stdin", ["-"], "var int: x; solve minimize missing;", "missing") + self.error("unsupported-string-escape", ["-"], "solve :: invalid(\"\\123\") satisfy;", "escape") + code, stdout, stderr = self.invoke("invalid-utf8-stdin", ["-"], b'solve :: text("\xff") satisfy;') + require(code == 2 and not stdout and "UTF-8" in stderr, "Invalid UTF-8 input was not rejected") + self.error("missing-file", [str(self.fixtures / "cli-v1-does-not-exist.fzn")], contains="Cannot open") + self.error("unknown-flag", [str(default), "--not-a-solver-flag", "1"], contains="Unsupported option") + self.error("unknown-backend", [str(default), "--backend", "auto"], contains="Backend") + self.error("missing-option-value", [str(default), "--node-limit"], contains="Missing") + self.error("duplicate-option", [str(default), "--node-limit", "1", "--node-limit", "2"], contains="repeated") + self.error("nonfinite-time", [str(default), "--time-limit", "nan"], contains="finite") + self.error("negative-node-count", [str(default), "--node-limit", "-1"], contains="unsigned") + self.error("input-byte-limit", [str(default), "--max-input-bytes", "1"], contains="byte limit") + self.error("no-arguments", [], contains="Usage") + # Exit status 1 and UNKNOWN are never UNSAT or an exhaustive marker. + source = self.fixture("cli-v1-max-linear.fzn") + for key, value in (("--time-limit", "0"), ("--node-limit", "0")): + code, stdout, stderr = self.invoke(f"zero-{key[2:]}", [str(source), key, value]) + parsed = parse_output(stdout) + require(code == 1 and not parsed.assignments and parsed.markers == ["=====UNKNOWN====="], + f"Zero budget incorrectly completed: {code}, {stdout!r}, {stderr!r}") + code, stdout, stderr = self.invoke("native-one-node", [str(source), "--node-limit", "1"]) + limited = parse_output(stdout) + require(code == 1 and "node_limit" in stderr, f"One-node fixture did not actually interrupt: {code}, {stdout!r}, {stderr!r}") + require(limited.comments == ["% guarantee: exact integer search"], "Limited native result lost attribution") + if limited.assignments: + require(limited.markers == ["----------"] and set(limited.assignments) == {"x", "y", "z"}, "Limited witness claimed completion") + xv, yv, zv = (limited.assignments[key] for key in ("x", "y", "z")) + require(all(type(v) is int for v in (xv, yv, zv)) and -2 <= xv <= 4 and -1 <= yv <= 3 and + xv-yv <= 2 and zv == 2*xv+yv and zv <= objective, "Limited source witness is invalid") + else: + require(limited.markers == ["=====UNKNOWN====="], "Limit without witness must be UNKNOWN") + code, stdout, stderr = self.invoke("help", ["--help"]) + require(code == 0 and not stderr and "Usage:" in stdout, "Help invocation failed") + # Quoting and file-path handling must not depend on the source checkout name. + with tempfile.TemporaryDirectory(prefix="fzn driver path ") as directory: + spaced = Path(directory) / "source model.fzn" + spaced.write_bytes(default.read_bytes()) + code, stdout, stderr = self.invoke("filename-with-spaces", [str(spaced)]) + require(code == 0 and not stderr and parse_output(stdout).assignments == channel, "Spaced filename failed") + self.native_controls(highs) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--binary", required=True, type=Path) + parser.add_argument("--fixtures", type=Path, default=Path(__file__).parent / "flatzinc-fixtures") + parser.add_argument("--highs", choices=("available", "unavailable"), default="available") + parser.add_argument("--timeout", type=float, default=60.0) + args = parser.parse_args() + try: + require(args.binary.is_file(), f"Required prebuilt driver is missing: {args.binary}") + require(math.isfinite(args.timeout) and args.timeout > 0, "Test timeout must be finite and positive") + digest = hashlib.sha256(args.binary.read_bytes()).hexdigest() + suite = Suite(args.binary.resolve(), args.fixtures.resolve(), args.timeout) + suite.cases(args.highs) + require(len(suite.results) == (135 if args.highs == "available" else 126), "Required CLI cases were skipped") + print(json.dumps({"status": "passed", "binary_sha256": digest, "cases": len(suite.results), + "fixture_sha256": suite.sources, "checks": suite.results}, sort_keys=True)) + return 0 + except (Failure, OSError, UnicodeError) as error: + print(f"FlatZinc driver CLI conformance failed: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/optimize/globals.cpp b/test/optimize/globals.cpp new file mode 100644 index 0000000000..79b384dc94 --- /dev/null +++ b/test/optimize/globals.cpp @@ -0,0 +1,196 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace O=Gecode::Optimize; +constexpr double inf=std::numeric_limits::infinity(); +using Assignment=std::vector; + +// Enumerate the complete finite product using predicates independent of the +// library's global validator and native compiler. Check both acceptance and +// optimal objective, then compare the native search result with that oracle. +static void oracle(O::Model& model,const std::vector>& domains, + const std::function& predicate) { + auto snapshot=model.snapshot();assert(domains.size()==snapshot.variables.size()); + double best=snapshot.objective.sense==O::ObjectiveSense::Minimize?inf:-inf; + std::size_t feasible=0; + Assignment assignment(domains.size()); + const auto visit=[&](auto&& self,std::size_t index)->void { + if(index values(assignment.begin(),assignment.end()); + const auto checked=O::validate(snapshot,values,0,0); + assert(checked.model_valid && checked.valid==expected); + if(!expected) return; + ++feasible; + double objective=snapshot.objective.offset; + for(const auto& term:snapshot.objective.terms) objective+=term.coefficient*values[term.variable.id]; + if(snapshot.objective.sense==O::ObjectiveSense::Minimize) best=std::min(best,objective); + else best=std::max(best,objective); + }; + visit(visit,0); + O::SolveOptions options;options.backend=O::Backend::Native;options.guarantee=O::Guarantee::Exact; + const auto result=O::solve(snapshot,options); + if(!O::capabilities(O::Backend::Native).available) { + assert(result.termination==O::Termination::Unsupported);return; + } + if(feasible) { + if(result.termination!=O::Termination::Optimal) std::cerr< static void invalid(F operation) {bool caught=false;try{operation();}catch(const O::ModelError&){caught=true;}assert(caught);} + +int main() { + O::Model different;std::vector v; + for(int i=0;i<4;++i) v.push_back(different.add_integer(-1,2)); + auto all=O::add_all_different(different,v,"permutation"); + different.minimize({{v[0],1},{v[1],2},{v[2],3},{v[3],4}},-7); + oracle(different,{{-1,2},{-1,2},{-1,2},{-1,2}},[](const auto& a){auto b=a;std::sort(b.begin(),b.end());return std::adjacent_find(b.begin(),b.end())==b.end();}); + O::Model aliases;auto x=aliases.add_integer(0,2); + O::add_all_different(aliases,{x,x});oracle(aliases,{{0,2}},[](const auto&){return false;}); + + O::Model element;auto index=element.add_integer(-2,2),a=element.add_integer(0,2),b=element.add_integer(-1,1); + O::add_element(element,index,{a,a,b},index,-1); + element.maximize({{index,3},{a,-2},{b,1}},4); + oracle(element,{{-2,2},{0,2},{-1,1}},[](const auto& q){return q[0]>=-1 && q[0]<=1 && q[0]==(q[0]<1?q[1]:q[2]);}); + O::Model empty_element;auto e=empty_element.add_integer(0,1); + O::add_element(empty_element,e,{},e);oracle(empty_element,{{0,1}},[](const auto&){return false;}); + + O::Model table;auto tx=table.add_integer(0,2),ty=table.add_integer(0,2); + O::add_table(table,{tx,ty,tx},{{0,0,1},{1,2,1},{2,0,2},{1,2,1}}); + table.minimize({{tx,3},{ty,-1}},-9); + oracle(table,{{0,2},{0,2}},[](const auto& q){return (q[0]==1 && q[1]==2)||(q[0]==2 && q[1]==0);}); + O::Model false_table;O::add_table(false_table,{},{});oracle(false_table,{},[](const auto&){return false;}); + O::Model true_table;O::add_table(true_table,{},{{}});oracle(true_table,{},[](const auto&){return true;}); + + O::Model schedule;auto s0=schedule.add_integer(0,4),s1=schedule.add_integer(0,4),s2=schedule.add_integer(0,4); + O::add_cumulative(schedule,{s0,s1,s2},{2,2,0},{1,1,100},1); + schedule.minimize({{s0,2},{s1,1},{s2,10}}); + oracle(schedule,{{0,4},{0,4},{0,4}},[](const auto& q){ + for(int time=0;time<7;++time) if((q[0]<=time && time1) return false; + return true; + }); + for(int capacity=1;capacity<=2;++capacity) { + O::Model same_start;auto s=same_start.add_integer(-2,2); + O::add_cumulative(same_start,{s,s},{2,2},{1,1},capacity); + oracle(same_start,{{-2,2}},[capacity](const auto&){return capacity==2;}); + } + O::Model no_resource;auto start=no_resource.add_integer(-2,2); + O::add_cumulative(no_resource,{start,start},{0,2},{99,0},0); + oracle(no_resource,{{-2,2}},[](const auto&){return true;}); + + O::Model circuit;std::vector successors; + for(int i=0;i<4;++i) successors.push_back(circuit.add_integer(-2,1)); + O::add_circuit(circuit,successors,-2); + circuit.maximize({{successors[0],1},{successors[1],2},{successors[2],3},{successors[3],4}},8); + oracle(circuit,{{-2,1},{-2,1},{-2,1},{-2,1}},[](const auto& q){ + bool seen[4]={};int node=0; + for(int step=0;step<4;++step){if(seen[node])return false;seen[node]=true;node=q[node]+2;} + return node==0; + }); + O::Model singleton;auto successor=singleton.add_integer(5,9); + O::add_circuit(singleton,{successor},7);oracle(singleton,{{5,9}},[](const auto& q){return q[0]==7;}); + O::Model same_successor;auto s=same_successor.add_integer(0,1); + O::add_circuit(same_successor,{s,s});oracle(same_successor,{{0,1}},[](const auto&){return false;}); + + // Old numerical workflows must reject globals rather than solve a relaxation + // as if it represented the original model. + const auto automatic=O::solve(different); + if(O::capabilities(O::Backend::Native).available) { + assert(automatic.has_solution() && automatic.backend=="Gecode native"); + const O::ObjectiveData total{{{v[0],1},{v[1],1},{v[2],1},{v[3],1}},0,O::ObjectiveSense::Minimize}; + const O::ObjectiveData prefer{{{v[0],1}},0,O::ObjectiveSense::Maximize}; + const auto ordered=O::solve_lexicographic(different,{{total,0,0,"total"},{prefer,0,0,"first"}}); + assert(ordered.completed_numerically() && ordered.objective_values==std::vector({2,2})); + assert(O::validate(different.snapshot(),ordered.final_solution.values,0,0).valid); + } else assert(automatic.termination==O::Termination::Unsupported); + O::SolveOptions highs;highs.backend=O::Backend::Highs; + assert(O::solve(different,highs).termination==O::Termination::Unsupported); + O::SolveSession session;assert(session.solve(different).termination==O::Termination::Unsupported); + assert(O::analyze_conflict(different).status==O::ConflictStatus::Unsupported); + invalid([&]{O::write_model(different,"unsupported-global.lp");}); + + const auto revision=different.revision();const auto snapshot=different.snapshot(); + invalid([&]{O::add_table(different,{v[0]},{{1,2}});}); + invalid([&]{O::add_cumulative(different,{v[0]},{-1},{1},1);}); + invalid([&]{O::add_circuit(different,{});}); + invalid([&]{O::add_all_different(different,{x});}); + assert(different.revision()==revision && different.snapshot().globals.size()==snapshot.globals.size()); + invalid([&]{different.remove(v[0]);}); + auto malformed=snapshot;malformed.globals[0].global.model_id=0; + assert(!O::validate(malformed,{-1,0,1,2}).model_valid); + malformed=snapshot;std::get(malformed.globals[0].payload).variables[0]=x; + assert(!O::validate(malformed,{-1,0,1,2}).model_valid); + different.set_name(all,"renamed");assert(different.global(all).name=="renamed"); + different.remove(all);invalid([&]{different.remove(all);}); + different.minimize({});different.remove(v[0]); + O::validate_structure(different.snapshot()); // Inactive global can refer to tombstones. + O::Model moved(std::move(different));O::validate_structure(moved.snapshot()); + invalid([&]{different.add_global(O::AllDifferentData{});}); + + O::Model huge;auto h0=huge.add_integer(0,0),h1=huge.add_integer(0,0); + O::add_cumulative(huge,{h0,h1},{1,1},{INT64_C(9007199254740992),INT64_C(9007199254740992)},INT64_C(9007199254740992)); + const auto failed=O::validate(huge.snapshot(),{0,0},0,0); + assert(failed.model_valid && !failed.valid && failed.violated_globals==1); + O::SolveOptions exact;exact.backend=O::Backend::Native;exact.guarantee=O::Guarantee::Exact; + assert(O::solve(huge,exact).termination==O::Termination::Unsupported); + + // The previous int64 energy/width guards admitted these six tasks, but + // C*est+sum(energy) overflows Omega/Lambda envelopes. Reject before posting. + O::Model envelope;std::vector tasks; + for(int i=0;i<6;++i) tasks.push_back(envelope.add_integer(715827880,715827880)); + O::add_cumulative(envelope,tasks,std::vector(6,1431655760), + std::vector(6,1073741823),2147483646); + const auto unsafe_envelope=O::solve(envelope,exact); + assert(unsafe_envelope.termination==O::Termination::Unsupported && !unsafe_envelope.has_solution()); + + // Exactly at the conservative edge-finding int-cast guard and just outside. + // C=2, h_min=1, E=2 gives 4*B+2 <= native_max=2147483646. + for(int sign:{-1,1}) for(int outside:{0,1}) { + const int magnitude=536870911+outside; + const int begin=sign>0?magnitude-1:-magnitude; + O::Model boundary;auto p=boundary.add_integer(begin,begin),q=boundary.add_integer(begin,begin); + O::add_cumulative(boundary,{p,q},{1,1},{1,1},2); + if(outside) assert(O::solve(boundary,exact).termination==O::Termination::Unsupported); + else oracle(boundary,{{begin,begin},{begin,begin}},[](const auto&){return true;}); + } + // Singleton and zero-resource constraints need no envelope arithmetic. + O::Model lone;auto late=lone.add_integer(2147483645,2147483645); + O::add_cumulative(lone,{late},{1},{1},10); + oracle(lone,{{2147483645,2147483645}},[](const auto&){return true;}); + O::Model no_energy;auto late_zero=no_energy.add_integer(2147483646,2147483646); + O::add_cumulative(no_energy,{late_zero,late_zero},{2147483646,0},{0,2147483646},0); + oracle(no_energy,{{2147483646,2147483646}},[](const auto&){return true;}); + O::Model overloaded;auto overload_start=overloaded.add_integer(-2,2); + O::add_cumulative(overloaded,{overload_start},{2},{2},1); + oracle(overloaded,{{-2,2}},[](const auto&){return false;}); + + // Native int addition of the two minimum heights overflowed here. The wide + // disjunctive test must preserve exact non-overlap semantics instead. + O::Model resource_units;auto r0=resource_units.add_integer(0,2),r1=resource_units.add_integer(0,2); + O::add_cumulative(resource_units,{r0,r1},{1,1},{1073741823,2147483646},2147483646); + resource_units.minimize({{r0,1},{r1,1}}); + oracle(resource_units,{{0,2},{0,2}},[](const auto& q){return q[0]!=q[1];}); + + // The advanced propagator indexes count*distinct_heights using int. + O::Model array_product;auto repeated=array_product.add_integer(0,0); + std::vector distinct_heights(46341); + for(std::size_t i=0;i(i+1); + O::add_cumulative(array_product,std::vector(46341,repeated), + std::vector(46341,1),distinct_heights,46341); + assert(O::solve(array_product,exact).termination==O::Termination::Unsupported); + std::cout<<"native globals and independent finite-product oracles passed\n"; +} diff --git a/test/optimize/io.cpp b/test/optimize/io.cpp new file mode 100644 index 0000000000..b4f6116a59 --- /dev/null +++ b/test/optimize/io.cpp @@ -0,0 +1,269 @@ +#include +#include +#include +#ifdef GECODE_OPTIMIZE_IO_TEST_HIGHS +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace O=Gecode::Optimize; +namespace fs=std::filesystem; +namespace { +constexpr double inf=std::numeric_limits::infinity(); +void require(bool value,const std::string& message){if(!value)throw std::runtime_error(message);} +void rejected(const std::function& action,const std::string& message){ + bool caught=false;try{action();}catch(const O::ModelError&){caught=true;}require(caught,message); +} +void put(const fs::path& path,const std::string& text){std::ofstream f(path);f<(f),std::istreambuf_iterator()};} +struct Directory { + fs::path path=fs::temp_directory_path()/("gecode-io-tests-"+std::to_string(std::random_device{}())); + Directory(){require(fs::create_directory(path),"temporary directory unavailable");} + ~Directory(){std::error_code ec;fs::remove_all(path,ec);} +}; +void equal(const O::ModelSnapshot& a,const O::ModelSnapshot& b){ + require(a.variables.size()==b.variables.size()&&a.rows.size()==b.rows.size(),"roundtrip dimensions"); + for(std::size_t i=0;i= 5\nBounds\n 0 <= x <= 10\nGeneral\n x\nEnd\n"); + auto m=O::read_model(path.string()).snapshot();require(m.objective.terms[0].coefficient==5&&m.objective.offset==7,"duplicate LP objective overwritten"); + require(m.rows[0].lower==3,"LP LHS constant ignored"); + put(path,"Maximize\n obj: 1e16 + 1 - 1e16\nSubject To\nEnd\n"); + require(O::read_model(path.string()).snapshot().objective.offset==1,"LP objective cancellation lost"); + for(const auto& relation:{">=","<=","="}){ + put(path,std::string("Minimize\n obj: x\nSubject To\n c: x + 1 - 1e16 ")+relation+" -1e16\nBounds\n x free\nEnd\n"); + const auto row=O::read_model(path.string()).snapshot().rows[0]; + require((std::string(relation)=="<="||row.lower==-1)&&(std::string(relation)==">="||row.upper==-1),"LP RHS cancellation lost residual"); + } + put(path,"Minimize\n obj: + 0 x + 0 y\nSubject To\n c: 2 x\n + 3 y >= 7\nBounds\n x free\n 2 <= y <= 8\nGeneral\n y\nSemi\n y\nEnd\n"); + m=O::read_model(path.string()).snapshot();require(m.variables[0].lower==-inf&&m.variables[1].type==O::VariableType::SemiInteger,"LP free/semi-integer import"); + require(m.rows[0].terms.size()==2,"LP wrapped row"); + put(path,"Minimize\n obj: y\nBounds\n 2 <= y <= 8\nSemi\n y\nGeneral\n y\nEnd\n"); + require(O::read_model(path.string()).snapshot().variables[0].type==O::VariableType::SemiInteger,"LP Semi then General lost integrality"); +} +void mps_semantics(const fs::path& dir){ + const auto path=dir/"external.mps"; + put(path,"NAME sample\nOBJSENSE MAX\nROWS\n N obj\n L upper\n G lower\n E eqpos\n E eqneg\nCOLUMNS\n x obj 2 upper 1\n x obj 3 lower 1\n x eqpos 1 eqneg 1\nRHS\n rhs obj -7 upper 10\n rhs lower 5 eqpos 8\n rhs eqneg 9\nRANGES\n range upper 4 lower 2\n range eqpos 3 eqneg -2\nBOUNDS\n FR bounds x\nENDATA\n"); + const auto m=O::read_model(path.string()).snapshot();require(m.objective.sense==O::ObjectiveSense::Maximize&&m.objective.offset==7&&m.objective.terms[0].coefficient==5,"MPS objective duplicates/sense/offset"); + require(m.rows[0].lower==6&&m.rows[0].upper==10&&m.rows[1].lower==5&&m.rows[1].upper==7&&m.rows[2].lower==8&&m.rows[2].upper==11&&m.rows[3].lower==7&&m.rows[3].upper==9,"MPS RANGES signs"); +} +void type_declarations(const fs::path& dir){ + const auto path=dir/"types.mps"; + const std::string base="NAME t\nROWS\n N obj\nCOLUMNS\n"; + const std::string column=" x obj 1\n"; + const std::string marked=" mark0 'MARKER' 'INTORG'\n"+column+" mark1 'MARKER' 'INTEND'\n"; + const std::vector declarations={" BV b x\n"," SC b x 2\n"," SI b x 2\n"," LI b x 1\n"," UI b x 2\n"}; + for(std::size_t i=0;i{" SI b x 2\n"," LI b x 1\n SI b x 2\n"}){ + put(path,base+marked+"BOUNDS\n"+declarations+"ENDATA\n"); + const auto v=O::read_model(path.string()).snapshot().variables[0]; + require(v.type==O::VariableType::SemiInteger&&v.lower==1&&v.upper==2,"valid SI discarded integer type"); + } + for(const auto& declarations:std::vector{" LI b x 1\n UI b x 2\n"," UI b x 2\n LI b x 1\n"}){ + put(path,base+column+"BOUNDS\n"+declarations+"ENDATA\n"); + const auto v=O::read_model(path.string()).snapshot().variables[0]; + require(v.type==O::VariableType::Integer&&v.lower==1&&v.upper==2,"valid integer bound order rejected"); + } + for(const auto& order:std::vector{"Binary\n x\nSemi\n x\n","Semi\n x\nBinary\n x\n","Binary\n x\nGeneral\n x\n","General\n x\nBinary\n x\n"}){ + const auto lp=dir/"types.lp";put(lp,"Minimize\n obj: x\n"+order+"End\n"); + rejected([&]{O::read_model(lp.string());},"conflicting LP declaration order accepted"); + } +} +void failures(const fs::path& dir){ + const auto target=dir/"protected.lp";put(target,"original bytes");auto m=comprehensive(); + auto bad=m.snapshot();bad.objective.offset=inf; + rejected([&]{O::write_model(bad,target.string());},"invalid export succeeded");require(get(target)=="original bytes","failed export destroyed destination"); + O::Model logical;auto active=logical.add_binary("active");auto quantity=logical.add_continuous(0,10,"quantity"); + const auto indicator=O::add_indicator(logical,active,true,{{quantity,1}},3,inf,"logical"); + rejected([&]{O::write_model(logical,target.string());},"original indicator semantics dropped on export"); + require(get(target)=="original bytes","indicator rejection changed destination"); + O::remove_indicator(logical,indicator.indicator); + O::write_model(logical,(dir/"removed-indicator.lp").string()); + rejected([&]{O::write_model(m,(dir/"missing"/"out.lp").string());},"missing directory accepted"); + rejected([&]{O::read_model((dir/"missing.lp").string());},"missing input accepted"); + rejected([&]{O::write_model(m,(dir/"out.txt").string());},"unknown output format accepted"); + auto protected_dir=dir/"destination.lp";fs::create_directory(protected_dir);put(protected_dir/"marker","keep"); + const auto count=[&]{return static_cast(std::distance(fs::directory_iterator(dir),fs::directory_iterator()));}; + const auto before=count();rejected([&]{O::write_model(m,protected_dir.string());},"directory replaced"); + require(get(protected_dir/"marker")=="keep"&&count()==before,"failed atomic replacement leaked or damaged files"); + O::Model nonrepresentable;auto x=nonrepresentable.add_continuous();nonrepresentable.add_row({{x,1}},std::numeric_limits::denorm_min(),std::numeric_limits::max()); + const auto mps=dir/"protected.mps";put(mps,"original MPS bytes");const auto files=count(); + rejected([&]{O::write_model(nonrepresentable,mps.string());},"unrepresentable MPS range changed semantics"); + require(get(mps)=="original MPS bytes"&&count()==files,"roundtrip rejection changed destination or leaked temp"); + for(const auto& bounds:std::vector>{{0.25,0.75},{0,0},{1,1}}){ + O::Model tighter;auto binary=tighter.add_binary();tighter.set_bounds(binary,bounds.first,bounds.second); + rejected([&]{O::write_model(tighter,mps.string());},"nondefault MPS binary bounds exported nonportably"); + require(get(mps)=="original MPS bytes"&&count()==files,"binary export rejection changed destination or leaked temp"); + const auto lp=dir/"tighter.lp";O::write_model(tighter,lp.string());equal(tighter.snapshot(),O::read_model(lp.string()).snapshot()); + fs::remove(lp); + } + const std::vector invalid_lp={ + "Minimize\n obj: [ x ^ 2 ] / 2\nEnd\n", + "Minimize\n obj: x\nSOS\n s: S1 :: x : 1\nEnd\n", + "Minimize\n obj: x\nSubject To\n c: z = 1 -> x <= 2\nEnd\n", + "Minimize\n obj: nan x\nEnd\n", + "Minimize\n obj: 1e-9999 x\nEnd\n", + "Minimize\n obj: 1e+2e+3\nEnd\n", + "Minimize\n obj: x\nBounds\n x <= 3\n x <= 2\nEnd\n", + "Minimize\n obj: x\nBounds\n x <= 3\n x free\nEnd\n", + "Minimize\n obj: x\nEnd\nSome ignored content\n", + "Minimize\n obj: x\n", + "Minimize\n obj: x\nSubject To\n c: x < 2\nEnd\n", + "\\ GECODE_NAME V absent 41\nMinimize\n obj: x\nEnd\n"}; + for(const auto& input:invalid_lp){put(target,input);rejected([&]{O::read_model(target.string());},"malformed/unsupported LP accepted: "+input);} + const std::string base="NAME t\nROWS\n N obj\n L row\nCOLUMNS\n x obj 1 row 1\n"; + for(const auto& tail:std::vector{ + "SOS\n S1 set\nENDATA\n","QUADOBJ\n x x 1\nENDATA\n","INDICATORS\n IF row x 1\nENDATA\n", + "RHS\n a row 1\n b row 2\nENDATA\n","RHS\n a unknown 1\nENDATA\n", + "BOUNDS\n UP b unknown 2\nENDATA\n","BOUNDS\n XX b x\nENDATA\n", + "BOUNDS\n LO b x 1\n LI b x 2\nENDATA\n","ENDATA\ntrailing\n",""}){ + put(mps,base+tail);rejected([&]{O::read_model(mps.string());},"malformed/unsupported MPS accepted: "+tail);} +} +#ifdef GECODE_OPTIMIZE_IO_TEST_HIGHS +// An independent parser checks exported mathematics, including every unused +// column. It intentionally ignores private display-name/type distinctions +// absent from HiGHS (Binary is represented as an integer with bounds [0,1]). +void highs_export(const O::ModelSnapshot& model,const fs::path& path){ + O::write_model(model,path.string()); + Highs highs;highs.setOptionValue("output_flag",false); + require(highs.readModel(path.string())==HighsStatus::kOk,"HiGHS export import warned/failed: "+path.string()); + const auto& lp=highs.getLp(); + require(static_cast(lp.num_col_)==model.variables.size(),"HiGHS lost unused column"); + require(lp.sense_==(model.objective.sense==O::ObjectiveSense::Minimize?ObjSense::kMinimize:ObjSense::kMaximize)&&lp.offset_==model.objective.offset,"HiGHS changed sense/offset"); + std::map columns; + for(std::size_t i=0;i costs(model.variables.size(),0);for(const auto& t:model.objective.terms)costs[t.variable.id]=t.coefficient; + for(const auto& v:model.variables){const auto name="x"+std::to_string(v.variable.id); + require(columns.count(name)!=0,"HiGHS lost column "+name+" in "+path.string());const auto col=columns.at(name); + const auto type=lp.integrality_.empty()?HighsVarType::kContinuous:lp.integrality_[col]; + const auto expected=v.type==O::VariableType::Continuous?HighsVarType::kContinuous:v.type==O::VariableType::SemiContinuous?HighsVarType::kSemiContinuous:v.type==O::VariableType::SemiInteger?HighsVarType::kSemiInteger:HighsVarType::kInteger; + require(type==expected&&lp.col_lower_[col]==v.lower&&lp.col_upper_[col]==v.upper&&lp.col_cost_[col]==costs[v.variable.id],"HiGHS changed exported variable domain/cost: "+std::to_string(v.variable.id)); + } + auto matrix=lp.a_matrix_;matrix.ensureColwise(); + std::vector> rows(static_cast(lp.num_row_)); + for(HighsInt col=0;col indices;for(std::size_t i=0;i terms;for(const auto& t:r.terms)terms.emplace("x"+std::to_string(t.variable.id),t.coefficient); + // HiGHS' free-MPS parser discards later N rows; these unconstrained + // expressions have no effect on the feasible set or objective. + if(path.extension()==".mps"&&r.lower==-inf&&r.upper==inf&&indices.count("r"+std::to_string(r.constraint.id))==0)continue; + const bool split=path.extension()==".lp"&&std::isfinite(r.lower)&&std::isfinite(r.upper)&&r.lower!=r.upper; + const auto check=[&](const std::string& name,double lb,double ub){ + require(indices.count(name)!=0,"HiGHS lost row "+name+" in "+path.string());const auto row=indices.at(name);++expected_rows; + require(lp.row_lower_[row]==lb&&lp.row_upper_[row]==ub&&rows[row]==terms,"HiGHS changed exported row: "+name);}; + check("r"+std::to_string(r.constraint.id),r.lower,split?inf:r.upper); + if(split)check("u"+std::to_string(r.constraint.id),-inf,r.upper); + } + require(expected_rows==static_cast(lp.num_row_),"HiGHS changed exported row count"); +} +void highs_portability(const fs::path& dir){ + auto all=comprehensive();for(const auto& ext:{".lp",".mps"})highs_export(all.snapshot(),dir/(std::string("highs-all")+ext)); + for(const auto& bounds:std::vector>{{0.25,0.75},{0,0},{1,1}}){ + O::Model m;auto x=m.add_binary();m.set_bounds(x,bounds.first,bounds.second);m.minimize({{x,1}}); + highs_export(m.snapshot(),dir/"highs-tighter.lp"); + } + O::Model m;auto x=m.add_integer(0,10);m.add_row({{x,100000000000000.125}},100000000000000.25,inf);m.minimize({{x,1}},0.125); + for(const auto& ext:{".lp",".mps"})highs_export(m.snapshot(),dir/(std::string("highs-precision")+ext)); +} +#endif +void public_mps(const fs::path& directory,const fs::path& output){ + struct Entry{const char* name;std::size_t variables,rows;}; + for(const auto& e:std::vector{{"p0033",33,16},{"lseu",89,28},{"p0201",201,133},{"p0282",282,241},{"p0548",548,176}}){ + const auto path=directory/(std::string(e.name)+".mps");auto m=O::read_model(path.string()).snapshot(); + require(m.variables.size()==e.variables&&m.rows.size()==e.rows,std::string("public MPS dimensions: ")+e.name); + for(const auto& v:m.variables)require((v.type==O::VariableType::Binary||v.type==O::VariableType::Integer)&&v.lower==0&&v.upper==1,"public binary bounds/types"); + O::validate_structure(m); + for(const auto& extension:{".lp",".mps"}){const auto saved=output/(std::string(e.name)+extension); + O::write_model(m,saved.string());equal(m,O::read_model(saved.string()).snapshot());} +#ifdef GECODE_OPTIMIZE_IO_TEST_HIGHS + for(const auto& extension:{".lp",".mps"})highs_export(m,output/(std::string("highs-")+e.name+extension)); +#endif + } +} +} +int main(int argc,char** argv){ + try{Directory temp;roundtrips(temp.path);precision(temp.path);lp_semantics(temp.path);mps_semantics(temp.path);type_declarations(temp.path);failures(temp.path); +#ifdef GECODE_OPTIMIZE_IO_TEST_HIGHS + highs_portability(temp.path); +#endif + if(argc>1)public_mps(argv[1],temp.path); + std::cout<<"PASS lossless numerical LP/MPS I/O"<<(argc>1?" and five public MPS imports":"") +#ifdef GECODE_OPTIMIZE_IO_TEST_HIGHS + <<" with independent HiGHS export checks" +#endif + <<'\n';return 0; + }catch(const std::exception& e){std::cerr<<"FAIL: "< +#include +#include +#include +#include +#include + +using namespace Gecode::Experimental::LpRelaxation; + +static void check(bool ok, const char* message) { + if (!ok) throw std::runtime_error(message); +} + +static void sparse_checks() { + const LinearModel input{{1,1,0, 0,1,1, 1,0,1},{1,1,1},{1,1,1}}; + Backend dense(input); + SparseBackend sparse(sparse_model(input)); + check(sparse.model.nonzeros()==6 && sparse.model.row_start.size()==4,"CSR storage"); + for (unsigned box=0;box<27;++box) { + std::vector lo(3),hi(3); auto remaining=box; + for (unsigned j=0;j<3;++j) { + const auto domain=remaining%3; remaining/=3; + lo[j]=domain==1; hi[j]=domain!=2; + } + const auto d=dense.bound(lo,hi,true),s=sparse.bound(lo,hi,true); + check(d.valid==s.valid,"sparse/dense bound validity"); + if (d.valid) { + check(d.lower_bound==s.lower_bound && s.certificate,"sparse/dense bound value"); + std::int64_t restored=0,original=0; + check(s.certificate->lower_bound({0,0,0},{1,1,1},restored) && + d.certificate->lower_bound({0,0,0},{1,1,1},original) && restored==original, + "sparse certificate sibling reuse"); + } + } + // Input containers remain mutable. Each constructor takes a fresh snapshot; + // changing one never changes already published dense/sparse backend models. + auto changed=input; + Backend before(changed); changed.a[0]=0; + Backend after(changed); + check(before.model.a[0]==1 && after.model.a[0]==0 && + before.SparseBackend::model.nonzeros()==6 && after.SparseBackend::model.nonzeros()==5, + "legacy mutable model reused stale CSR"); + auto csr=sparse_model(input); + SparseBackend immutable(csr); csr.a[0]=7; + check(immutable.model.a[0]==1,"sparse input mutation changed published model"); + for (unsigned bad=0;bad<5;++bad) { + auto invalid=sparse_model(input); + if (bad==0) invalid.row_start.clear(); + if (bad==1) invalid.column[1]=invalid.column[0]; + if (bad==2) invalid.a[0]=0; + if (bad==3) invalid.column[0]=3; + if (bad==4) invalid.a[0]=1000000001LL; + bool rejected=false; + try { SparseBackend unused(std::move(invalid)); } + catch (const std::invalid_argument&) { rejected=true; } + check(rejected,"invalid sparse backend input accepted"); + } + SparseLinearModel zero; + zero.row_start={0,0}; zero.b={1}; zero.c={-3,4}; + SparseBackend constant(zero); + const auto box=constant.bound({0,0},{1,1},true); + check(box.valid && box.lower_bound==-3 && constant.statistics().lp_calls==0, + "zero matrix numerical assertion or unsupported infeasibility inference"); + SparseBackend sibling(sparse_model({{1,1},{1},{2,9}})); + bool first_ok=true,second_ok=true; + std::thread first([&] { for (int i=0;i<20;++i) { + auto r=sibling.bound({0,1},{0,1}); first_ok &= r.valid && r.lower_bound==9; + } }); + std::thread second([&] { for (int i=0;i<20;++i) { + auto r=sibling.bound({1,0},{1,0}); second_ok &= r.valid && r.lower_bound==2; + } }); + first.join(); second.join(); + check(first_ok && second_ok,"sparse serialized sibling restoration"); + static_assert(std::is_base_of::value,"legacy sparse backend conversion"); +} + +int main() { + sparse_checks(); + // Fractional triangle-cover optimum is 1.5, hence integer lower bound 2. + Backend triangle({{1,1,0, 0,1,1, 1,0,1}, {1,1,1}, {1,1,1}}); + auto t = triangle.bound({0,0,0},{1,1,1}); + check(t.valid && t.lower_bound == 2, "triangle bound"); + check(std::abs(t.lp_objective-1.5) < 1e-8, "row-dual sign/LP objective"); + // Force one sibling to a high bound, then loosen it on another sibling. + Backend sibling({{1,1}, {1}, {2,9}}); + auto a = sibling.bound({0,1},{0,1}); + auto b = sibling.bound({1,0},{1,0}); + auto c = sibling.bound({0,0},{1,1}); + check(a.valid && a.lower_bound == 9, "first sibling"); + check(b.valid && b.lower_bound == 2, "second sibling"); + check(c.valid && c.lower_bound == 2, "restored bounds"); + // An infeasibility status is only a statistic. HiGHS may still supply + // multipliers, in which case only the exact checker can accept a bound. + (void) sibling.bound({0,0},{0,0}); + auto recovered = sibling.bound({0,0},{1,1}); + check(recovered.valid && recovered.lower_bound == 2, "recovery after infeasibility"); + check(sibling.statistics().infeasible_status >= 1, "infeasibility statistics"); + check(!sibling.bound({-1,0},{1,1}).valid, "nonbinary rejection"); + check(!sibling.bound({0},{1}).valid, "dimension rejection"); + bool threw = false; + try { Backend bad({{1}, {1}, {1,2}}); } catch (const std::invalid_argument&) { threw=true; } + check(threw,"invalid dense model rejection"); + Backend empty({{}, {}, {}}); + check(empty.bound({},{}).valid,"empty model"); + Backend box({{}, {}, {-3,4}}); + check(box.bound({0,0},{1,1}).lower_bound == -3,"negative-cost empty-row box"); + // One shared backend is serialized across callers; every bound restored. + bool first_ok = true, second_ok = true; + std::thread first([&] { for(int i=0;i<20;++i) { auto r=sibling.bound({0,1},{0,1}); first_ok &= r.valid && r.lower_bound==9; } }); + std::thread second([&] { for(int i=0;i<20;++i) { auto r=sibling.bound({1,0},{1,0}); second_ok &= r.valid && r.lower_bound==2; } }); + first.join(); second.join(); + check(first_ok && second_ok,"serialized concurrent sibling calls"); + const auto stats=sibling.statistics(); + check(stats.lp_calls==45 && stats.valid_bounds+stats.rejected==47 && stats.rejected>=2,"statistics"); + std::cout << "{\"status\":\"passed\",\"highs_version\":\"" << highsVersion() + << "\",\"checks\":14,\"serialized_thread_calls\":40," + << "\"cases\":[\"triangle_dual_sign_and_exact_bound\"," + << "\"sibling_bound_restoration\",\"recovery_after_infeasible_lp\"," + << "\"binary_and_dimension_checks\",\"empty_models\"," + << "\"serialized_callers\",\"sparse_equivalence_and_corruption\"],\"lp_calls\":" << stats.lp_calls + << ",\"valid_bounds\":" << stats.valid_bounds + << ",\"rejected\":" << stats.rejected << "}\n"; +} diff --git a/test/optimize/lp_basis.cpp b/test/optimize/lp_basis.cpp new file mode 100644 index 0000000000..1f121eab49 --- /dev/null +++ b/test/optimize/lp_basis.cpp @@ -0,0 +1,170 @@ +#include +#include +#include +#include +#include +#include +#include +#include +namespace O=Gecode::Optimize; +using B=O::LpBasisStatus; +using S=O::LpBasisSubmissionState; +constexpr double inf=std::numeric_limits::infinity(); +static void near(double a,double b){assert(std::isfinite(a)&&std::abs(a-b)<1e-7);} +templatestatic void invalid(F fn){bool caught=false;try{fn();}catch(const O::ModelError&){caught=true;}assert(caught);} +static void optimal(const O::LpBasisSolveResult& out,double objective){ + if(out.observed.result.termination!=O::Termination::Optimal) + std::cerr<checks().accepted); +} +int main(){ + O::Model model;auto x=model.add_continuous(),y=model.add_continuous(); + auto row=model.add_row({{x,1},{y,1}},4,inf);model.minimize({{x,2},{y,3}},7); + O::LpBasisData data;data.source=model.snapshot();data.columns={B::Basic,B::Lower};data.rows={B::Lower}; + auto basis=O::make_lp_basis(data);assert(basis->origin()==O::LpBasisOrigin::Caller); + assert(basis->id()==model.id()&&basis->revision()==model.revision()); + data.source.objective.offset=100;data.columns[0]=B::Upper; + assert(basis->source().objective.offset==7&&*basis->columns()[0]==B::Basic); + for(int mode=0;mode<9;++mode){ + O::LpBasisData bad;bad.source=basis->source();bad.rows=basis->rows();bad.columns=basis->columns(); + if(mode==0)bad.rows.clear(); + if(mode==1)bad.columns.push_back(B::Lower); + if(mode==2)bad.columns[0].reset(); + if(mode==3)bad.columns[0]=B::Lower; + if(mode==4)bad.columns[1]=B::Basic; + if(mode==5)bad.columns[1]=static_cast(99); + if(mode==6)bad.columns[1]=B::Upper; + if(mode==7)bad.columns[1]=B::Zero; + if(mode==8)bad.source.rows[0].terms[0].variable.model_id=0; + invalid([&]{O::make_lp_basis(bad);}); + } + O::Model constant;constant.add_row({},-1,1);O::LpBasisData cd;cd.source=constant.snapshot();cd.rows={B::Basic}; + invalid([&]{O::make_lp_basis(cd);}); + O::Model integer;integer.add_integer(0,1);O::LpBasisData id;id.source=integer.snapshot();id.columns={B::Lower}; + invalid([&]{O::make_lp_basis(id);}); + O::LpBasisSolveOptions options;options.basis=basis; + O::SolveSession session; + auto missing=session.solve_lp_with_basis(model,{});assert(missing.observed.result.termination==O::Termination::InvalidModel); + assert(missing.submission.state==S::NotAttempted&&!missing.submission.backend_attempted); + auto starts=options;starts.observations.solve.primal_start={{x,4},{y,0}}; + assert(session.solve_lp_with_basis(model,starts).observed.result.termination==O::Termination::InvalidModel); + assert(session.statistics().solve_calls==0); + // Every compatible-content gate is checked even if an untrusted snapshot reuses revision. + for(int mode=0;mode<9;++mode){ + auto changed=model.snapshot(); + if(mode==0)++changed.revision; + if(mode==1)changed.variables[0].upper=10; + if(mode==2)changed.variables[0].name="new label"; + if(mode==3)changed.rows[0].lower=3; + if(mode==4)changed.rows[0].name="new row label"; + if(mode==5)changed.rows[0].terms[0].coefficient=2; + if(mode==6)changed.objective.offset=8; + if(mode==7)changed.objective.sense=O::ObjectiveSense::Maximize; + if(mode==8)changed.objective.terms[0].coefficient=1; + auto out=session.solve_lp_with_basis(changed,options); + assert(out.observed.result.termination==O::Termination::InvalidModel&&!out.submission.backend_attempted); + assert(out.requested_basis==basis&&session.statistics().solve_calls==0); + } + O::Model foreign;auto fx=foreign.add_continuous(),fy=foreign.add_continuous(); + foreign.add_row({{fx,1},{fy,1}},4,inf);foreign.minimize({{fx,2},{fy,3}},7); + assert(session.solve_lp_with_basis(foreign,options).observed.result.termination==O::Termination::InvalidModel); + for(auto backend:{O::Backend::Native}){ + auto unsupported=options;unsupported.observations.solve.backend=backend; + auto out=session.solve_lp_with_basis(model,unsupported);assert(out.observed.result.termination==O::Termination::Unsupported&&!out.submission.backend_attempted); + } + auto exact=options;exact.observations.solve.guarantee=O::Guarantee::Exact; + assert(session.solve_lp_with_basis(model,exact).observed.result.termination==O::Termination::Unsupported); + if(!O::lp_observation_capabilities().available){ + auto out=session.solve_lp_with_basis(model,options);assert(out.observed.result.termination==O::Termination::Unsupported); + assert(out.submission.state==S::NotAttempted&&!out.submission.backend_attempted&&out.requested_basis==basis); + std::cout<<"LP basis factory, source admission and missing-backend checks passed\n";return 0; + } + auto first=session.solve_lp_with_basis(model,options);optimal(first,15);assert(first.submission.state==S::Accepted); + assert(session.statistics().model_loads==1&&session.statistics().basis_warm_starts==0); + auto exported=O::make_lp_basis(*first.observed.observations);assert(exported->origin()==O::LpBasisOrigin::Observations); + options.basis=exported;optimal(session.solve_lp_with_basis(model,options),15); + optimal(O::solve_lp_with_basis(model,options),15); + // Round-trip original row statuses for max sense and ranged/free entities. + for(int scenario=0;scenario<3;++scenario){ + O::Model ranged;auto a=ranged.add_continuous(-inf,inf),fixed=ranged.add_continuous(2,2); + ranged.add_row({{a,1}},1,4); + if(scenario==0)ranged.maximize({{a,2},{fixed,1}},7); + else ranged.minimize({{a,scenario==1?1.0:-1.0},{fixed,1}},7); + const double objective[]={17,10,5}; + auto observed=O::solve_lp_observed(ranged); + assert(observed.observations->basis().state==O::LpObservationState::Available); + O::LpBasisSolveOptions ro;ro.basis=O::make_lp_basis(*observed.observations); + optimal(O::solve_lp_with_basis(ranged,ro),objective[scenario]); + } + // A legitimate nonoptimal all-logical basis is accepted before later pivots. + data.source=model.snapshot();data.columns={B::Lower,B::Lower};data.rows={B::Basic}; + options.basis=O::make_lp_basis(data);auto logical=session.solve_lp_with_basis(model,options);optimal(logical,15); + assert(logical.submission.state==S::Accepted&&!*logical.submission.statuses_changed); + assert(logical.observed.observations->columns()[0].basis==B::Basic); + // Count-correct singular structural basis: HiGHS repairs before optimization. + { + O::Model singular;auto a=singular.add_continuous(),b=singular.add_continuous(); + singular.add_row({{a,1},{b,1}},1,1);singular.add_row({{a,2},{b,2}},2,2); + singular.minimize({{a,1},{b,2}}); + O::LpBasisData sd;sd.source=singular.snapshot();sd.columns={B::Basic,B::Basic};sd.rows={B::Lower,B::Lower}; + O::LpBasisSolveOptions so;so.basis=O::make_lp_basis(sd); + auto repaired=session.solve_lp_with_basis(singular,so);optimal(repaired,1); + assert(repaired.submission.state==S::Repaired&&*repaired.submission.statuses_changed); + near(*session.solve(singular).objective,1); + optimal(session.solve_lp_with_basis(singular,so),1); + } + // Zero-row submitted statuses must never enter the pinned unsafe alien branch. + { + O::Model zero;auto a=zero.add_continuous(0,5),b=zero.add_continuous(-inf,inf),c=zero.add_continuous(-2,3); + zero.minimize({{a,1},{c,-1}},5); + O::LpBasisData zd;zd.source=zero.snapshot();zd.columns={B::Lower,B::Zero,B::Upper}; + O::LpBasisSolveOptions zo;zo.basis=O::make_lp_basis(zd); + auto out=O::solve_lp_with_basis(zero,zo);optimal(out,2);assert(out.submission.state==S::Accepted); + near(out.observed.result.value(b),0); + } + // Tombstones retain their slot but never carry a submitted status. + { + O::Model holes;auto removed=holes.add_continuous();holes.remove(removed);auto a=holes.add_continuous(); + auto deleted=holes.add_row({},-1,1);holes.remove(deleted);holes.add_row({{a,1}},2,inf);holes.minimize({{a,1}}); + O::LpBasisData hd;hd.source=holes.snapshot();hd.columns={std::nullopt,B::Basic};hd.rows={std::nullopt,B::Lower}; + O::LpBasisSolveOptions ho;ho.basis=O::make_lp_basis(hd);optimal(O::solve_lp_with_basis(holes,ho),2); + hd.columns[0]=B::Lower;invalid([&]{O::make_lp_basis(hd);}); + hd.columns[0].reset();hd.rows[0]=B::Basic;invalid([&]{O::make_lp_basis(hd);}); + } + options.basis=exported;auto zero=options;zero.observations.solve.time_limit_seconds=0; + auto limited=session.solve_lp_with_basis(model,zero);assert(limited.observed.result.termination==O::Termination::TimeLimit&&!limited.submission.backend_attempted); + auto cancel=options;cancel.observations.solve.cancellation=std::make_shared();cancel.observations.solve.cancellation->cancel(); + assert(session.solve_lp_with_basis(model,cancel).observed.result.termination==O::Termination::Cancelled); + optimal(session.solve_lp_with_basis(model,options),15); + auto p=options;p.observations.duals=p.observations.basis=false;auto primal=session.solve_lp_with_basis(model,p); + assert(primal.observed.result.has_solution()&&primal.observed.observations->basis().state==O::LpObservationState::NotRequested); + invalid([&]{O::make_lp_basis(*primal.observed.observations);}); + model.set_bounds(row,5,inf); + assert(session.solve_lp_with_basis(model,options).observed.result.termination==O::Termination::InvalidModel); + near(*session.solve(model).objective,17); + session.reset();assert(first.requested_basis->source().objective.offset==7); + assert(first.observed.observations->row(row).dual==2); + O::SolveSession moved(std::move(session)); + assert(session.solve_lp_with_basis(model,options).observed.result.termination==O::Termination::InvalidModel); + O::Model empty;empty.minimize({},7);O::LpBasisData ed;ed.source=empty.snapshot(); + O::LpBasisSolveOptions eo;eo.basis=O::make_lp_basis(ed); + auto empty_result=O::solve_lp_with_basis(empty,eo); + assert(empty_result.observed.result.termination==O::Termination::Optimal); + near(*empty_result.observed.result.objective,7); + assert(empty_result.requested_basis&&empty_result.submission.state==S::NotAttempted&&!empty_result.submission.backend_attempted); + std::shared_ptr detached; + { + O::Model temporary;auto a=temporary.add_continuous();temporary.add_row({{a,1}},2,inf); + temporary.minimize({{a,1}},1);O::LpBasisData td;td.source=temporary.snapshot(); + td.columns={B::Basic};td.rows={B::Lower};detached=O::make_lp_basis(td); + } + O::LpBasisSolveOptions detached_options;detached_options.basis=detached;detached.reset(); + auto retained=O::solve_lp_with_basis(detached_options.basis->source(),detached_options);optimal(retained,3); + detached_options.basis.reset();assert(retained.requested_basis->source().objective.offset==1); + std::cout<<"LP basis accepted/repaired, singular/zero-row, source and session conformance passed\n"; +} diff --git a/test/optimize/lp_basis_c_api.c b/test/optimize/lp_basis_c_api.c new file mode 100644 index 0000000000..ade44235e1 --- /dev/null +++ b/test/optimize/lp_basis_c_api.c @@ -0,0 +1,83 @@ +/* Compile this consumer as C99, then link the versioned shared C ABI. */ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +#define OK(call) do { int32_t code=(call); if(code)fprintf(stderr,"%s: %s\n",#call,gecode_opt_v1_last_error()); assert(code==GECODE_OPT_OK); } while(0) +int main(void) { + gecode_opt_handle model=0,basis=0,session=0,solved=0,observed=0,ordinary=0,copy=0,exported=0; + gecode_opt_id x,y,dead,row,deleted,foreign; + gecode_opt_term terms[2];int32_t columns[3]={1,0,-1},rows[2]={-1,0},status=-9; + gecode_opt_basis_info_v1 info;gecode_opt_basis_result_info_v1 result; + gecode_opt_lp_capabilities_v1 caps;gecode_opt_lp_options_v1 options; + uint64_t needed=0;int32_t small[2]={91,92}; + OK(gecode_opt_v1_lp_capabilities(&caps,sizeof(caps))); + OK(gecode_opt_v1_model_create(&model)); + OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_CONTINUOUS,0,INFINITY,"x",&x)); + OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_CONTINUOUS,0,INFINITY,"y",&y)); + OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_CONTINUOUS,0,1,"deleted",&dead)); + OK(gecode_opt_v1_model_remove_variable(model,dead)); + OK(gecode_opt_v1_model_add_row(model,NULL,0,-1,1,"deleted",&deleted)); + OK(gecode_opt_v1_model_remove_row(model,deleted)); + terms[0].variable=x;terms[0].coefficient=1;terms[1].variable=y;terms[1].coefficient=1; + OK(gecode_opt_v1_model_add_row(model,terms,2,4,INFINITY,"demand",&row)); + terms[0].coefficient=2;terms[1].coefficient=3; + OK(gecode_opt_v1_model_set_objective(model,terms,2,0,7)); + assert(gecode_opt_v1_basis_from_model(model,columns,3,rows,2,NULL)==GECODE_OPT_INVALID_ARGUMENT); + basis=99;assert(gecode_opt_v1_basis_from_model(model,NULL,3,rows,2,&basis)==GECODE_OPT_INVALID_ARGUMENT&&basis==0); + assert(gecode_opt_v1_basis_from_model(model,columns,UINT64_MAX,rows,2,&basis)==GECODE_OPT_INVALID_ARGUMENT); + assert(gecode_opt_v1_basis_from_model(model,columns,2,rows,2,&basis)==GECODE_OPT_MODEL_ERROR); + columns[0]=-2;assert(gecode_opt_v1_basis_from_model(model,columns,3,rows,2,&basis)==GECODE_OPT_INVALID_ARGUMENT); + columns[0]=1;columns[2]=0;assert(gecode_opt_v1_basis_from_model(model,columns,3,rows,2,&basis)==GECODE_OPT_MODEL_ERROR); + columns[2]=-1;OK(gecode_opt_v1_basis_from_model(model,columns,3,rows,2,&basis)); + OK(gecode_opt_v1_basis_info(basis,&info,sizeof(info))); + assert(info.struct_size==sizeof(info)&&info.reserved==0&&info.reserved_flags==0&&info.origin==GECODE_OPT_BASIS_CALLER); + assert(info.column_slots==3&&info.row_slots==2&&info.model_id==x.model_id); + assert(gecode_opt_v1_basis_info(basis,&info,sizeof(info)-1)==GECODE_OPT_INVALID_ARGUMENT); + assert(gecode_opt_v1_basis_info(model,&info,sizeof(info))==GECODE_OPT_INVALID_HANDLE); + assert(gecode_opt_v1_basis_statuses(basis,99,NULL,0,&needed)==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_basis_statuses(basis,GECODE_OPT_VARIABLE_ID,NULL,0,&needed));assert(needed==3); + assert(gecode_opt_v1_basis_statuses(basis,GECODE_OPT_VARIABLE_ID,small,2,&needed)==GECODE_OPT_BUFFER_TOO_SMALL); + assert(small[0]==91&&small[1]==92&&needed==3); + OK(gecode_opt_v1_basis_column(basis,x,&status));assert(status==GECODE_OPT_LP_BASIS_BASIC); + OK(gecode_opt_v1_basis_row(basis,row,&status));assert(status==GECODE_OPT_LP_BASIS_LOWER); + assert(gecode_opt_v1_basis_column(basis,dead,&status)==GECODE_OPT_MODEL_ERROR); + assert(gecode_opt_v1_basis_row(basis,deleted,&status)==GECODE_OPT_MODEL_ERROR); + foreign=x;foreign.model_id++;assert(gecode_opt_v1_basis_column(basis,foreign,&status)==GECODE_OPT_MODEL_ERROR); + foreign=x;foreign.reserved=1;assert(gecode_opt_v1_basis_column(basis,foreign,&status)==GECODE_OPT_INVALID_ARGUMENT); + assert(gecode_opt_v1_basis_row(basis,x,&status)==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_session_create(&session));OK(gecode_opt_v1_lp_options_default(&options,sizeof(options))); + options.reserved=1;assert(gecode_opt_v1_solve_lp_with_basis(model,basis,&options,&solved)==GECODE_OPT_INVALID_ARGUMENT&&solved==0);options.reserved=0; + options.solve.reserved=1;assert(gecode_opt_v1_solve_lp_with_basis(model,basis,&options,&solved)==GECODE_OPT_INVALID_ARGUMENT);options.solve.reserved=0; + assert(gecode_opt_v1_solve_lp_with_basis(model,model,&options,&solved)==GECODE_OPT_INVALID_HANDLE); + OK(gecode_opt_v1_session_solve_lp_with_basis(session,model,basis,&options,&solved)); + OK(gecode_opt_v1_basis_result_info(solved,&result,sizeof(result))); + assert(result.struct_size==sizeof(result)&&result.reserved==0&&result.reserved_flags==0&&result.result.reserved==0); + assert(result.has_requested_basis&&result.requested_model_id==x.model_id); + assert(result.result.termination==(caps.available?GECODE_OPT_OPTIMAL:GECODE_OPT_UNSUPPORTED)); + assert(result.state==(caps.available?GECODE_OPT_BASIS_ACCEPTED:GECODE_OPT_BASIS_NOT_ATTEMPTED)); + assert(result.backend_attempted==caps.available&&result.has_statuses_changed==caps.available&&!result.statuses_changed); + assert(gecode_opt_v1_basis_result_info(basis,&result,sizeof(result))==GECODE_OPT_INVALID_HANDLE); + OK(gecode_opt_v1_basis_result_message(solved,NULL,0,&needed));assert(needed>0); + {char text[4096];assert(needed<=sizeof(text));OK(gecode_opt_v1_basis_result_message(solved,text,sizeof(text),&needed));assert(text[needed-1]=='\0');} + OK(gecode_opt_v1_basis_result_copy_observed(solved,&observed)); + OK(gecode_opt_v1_basis_result_copy_basis(solved,©)); + OK(gecode_opt_v1_lp_observed_result_copy_result(observed,&ordinary)); + if(caps.available) {OK(gecode_opt_v1_basis_from_observed(observed,&exported));OK(gecode_opt_v1_basis_info(exported,&info,sizeof(info)));assert(info.origin==GECODE_OPT_BASIS_OBSERVATIONS);} + else assert(gecode_opt_v1_basis_from_observed(observed,&exported)==GECODE_OPT_MODEL_ERROR&&exported==0); + OK(gecode_opt_v1_basis_result_destroy(solved));OK(gecode_opt_v1_basis_destroy(basis)); + assert(gecode_opt_v1_basis_info(basis,&info,sizeof(info))==GECODE_OPT_INVALID_HANDLE); + OK(gecode_opt_v1_lp_observed_result_destroy(observed)); + OK(gecode_opt_v1_model_set_row_bounds(model,row,5,INFINITY)); + OK(gecode_opt_v1_session_solve_lp_with_basis(session,model,copy,NULL,&solved)); + OK(gecode_opt_v1_basis_result_info(solved,&result,sizeof(result)));assert(result.result.termination==GECODE_OPT_INVALID_MODEL&&!result.backend_attempted); + OK(gecode_opt_v1_model_destroy(model));OK(gecode_opt_v1_session_destroy(session)); + OK(gecode_opt_v1_basis_column(copy,x,&status));assert(status==1); + if(caps.available) {int32_t present=0;double value=0;OK(gecode_opt_v1_result_number(ordinary,GECODE_OPT_OBJECTIVE,&present,&value));assert(present&&fabs(value-15)<1e-7);OK(gecode_opt_v1_result_value(ordinary,x,&value));assert(value==4);OK(gecode_opt_v1_basis_destroy(exported));} + OK(gecode_opt_v1_result_destroy(ordinary));OK(gecode_opt_v1_basis_result_destroy(solved));OK(gecode_opt_v1_basis_destroy(copy)); + puts("C99 owning basis, submission, buffers, source identity and lifetime conformance passed");return 0; +} diff --git a/test/optimize/lp_basis_failure.cpp b/test/optimize/lp_basis_failure.cpp new file mode 100644 index 0000000000..09c4f49c82 --- /dev/null +++ b/test/optimize/lp_basis_failure.cpp @@ -0,0 +1,62 @@ +// Link against a separately compiled solve.cpp with +// GECODE_OPTIMIZE_TEST_LP_BASIS_FAILURE=1, and real HiGHS. The seam injects +// rejection/cancellation only AFTER real setBasis has modified backend state. +#include +#include +#include +#include +#include +#include +namespace O=Gecode::Optimize; +using B=O::LpBasisStatus; +using S=O::LpBasisSubmissionState; +static bool inject_cancellation=false; +namespace Gecode { namespace Optimize { namespace Detail { +bool lp_basis_test_cancel() noexcept { return inject_cancellation; } +}}} +static void value(const O::SolveResult& result,double objective){ + assert(result.termination==O::Termination::Optimal&&result.has_solution()); + assert(std::abs(*result.objective-objective)<1e-7); +} +int main(){ + assert(O::lp_observation_capabilities().available); // Required real numerical backend. + const double inf=std::numeric_limits::infinity(); + O::Model model;auto x=model.add_continuous(),y=model.add_continuous(); + model.add_row({{x,1},{y,1}},4,inf);model.minimize({{x,2},{y,3}},7); + O::LpBasisData data;data.source=model.snapshot();data.columns={B::Basic,B::Lower};data.rows={B::Lower}; + O::LpBasisSolveOptions options;options.basis=O::make_lp_basis(data); + O::SolveSession session; + auto historical=session.solve_lp_observed(model);value(historical.result,15); + const auto initial_loads=session.statistics().model_loads; + auto rejected=session.solve_lp_with_basis(model,options); + assert(rejected.submission.backend_attempted&&rejected.submission.state==S::Rejected); + assert(!rejected.submission.statuses_changed&&!rejected.observed.result.has_solution()); + assert(rejected.observed.result.termination==O::Termination::BackendError); + assert(rejected.observed.observations->basis().state!=O::LpObservationState::Available); + // No fallback solve occurred. The next call must load a clean backend. + assert(session.statistics().model_loads==initial_loads); + value(session.solve(model),15);assert(session.statistics().model_loads==initial_loads+1); + assert(historical.observations->checks().accepted&&historical.observations->rows()[0].dual==2); + // Cancellation after actual factor/repair has completed also clears callbacks + // and disposes the backend before the caller can reuse the session. + options.observations.solve.cancellation=std::make_shared(); + inject_cancellation=true; + const auto before=session.statistics().model_loads; + auto cancelled=session.solve_lp_with_basis(model,options); + assert(cancelled.submission.backend_attempted&&cancelled.submission.state==S::Interrupted); + assert(!cancelled.submission.statuses_changed); + assert(cancelled.observed.result.termination==O::Termination::Cancelled); + assert(cancelled.observed.observations->dual_point().reason==O::LpObservationReason::Interrupted); + value(session.solve(model),15);assert(session.statistics().model_loads==before+1); + // The zero-row safe submission path has the same cleanup contract. + O::Model zero;auto z=zero.add_continuous(0,5);zero.minimize({{z,-1}},2); + O::LpBasisData zd;zd.source=zero.snapshot();zd.columns={B::Upper}; + options={};options.basis=O::make_lp_basis(zd); + inject_cancellation=false; + auto zero_failure=session.solve_lp_with_basis(zero,options); + assert(zero_failure.submission.backend_attempted&&zero_failure.submission.state==S::Rejected); + assert(zero_failure.observed.result.termination==O::Termination::BackendError); + const auto zero_loads=session.statistics().model_loads; + value(session.solve(zero),-3);assert(session.statistics().model_loads==zero_loads+1); + std::cout<<"LP basis post-real-submission rejection/cancellation cleanup passed\n"; +} diff --git a/test/optimize/lp_certificate.cpp b/test/optimize/lp_certificate.cpp new file mode 100644 index 0000000000..8219165dc8 --- /dev/null +++ b/test/optimize/lp_certificate.cpp @@ -0,0 +1,179 @@ +// Independent correctness checks; no Gecode or LP library is needed to link. +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Cert = Gecode::Experimental::LpCertificate; +using I = std::int64_t; +using V = std::vector; + +static void require(bool condition, const char* message) { + if (!condition) + throw std::runtime_error(message); +} + +static void expect_bound(const V& A, const V& b, const V& c, + const V& lower, const V& upper, + const std::vector& duals, I expected) { + I result=987654321; + require(Cert::lower_bound(A,b,c,lower,upper,duals,result), + "expected a valid certificate"); + require(result==expected,"incorrect certified lower bound"); +} + +static void expect_rejected(const V& A, const V& b, const V& c, + const V& lower, const V& upper, + const std::vector& duals) { + I result=987654321; + require(!Cert::lower_bound(A,b,c,lower,upper,duals,result), + "invalid or overflowing certificate was accepted"); + require(result==987654321,"failure changed the output"); +} + +// Exhaustively compute the true minimum without using the certificate formula. +static bool brute_min(const V& A, const V& b, const V& c, + const V& lower, const V& upper, I& optimum) { + bool found=false; + const std::size_t n=c.size(); + for (unsigned int mask=0; mask<(1U<>j)&1U; + if (xupper[j]) + feasible=false; + } + for (std::size_t i=0; feasible && i>j)&1U); + if (sum>j)&1U); + if (!found || objective::denorm_min()},0); + + std::vector quantized={123}; + require(Cert::quantize({0.5,-2.0,0.0},quantized), + "finite multiplier conversion failed"); + require(quantized==V({Cert::scale/2,0,0}),"wrong multiplier conversion"); + const V saved=quantized; + require(!Cert::quantize({std::numeric_limits::quiet_NaN()},quantized), + "NaN multiplier accepted"); + require(quantized==saved,"failed quantization changed the output"); + + for (double bad : {std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + std::ldexp(1.0,43), + std::numeric_limits::max()}) + expect_rejected({1},{1},{1},{0},{1},{bad}); + expect_rejected({}, {1}, {1}, {0}, {1}, {0.5}); // Matrix dimensions + expect_rejected({1}, {1}, {1}, {}, {1}, {0.5}); // Bounds dimensions + expect_rejected({1}, {1}, {1}, {0}, {1}, {}); // Dual dimensions + expect_rejected({1}, {1}, {1}, {-1}, {1}, {0}); + expect_rejected({1}, {1}, {1}, {1}, {0}, {0}); + expect_rejected({1}, {1}, {1}, {0}, {2}, {0}); + const I largest=std::numeric_limits::max(); + const I smallest=std::numeric_limits::min(); + // Exact result overflow, reduced-cost accumulation overflow, y*b overflow. + expect_rejected({}, {}, {largest,largest}, {1,1}, {1,1}, {}); + expect_rejected({}, {}, {smallest,smallest}, {1,1}, {1,1}, {}); + expect_rejected(V(5,largest),V(5,0),{0},{0},{1}, + std::vector(5,std::ldexp(1.0,42))); + expect_rejected(V(5,0),V(5,largest),{0},{0},{1}, + std::vector(5,std::ldexp(1.0,42))); + expect_bound({}, {}, {largest}, {1}, {1}, {}, largest); + expect_bound({}, {}, {smallest}, {1}, {1}, {}, smallest); + + std::mt19937 random(712367); + unsigned int feasible_cases=0; + unsigned int infeasible_cases=0; + const unsigned int trials=3000; + for (unsigned int trial=0; trial duals(m); + for (std::size_t j=0; j(random()%23)-11; + } + for (std::size_t i=0; i(random()%15)-7; + at_witness+=A[i*n+j]*witness[j]; + } + // Half the models are constructed with a known feasible witness. + b[i]=(trial%2==0) ? at_witness-static_cast(random()%5) + : static_cast(random()%31)-15; + // Deliberately arbitrary candidate duals, including wrong signs. + duals[i]=(static_cast(random()%81)-20)/7.0; + } + I bound=0; + require(Cert::lower_bound(A,b,c,lower,upper,duals,bound), + "small valid arithmetic was rejected"); + I optimum=0; + if (brute_min(A,b,c,lower,upper,optimum)) { + ++feasible_cases; + require(bound<=optimum,"certificate exceeds the true integer optimum"); + } else { + ++infeasible_cases; + } + } + require(feasible_cases>=trials/2,"random witness construction failed"); + require(infeasible_cases>0,"infeasible random models were not exercised"); + std::cout << "PASS certificate edge cases and " << trials + << " exhaustive randomized models (" << feasible_cases + << " feasible, " << infeasible_cases << " infeasible)\n"; + return EXIT_SUCCESS; + } catch (const std::exception& error) { + std::cerr << "FAIL: " << error.what() << '\n'; + return EXIT_FAILURE; + } +} diff --git a/test/optimize/lp_evidence.cpp b/test/optimize/lp_evidence.cpp new file mode 100644 index 0000000000..1b554b5d6e --- /dev/null +++ b/test/optimize/lp_evidence.cpp @@ -0,0 +1,115 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +#include +#include +namespace O=Gecode::Optimize; +using State=O::LpEvidenceState;using Request=O::LpEvidenceRequest; +constexpr double inf=std::numeric_limits::infinity(); +static void near(double a,double b){assert(std::isfinite(a)&&std::abs(a-b)<1e-7);} +templatestatic void invalid(F fn){bool caught=false;try{fn();}catch(const O::ModelError&){caught=true;}assert(caught);} +static const O::LpEvidence& good(const O::LpEvidenceResult& r){ + if(r.completion!=O::LpEvidenceCompletion::Complete)std::cerr<stages()){ + assert(stage.auxiliary_model&&stage.columns.size()==stage.auxiliary_model->variables.size()); + if(stage.phase!=O::LpEvidencePhase::FeasibleBase){ + std::vector zero(stage.columns.size(),0); + assert(O::validate(*stage.auxiliary_model,zero,0,0).valid); + } + if(stage.attempted){assert(stage.auxiliary_result); + assert(stage.auxiliary_model->model_id!=r.model_id&&stage.auxiliary_result->model_id==stage.auxiliary_model->model_id); + assert(stage.auxiliary_result->revision==stage.auxiliary_model->revision&&stage.auxiliary_result->guarantee==O::Guarantee::Numerical);} + } + return *r.evidence; +} +int main(){ + O::Model basic;auto x=basic.add_continuous();basic.minimize({{x,-1}},7); + for(auto request:{Request::Automatic,Request::PrimalRay,Request::Farkas,Request::Both}){ + O::LpEvidenceOptions options;options.request=request;auto out=O::analyze_lp_evidence(basic,options); + if(!O::capabilities(O::Backend::Highs).available){assert(out.stop_reason==O::Termination::Unsupported&&out.attempted_calls==0&&out.evidence);continue;} + const auto& e=good(out);assert(out.attempted_calls==(request==Request::Farkas?1:request==Request::Both?3:2)); + if(request!=Request::Farkas){assert(e.primal_ray().state==State::Available);near(e.base_value(x),0);near(e.direction_value(x),1);near(*e.primal_data().normalized_objective_slope,-1);} + if(request!=Request::PrimalRay)assert(e.farkas().state==State::Unavailable); + } + O::LpEvidenceOptions options; + for(int mode=0;mode<7;++mode){auto wrong=options; + if(mode==0)wrong.request=static_cast(99); + if(mode==1)wrong.checks.recession=-1; + if(mode==2)wrong.checks.stationarity=inf; + if(mode==3)wrong.checks.minimum_contradiction=std::numeric_limits::quiet_NaN(); + if(mode==4)wrong.solve.backend=O::Backend::Native; + if(mode==5)wrong.solve.guarantee=O::Guarantee::Exact; + if(mode==6)wrong.solve.primal_start={{x,0}}; + auto out=O::analyze_lp_evidence(basic,wrong);assert(out.attempted_calls==0&&out.completion==O::LpEvidenceCompletion::Rejected); + } + auto invalid_source=basic.snapshot();invalid_source.variables[0].variable.model_id=0; + assert(O::analyze_lp_evidence(invalid_source).stop_reason==O::Termination::InvalidModel); + {O::Model discrete;discrete.add_integer(0,0);assert(O::analyze_lp_evidence(discrete).stop_reason==O::Termination::Unsupported);} + {O::Model global;O::add_all_different(global,{});assert(O::analyze_lp_evidence(global).stop_reason==O::Termination::Unsupported);} + for(int stop=0;stop<6;++stop){auto limited=options; + if(stop==0)limited.solve.time_limit_seconds=0; + if(stop==1){limited.solve.cancellation=std::make_shared();limited.solve.cancellation->cancel();} + if(stop==2)limited.limits.max_work=0; + if(stop==3)limited.limits.max_auxiliary_variables=0; + if(stop==4)limited.limits.max_retained_slots=0; + if(stop==5)limited.solve.node_limit=0; + auto out=O::analyze_lp_evidence(basic,limited);assert(out.attempted_calls==0&&out.stop_reason); + } + if(!O::capabilities(O::Backend::Highs).available){std::cout<<"LP evidence core admission and missing backend passed\n";return 0;} + for(bool maximize:{false,true})for(bool upper:{false,true}){ + O::Model m;auto v=m.add_continuous(upper?-inf:0,upper?0:inf); + const double coefficient=(upper?1:-1)*(maximize?-1:1); + m.set_objective({{v,coefficient}},maximize?O::ObjectiveSense::Maximize:O::ObjectiveSense::Minimize,1e16); + auto out=O::analyze_lp_evidence(m);const auto& e=good(out);assert(e.primal_ray().state==State::Available); + near(e.direction_value(v),upper?-1:1);near(*e.primal_data().normalized_objective_slope,-1); + } + { + O::Model m;auto a=m.add_continuous(-inf,inf),b=m.add_continuous(-inf,inf),fixed=m.add_continuous(2,2); + auto equality=m.add_row({{a,1},{b,-1}},0,0);auto ranged=m.add_row({{fixed,1}},1,3);m.minimize({{a,-1}},100); + auto out=O::analyze_lp_evidence(m);const auto& e=good(out);near(e.direction_value(a),1);near(e.direction_value(b),1);near(e.direction_value(fixed),0); + near(e.primal_data().row_direction[equality.id],0);near(e.primal_data().row_direction[ranged.id],0); + } + for(bool upper:{false,true})for(bool maximize:{false,true}){ + O::Model m;auto v=m.add_continuous(upper?0:-inf,upper?inf:0); + auto r=m.add_row({{v,1}},upper?-inf:1,upper?-1:inf);m.set_objective({{v,1}},maximize?O::ObjectiveSense::Maximize:O::ObjectiveSense::Minimize,7); + auto out=O::analyze_lp_evidence(m);const auto& e=good(out);assert(e.farkas().state==State::Available&&e.primal_ray().state==State::Unavailable); + near(e.row_multiplier(r).multiplier,upper?-1:1);near(e.column_multiplier(v).multiplier,upper?1:-1);near(*e.farkas_data().contradiction_margin,1); + assert(e.row_multiplier(r).side==(upper?O::LpEvidenceSide::Upper:O::LpEvidenceSide::Lower)); + assert(out.attempted_calls==2&&!e.stages()[1].attempted); + } + { + O::Model m;auto v=m.add_continuous(-inf,inf);auto lower=m.add_row({{v,1}},1,inf);auto upper=m.add_row({{v,1}},-inf,0); + O::LpEvidenceOptions o;o.request=Request::Farkas;auto out=O::analyze_lp_evidence(m,o);const auto& e=good(out); + near(e.row_multiplier(lower).multiplier,1);near(e.row_multiplier(upper).multiplier,-1);near(e.column_multiplier(v).multiplier,0); + assert(!e.column_multiplier(v).side);near(*e.farkas_data().contradiction_margin,1); + } + { + O::Model m;auto r=m.add_row({},-inf,-2);auto out=O::analyze_lp_evidence(m);const auto& e=good(out); + assert(e.farkas().state==State::Available&&e.farkas_data().columns.empty());near(e.row_multiplier(r).multiplier,-1);near(*e.farkas_data().contradiction_margin,2); + } + { + O::Model m;auto v=m.add_continuous(0,0);m.add_row({{v,1}},1,1);auto out=O::analyze_lp_evidence(m);const auto& e=good(out); + assert(e.farkas().state==State::Available);near(e.column_multiplier(v).multiplier,-1); + } + for(bool empty:{false,true}){ + O::Model m;if(!empty){auto v=m.add_continuous(0,1);m.minimize({{v,1}});}O::LpEvidenceOptions o;o.request=Request::Both; + auto out=O::analyze_lp_evidence(m,o);const auto& e=good(out);assert(e.primal_ray().state==State::Unavailable&&e.farkas().state==State::Unavailable&&out.attempted_calls==3); + } + { + O::Model m;auto v=m.add_continuous(0,0);auto u=m.add_continuous(0,inf);m.add_row({{v,1}},1,inf);m.minimize({{u,-1}}); + auto out=O::analyze_lp_evidence(m);const auto& e=good(out);assert(e.primal_ray().state==State::Unavailable&&e.primal_data().direction.empty()&&e.farkas().state==State::Available); + } + O::LpEvidenceResult historical;O::Variable live,dead;O::Constraint removed; + {O::Model m;dead=m.add_continuous();m.remove(dead);live=m.add_continuous();removed=m.add_row({},-1,1);m.remove(removed);m.minimize({{live,-1}}); + historical=O::analyze_lp_evidence(m);m.maximize({{live,1}},9);} + const auto& h=good(historical);near(h.direction_value(live),1);assert(std::isnan(h.primal_data().direction[dead.id])); + invalid([&]{h.direction_value(dead);});invalid([&]{h.direction_value({live.model_id+1,live.id});});invalid([&]{h.row_multiplier(removed);}); + options.limits.max_auxiliary_solves=1;auto capped=O::analyze_lp_evidence(basic,options);assert(capped.stop_reason==O::Termination::IterationLimit&&capped.attempted_calls==1&&capped.evidence->primal_ray().state!=State::Available); + std::cout<<"LP evidence original signs, bounds, rays, Farkas, zero rows, identities and limits passed\n"; +} diff --git a/test/optimize/lp_evidence_binding_cleanup.cpp b/test/optimize/lp_evidence_binding_cleanup.cpp new file mode 100644 index 0000000000..b4653c9cd5 --- /dev/null +++ b/test/optimize/lp_evidence_binding_cleanup.cpp @@ -0,0 +1,52 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +static gecode_opt_handle cancellation=0; +extern "C" void gecode_opt_test_evidence_binding_checkpoint(void){ + if(cancellation)assert(gecode_opt_v1_cancellation_cancel(cancellation)==GECODE_OPT_OK); +} +static void ok(int32_t code){if(code)std::cerr<::infinity(); + for(bool farkas:{false,true}){ + gecode_opt_handle model=0,analysis=0,child=0;gecode_opt_id x{},row{}; + ok(gecode_opt_v1_model_create(&model));ok(gecode_opt_v1_model_add_variable(model,GECODE_OPT_CONTINUOUS,farkas?-inf:0,farkas?0:inf,"x",&x)); + gecode_opt_term t{x,farkas?1.0:-1.0}; + if(farkas)ok(gecode_opt_v1_model_add_row(model,&t,1,1,inf,"contradiction",&row)); + else ok(gecode_opt_v1_model_set_objective(model,&t,1,GECODE_OPT_MINIMIZE,7)); + gecode_opt_evidence_options_v1 options{};ok(gecode_opt_v1_evidence_options_default(&options,sizeof(options))); + options.request=farkas?GECODE_OPT_EVIDENCE_FARKAS:GECODE_OPT_EVIDENCE_PRIMAL_RAY; + ok(gecode_opt_v1_analyze_lp_evidence(model,&options,&analysis)); + gecode_opt_evidence_group_v1 group{}; + ok(gecode_opt_v1_lp_evidence_group(analysis,farkas?1:0,&group,sizeof(group)));assert(group.state==GECODE_OPT_EVIDENCE_AVAILABLE); + ok(gecode_opt_v1_lp_evidence_destroy(analysis)); + ok(gecode_opt_v1_cancellation_create(&cancellation));options.solve.cancellation=cancellation; + ok(gecode_opt_v1_analyze_lp_evidence(model,&options,&analysis)); + gecode_opt_evidence_info_v1 info{};ok(gecode_opt_v1_lp_evidence_info(analysis,&info,sizeof(info))); + assert(info.has_evidence&&info.completion==GECODE_OPT_EVIDENCE_INTERRUPTED&&info.has_stop_reason&&info.stop_reason==GECODE_OPT_CANCELLED); + ok(gecode_opt_v1_lp_evidence_group(analysis,farkas?1:0,&group,sizeof(group))); + assert(group.state==GECODE_OPT_EVIDENCE_UNAVAILABLE&&group.reason==GECODE_OPT_EVIDENCE_REASON_STOPPED); + ok(gecode_opt_v1_lp_evidence_group(analysis,farkas?0:1,&group,sizeof(group)));assert(group.state==GECODE_OPT_EVIDENCE_NOT_REQUESTED); + double value=0;gecode_opt_evidence_slot_v1 slot{}; + assert(gecode_opt_v1_lp_evidence_value(analysis,x,GECODE_OPT_EVIDENCE_BASE_VALUE,&value)==GECODE_OPT_NO_EVIDENCE); + assert(gecode_opt_v1_lp_evidence_value(analysis,x,GECODE_OPT_EVIDENCE_DIRECTION_VALUE,&value)==GECODE_OPT_NO_EVIDENCE); + assert(gecode_opt_v1_lp_evidence_multiplier(analysis,farkas?row:x,&slot,sizeof(slot))==GECODE_OPT_NO_EVIDENCE); + ok(gecode_opt_v1_lp_evidence_slot(analysis,farkas?row:x,&slot,sizeof(slot))); + assert(farkas?slot.multiplier.present:slot.direction.present); + ok(gecode_opt_v1_lp_evidence_copy_stage(analysis,farkas?0:1,&child)); + ok(gecode_opt_v1_lp_evidence_destroy(analysis));ok(gecode_opt_v1_model_destroy(model));ok(gecode_opt_v1_cancellation_destroy(cancellation));cancellation=0; + gecode_opt_evidence_stage_v1 stage{};ok(gecode_opt_v1_lp_evidence_stage_info(child,&stage,sizeof(stage))); + assert(stage.attempted&&stage.candidate_examined&&stage.check.valid&&stage.has_raw_result); + assert(stage.raw_result.reported_solution_validated&&stage.raw_result.termination_code==GECODE_OPT_OPTIMAL); + gecode_opt_result_info_v1 ordinary{};assert(gecode_opt_v1_result_info(child,&ordinary,sizeof(ordinary))==GECODE_OPT_INVALID_HANDLE); + ok(gecode_opt_v1_lp_evidence_stage_destroy(child)); + } + std::cout<<"LP evidence binding final cancellation revokes accepted getters; raw child history survives\n"; +} diff --git a/test/optimize/lp_evidence_c.c b/test/optimize/lp_evidence_c.c new file mode 100644 index 0000000000..a6b227639e --- /dev/null +++ b/test/optimize/lp_evidence_c.c @@ -0,0 +1,123 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +#include +#define OK(call) do { int32_t code_=(call); if(code_){fprintf(stderr,"%s: %d %s\n",#call,(int)code_,gecode_opt_v1_last_error());assert(code_==0);} } while(0) +static int32_t available; +static void near(double x,double y){assert(isfinite(x)&&fabs(x-y)<1e-7);} +static gecode_opt_handle analyze(gecode_opt_handle m,const gecode_opt_evidence_options_v1* options){ + gecode_opt_handle h=0;OK(gecode_opt_v1_analyze_lp_evidence(m,options,&h));assert(h);return h; +} +static void ray(void){ + gecode_opt_handle m=0,out=0,child=0;gecode_opt_id dead,x,y,row,gone,bad; + gecode_opt_term terms[2];gecode_opt_evidence_options_v1 options;gecode_opt_evidence_info_v1 info; + gecode_opt_evidence_group_v1 group;gecode_opt_evidence_primal_v1 primal;gecode_opt_evidence_slot_v1 slot; + gecode_opt_evidence_stage_v1 stage;gecode_opt_evidence_column_v1 map[2];gecode_opt_evidence_raw_value_v1 raw[2]; + gecode_opt_result_info_v1 ordinary;uint64_t owner,revision,needed;double value; + OK(gecode_opt_v1_model_create(&m));OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_CONTINUOUS,0,INFINITY,"dead",&dead)); + OK(gecode_opt_v1_model_remove_variable(m,dead));OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_CONTINUOUS,0,INFINITY,"x",&x)); + OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_CONTINUOUS,-INFINITY,INFINITY,"y",&y)); + OK(gecode_opt_v1_model_add_row(m,NULL,0,-INFINITY,INFINITY,"gone",&gone));OK(gecode_opt_v1_model_remove_row(m,gone)); + terms[0].variable=x;terms[0].coefficient=1;terms[1].variable=y;terms[1].coefficient=-1; + OK(gecode_opt_v1_model_add_row(m,terms,2,0,0,"same",&row));terms[0].coefficient=-1; + OK(gecode_opt_v1_model_set_objective(m,terms,1,GECODE_OPT_MINIMIZE,7));OK(gecode_opt_v1_model_identity(m,&owner,&revision)); + OK(gecode_opt_v1_evidence_options_default(&options,sizeof(options)));options.request=GECODE_OPT_EVIDENCE_BOTH; + assert(!options.reserved&&!options.reserved_flags&&options.max_auxiliary_solves==3); + out=analyze(m,&options);memset(&info,0xa5,sizeof(info));OK(gecode_opt_v1_lp_evidence_info(out,&info,sizeof(info))); + assert(!info.reserved&&info.model_id==owner&&info.revision==revision&&info.has_evidence&&info.stage_count==3); + assert(info.row_slots==2&&info.column_slots==3);OK(gecode_opt_v1_lp_evidence_group(out,GECODE_OPT_EVIDENCE_PRIMAL_GROUP,&group,sizeof(group))); + assert(group.state==(available?GECODE_OPT_EVIDENCE_AVAILABLE:GECODE_OPT_EVIDENCE_REJECTED)); + if(available){assert(info.completion==GECODE_OPT_EVIDENCE_COMPLETE&&!info.has_stop_reason&&info.attempted_calls==3); + OK(gecode_opt_v1_lp_evidence_value(out,x,GECODE_OPT_EVIDENCE_DIRECTION_VALUE,&value));near(value,1); + OK(gecode_opt_v1_lp_evidence_value(out,y,GECODE_OPT_EVIDENCE_DIRECTION_VALUE,&value));near(value,1); + OK(gecode_opt_v1_lp_evidence_value(out,x,GECODE_OPT_EVIDENCE_BASE_VALUE,&value));near(value,0); + }else{assert(info.stop_reason==GECODE_OPT_UNSUPPORTED&&!info.attempted_calls); + assert(gecode_opt_v1_lp_evidence_value(out,x,GECODE_OPT_EVIDENCE_DIRECTION_VALUE,&value)==GECODE_OPT_NO_EVIDENCE);} + OK(gecode_opt_v1_lp_evidence_primal(out,&primal,sizeof(primal)));assert(!primal.reserved&&!primal.reserved_flags); + assert(primal.has_base_check==available&&primal.normalized_objective_slope.present==available); + if(available){assert(primal.base_check.valid);near(primal.normalized_objective_slope.value,-1);} + bad=x;bad.model_id++;assert(gecode_opt_v1_lp_evidence_slot(out,bad,&slot,sizeof(slot))==GECODE_OPT_MODEL_ERROR); + assert(gecode_opt_v1_lp_evidence_slot(out,dead,&slot,sizeof(slot))==GECODE_OPT_MODEL_ERROR); + assert(gecode_opt_v1_lp_evidence_slot(out,gone,&slot,sizeof(slot))==GECODE_OPT_MODEL_ERROR); + OK(gecode_opt_v1_lp_evidence_slot(out,row,&slot,sizeof(slot)));assert(slot.active&&slot.source.kind==GECODE_OPT_ROW_ID&&!slot.base_value.present); + if(available){assert(slot.direction.present);near(slot.direction.value,0);} + {gecode_opt_evidence_slot_v1 slots[3];OK(gecode_opt_v1_lp_evidence_slots(out,GECODE_OPT_VARIABLE_ID,slots,sizeof(slots[0]),3,&needed)); + assert(needed==3&&!slots[0].active&&!slots[0].direction.present&&!slots[0].base_value.present&&slots[0].direction.value==0); + assert(!slots[1].reserved&&!slots[1].reserved_flags);} + memset(&slot,0xa5,sizeof(slot));assert(gecode_opt_v1_lp_evidence_slots(out,GECODE_OPT_VARIABLE_ID,&slot,sizeof(slot),1,&needed)==GECODE_OPT_BUFFER_TOO_SMALL); + assert(needed==3&&slot.struct_size==UINT64_C(0xa5a5a5a5a5a5a5a5)); + assert(gecode_opt_v1_lp_evidence_slots(out,GECODE_OPT_VARIABLE_ID,NULL,sizeof(slot)-1,0,&needed)==GECODE_OPT_INVALID_ARGUMENT); + assert(gecode_opt_v1_lp_evidence_slots(out,99,NULL,sizeof(slot),0,&needed)==GECODE_OPT_INVALID_ARGUMENT); + assert(gecode_opt_v1_lp_evidence_info(out,&info,sizeof(info)-1)==GECODE_OPT_INVALID_ARGUMENT); + assert(gecode_opt_v1_result_info(out,&ordinary,sizeof(ordinary))==GECODE_OPT_INVALID_HANDLE); + OK(gecode_opt_v1_lp_evidence_copy_stage(out,1,&child));OK(gecode_opt_v1_lp_evidence_stage_info(child,&stage,sizeof(stage))); + assert(stage.phase==GECODE_OPT_EVIDENCE_RECESSION&&stage.private_model_id!=owner&&stage.column_count==2); + assert(stage.attempted==available&&stage.has_raw_result==available&&stage.candidate_examined==available); + if(available){assert(stage.check.valid&&stage.raw_result.reported_solution_validated);near(stage.raw_result.objective.value,-1); + assert(stage.raw_result.model_id==stage.private_model_id&&stage.raw_result.revision==stage.private_revision);} + assert(gecode_opt_v1_result_info(child,&ordinary,sizeof(ordinary))==GECODE_OPT_INVALID_HANDLE); + assert(gecode_opt_v1_lp_evidence_stage_info(out,&stage,sizeof(stage))==GECODE_OPT_INVALID_HANDLE); + OK(gecode_opt_v1_lp_evidence_stage_columns(child,map,sizeof(map[0]),2,&needed));assert(needed==2); + assert(map[0].source.model_id==owner&&map[0].source.slot==x.slot&&map[0].private_variable.model_id!=owner&&!map[0].has_side); + OK(gecode_opt_v1_model_set_objective_offset(m,123));OK(gecode_opt_v1_model_destroy(m));OK(gecode_opt_v1_lp_evidence_destroy(out)); + OK(gecode_opt_v1_lp_evidence_stage_info(child,&stage,sizeof(stage)));assert(stage.attempted==available); + OK(gecode_opt_v1_lp_evidence_stage_raw_values(child,raw,sizeof(raw[0]),2,&needed));assert(needed==(available?2:0)); + if(available){near(raw[0].reported_value.value,1);assert(raw[0].reported_value.present&&raw[0].has_reported_mask&&raw[0].reported_mask==1);} + OK(gecode_opt_v1_lp_evidence_stage_destroy(child));assert(gecode_opt_v1_lp_evidence_stage_destroy(child)==GECODE_OPT_INVALID_HANDLE); +} +static void farkas(void){ + gecode_opt_handle m=0,out=0,child=0;gecode_opt_id x,row;gecode_opt_term t;gecode_opt_evidence_options_v1 options; + gecode_opt_evidence_group_v1 group;gecode_opt_evidence_farkas_v1 summary;gecode_opt_evidence_slot_v1 slot; + gecode_opt_evidence_column_v1 maps[2];uint64_t needed; + OK(gecode_opt_v1_model_create(&m));OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_CONTINUOUS,-INFINITY,0,"x",&x)); + t.variable=x;t.coefficient=1;OK(gecode_opt_v1_model_add_row(m,&t,1,1,INFINITY,"impossible",&row)); + OK(gecode_opt_v1_evidence_options_default(&options,sizeof(options)));options.request=GECODE_OPT_EVIDENCE_FARKAS; + out=analyze(m,&options);OK(gecode_opt_v1_lp_evidence_group(out,GECODE_OPT_EVIDENCE_PRIMAL_GROUP,&group,sizeof(group))); + assert(group.state==GECODE_OPT_EVIDENCE_NOT_REQUESTED&&group.reason==GECODE_OPT_EVIDENCE_REASON_NOT_REQUESTED); + OK(gecode_opt_v1_lp_evidence_farkas(out,&summary,sizeof(summary)));assert(summary.contradiction_margin.present==available); + if(available){near(summary.contradiction_margin.value,1);OK(gecode_opt_v1_lp_evidence_multiplier(out,row,&slot,sizeof(slot))); + assert(slot.has_side&&slot.side==GECODE_OPT_EVIDENCE_LOWER&&slot.selected_bound.present);near(slot.multiplier.value,1);near(slot.selected_bound.value,1); + OK(gecode_opt_v1_lp_evidence_multiplier(out,x,&slot,sizeof(slot)));assert(slot.side==GECODE_OPT_EVIDENCE_UPPER); + near(slot.multiplier.value,-1);assert(slot.contribution.present);near(slot.contribution.value,0);near(slot.selected_bound.value,0); + }else assert(gecode_opt_v1_lp_evidence_multiplier(out,row,&slot,sizeof(slot))==GECODE_OPT_NO_EVIDENCE); + OK(gecode_opt_v1_lp_evidence_copy_stage(out,0,&child));OK(gecode_opt_v1_lp_evidence_stage_columns(child,maps,sizeof(maps[0]),2,&needed)); + assert(needed==2&&maps[0].kind==GECODE_OPT_EVIDENCE_ROW_SIDE&&maps[0].source.slot==row.slot&&maps[0].has_side); + assert(maps[1].kind==GECODE_OPT_EVIDENCE_VARIABLE_SIDE&&maps[1].source.slot==x.slot&&maps[1].side==GECODE_OPT_EVIDENCE_UPPER); + OK(gecode_opt_v1_model_destroy(m));OK(gecode_opt_v1_lp_evidence_destroy(out));OK(gecode_opt_v1_lp_evidence_stage_destroy(child)); +} +static void options_and_stops(void){ + gecode_opt_handle m=0,out=0,cancel=0;gecode_opt_id x;gecode_opt_evidence_options_v1 options,bad; + gecode_opt_evidence_info_v1 info;gecode_opt_evidence_group_v1 group;int i; + OK(gecode_opt_v1_model_create(&m));OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_CONTINUOUS,0,INFINITY,"x",&x)); + OK(gecode_opt_v1_evidence_options_default(&options,sizeof(options))); + for(i=0;i<7;i++){ + bad=options;switch(i){case 0:bad.struct_size--;break;case 1:bad.reserved=1;break;case 2:bad.reserved_flags=1;break; + case 3:bad.solve.struct_size--;break;case 4:bad.solve.has_node_limit=2;break;case 5:bad.request=99;break;default:bad.solve.reserved=1;break;} + out=99;assert(gecode_opt_v1_analyze_lp_evidence(m,&bad,&out)==GECODE_OPT_INVALID_ARGUMENT&&out==0); + } + bad=options;bad.minimum_contradiction=NAN;out=99;assert(gecode_opt_v1_analyze_lp_evidence(m,&bad,&out)==GECODE_OPT_MODEL_ERROR&&out==0); + for(i=0;i<8;i++){ + int expected;bad=options;switch(i){case 0:bad.solve.backend=GECODE_OPT_NATIVE;expected=GECODE_OPT_UNSUPPORTED;break; + case 1:bad.solve.guarantee=GECODE_OPT_EXACT;expected=GECODE_OPT_UNSUPPORTED;break; + case 2:bad.solve.time_limit_seconds=0;expected=GECODE_OPT_TIME_LIMIT;break; + case 3:bad.solve.has_node_limit=1;bad.solve.node_limit=0;expected=GECODE_OPT_NODE_LIMIT;break; + case 4:bad.max_work=0;expected=GECODE_OPT_ITERATION_LIMIT;break; + case 5:bad.max_auxiliary_variables=0;expected=GECODE_OPT_MEMORY_LIMIT;break; + case 6:bad.max_retained_slots=0;expected=GECODE_OPT_MEMORY_LIMIT;break; + default:bad.max_auxiliary_solves=0;expected=available?GECODE_OPT_ITERATION_LIMIT:GECODE_OPT_UNSUPPORTED;break;} + out=analyze(m,&bad);OK(gecode_opt_v1_lp_evidence_info(out,&info,sizeof(info))); + assert(info.has_stop_reason&&info.stop_reason==expected&&!info.attempted_calls); + if(!info.has_evidence)assert(gecode_opt_v1_lp_evidence_group(out,0,&group,sizeof(group))==GECODE_OPT_NO_EVIDENCE); + OK(gecode_opt_v1_lp_evidence_destroy(out)); + } + OK(gecode_opt_v1_cancellation_create(&cancel));OK(gecode_opt_v1_cancellation_cancel(cancel));bad=options;bad.solve.cancellation=cancel; + out=analyze(m,&bad);OK(gecode_opt_v1_lp_evidence_info(out,&info,sizeof(info)));assert(info.stop_reason==GECODE_OPT_CANCELLED); + OK(gecode_opt_v1_cancellation_destroy(cancel));OK(gecode_opt_v1_lp_evidence_destroy(out));OK(gecode_opt_v1_model_destroy(m)); +} +int main(void){int32_t lp,mip;OK(gecode_opt_v1_capabilities(GECODE_OPT_HIGHS,&available,&lp,&mip)); + ray();farkas();options_and_stops();puts("C99 LP evidence ownership, raw stages, groups and boundary tests passed");return 0;} diff --git a/test/optimize/lp_evidence_coordinator.cpp b/test/optimize/lp_evidence_coordinator.cpp new file mode 100644 index 0000000000..a2a804238d --- /dev/null +++ b/test/optimize/lp_evidence_coordinator.cpp @@ -0,0 +1,122 @@ +// Compile lp_evidence.cpp separately with GECODE_OPTIMIZE_TEST_LP_EVIDENCE=1. +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +#include +#include +namespace O=Gecode::Optimize; +using Phase=O::LpEvidencePhase;using State=O::LpEvidenceState;using Request=O::LpEvidenceRequest; +namespace { +enum class Fault {None,Owner,Revision,Mask,Dimension,NaNValue,Objective,NaNBound,WrongBound,Gap,Status,Guarantee, + FalseInfeasible,Unbounded,NoWitness,NormalizedInfeasible,FiniteLimit,OriginalDirection,InfiniteSide,CancellationSum}; +Fault fault=Fault::None;Phase target=Phase::Recession;std::size_t calls=0; +std::string cancel_point,allocation_point;std::size_t cancel_index=0; +std::shared_ptr cancellation; +double previous=std::numeric_limits::infinity(); +O::SolveResult oracle(const O::ModelSnapshot& m,const O::SolveOptions& options){ + O::SolveResult out;out.model_id=m.model_id;out.revision=m.revision;out.guarantee=O::Guarantee::Numerical; + out.backend="deterministic finite-grid oracle";for(const auto& v:m.variables)out.active_variables.push_back(v.active); + std::vector values(m.variables.size(),0);const double grid[]={-1,0,.25,.5,1}; + std::function visit=[&](std::size_t i){ + if(i=m.variables[i].lower&&x<=m.variables[i].upper){values[i]=x;visit(i+1);}return;} + for(const auto& row:m.rows){long double sum=0;for(const auto& t:row.terms)sum+=t.coefficient*values[t.variable.id]; + if(sumrow.upper+options.feasibility_tolerance)return;} + long double objective=m.objective.offset;for(const auto& t:m.objective.terms)objective+=t.coefficient*values[t.variable.id]; + if(!out.objective||(m.objective.sense==O::ObjectiveSense::Minimize?objective<*out.objective:objective>*out.objective)){out.objective=static_cast(objective);out.values=values;} + };visit(0);out.solution_validated=bool(out.objective);out.best_bound=out.objective; + out.termination=out.objective?O::Termination::Optimal:O::Termination::Infeasible;return out; +} +void reset(){fault=Fault::None;target=Phase::Recession;calls=0;cancel_point.clear();allocation_point.clear();cancel_index=0; + cancellation=std::make_shared();previous=std::numeric_limits::infinity();} +} +namespace Gecode { namespace Optimize { namespace Detail { +SolveResult lp_evidence_test_solve(LpEvidencePhase phase,const ModelSnapshot& m,const SolveOptions& options){ + assert(options.backend==Backend::Highs&&options.guarantee==Guarantee::Numerical&&options.time_limit_seconds<=previous); + previous=options.time_limit_seconds;++calls;auto out=oracle(m,options);if(phase!=target)return out; + const auto discard=[&]{out.values.clear();out.objective.reset();out.best_bound.reset();out.solution_validated=false;}; + switch(fault){ + case Fault::None:break; + case Fault::Owner:++out.model_id;break;case Fault::Revision:++out.revision;break; + case Fault::Mask:out.active_variables[0]=false;break; + case Fault::Dimension:out.values.push_back(0);break; + case Fault::NaNValue:out.values[0]=std::numeric_limits::quiet_NaN();break; + case Fault::Objective:*out.objective+=1;break; + case Fault::NaNBound:out.best_bound=std::numeric_limits::quiet_NaN();break; + case Fault::WrongBound:out.best_bound=std::numeric_limits::infinity();break; + case Fault::Gap:out.relative_gap=std::numeric_limits::quiet_NaN();break; + case Fault::Status:out.termination=static_cast(99);break; + case Fault::Guarantee:out.guarantee=Guarantee::Exact;break; + case Fault::FalseInfeasible:out.termination=Termination::Infeasible;out.solution_validated=false;break; + case Fault::Unbounded:discard();out.termination=Termination::Unbounded;break; + case Fault::NoWitness:discard();break; + case Fault::NormalizedInfeasible:discard();out.termination=Termination::Infeasible;break; + case Fault::FiniteLimit:out.termination=Termination::IterationLimit;out.best_bound=-std::numeric_limits::infinity();break; + case Fault::OriginalDirection:out.values={1e-8};out.objective=-1e-8;out.best_bound=out.objective;break; + case Fault::InfiniteSide:out.values={1e-8};out.objective=1e-8;out.best_bound=out.objective;break; + case Fault::CancellationSum:out.values={.25,.25,.25};out.objective=.25;out.best_bound=out.objective;out.solution_validated=true;out.termination=Termination::Optimal;break; + } + return out; +} +void lp_evidence_test_checkpoint(const char* point,std::size_t index){ + if(cancel_point==point&&cancel_index==index)cancellation->cancel(); + if(allocation_point==point)throw std::bad_alloc(); +} +}}} +int main(){ + O::Model source;auto x=source.add_continuous();source.minimize({{x,-1}},7); + O::LpEvidenceOptions options;options.solve.time_limit_seconds=30;options.request=Request::Both; + reset();auto good=O::analyze_lp_evidence(source,options);assert(good.completion==O::LpEvidenceCompletion::Complete&&calls==3&&previous<30); + assert(good.evidence->primal_ray().state==State::Available&&good.evidence->direction_value(x)==1); + for(auto f:{Fault::Owner,Fault::Revision,Fault::Mask,Fault::Dimension,Fault::NaNValue,Fault::Objective, + Fault::NaNBound,Fault::WrongBound,Fault::Gap,Fault::Status,Fault::Guarantee,Fault::FalseInfeasible,Fault::Unbounded,Fault::NoWitness,Fault::NormalizedInfeasible}){ + reset();fault=f;auto out=O::analyze_lp_evidence(source,options); + assert(out.stop_reason==((f==Fault::Unbounded||f==Fault::NormalizedInfeasible)?O::Termination::BackendError:O::Termination::NumericalFailure)&&out.completion==O::LpEvidenceCompletion::Rejected&&calls==2); + assert(out.evidence->primal_ray().state==State::Rejected&&out.evidence->farkas().state==State::Rejected); + } + reset();target=Phase::FeasibleBase;fault=Fault::FalseInfeasible; + auto false_infeasible=O::analyze_lp_evidence(source,options);assert(false_infeasible.stop_reason==O::Termination::NumericalFailure&&calls==1); + reset();fault=Fault::FiniteLimit;auto limited=O::analyze_lp_evidence(source,options); + assert(limited.completion==O::LpEvidenceCompletion::Interrupted&&limited.stop_reason==O::Termination::IterationLimit&&calls==2); + assert(limited.evidence->primal_ray().state==State::Available&&!limited.evidence->stages()[2].attempted); + // An auxiliary direction within its primal tolerance can fail after normalization. + O::Model bounded;auto fixed=bounded.add_continuous(0,0);bounded.minimize({{fixed,-1}}); + reset();fault=Fault::OriginalDirection;auto normalized=O::analyze_lp_evidence(bounded,options); + assert(normalized.stop_reason==O::Termination::NumericalFailure&&normalized.evidence->primal_ray().reason==O::LpEvidenceReason::FailedOriginalChecks); + // Tiny auxiliary stationarity residual does not authorize an infinite bound side. + O::Model free;auto f=free.add_continuous(-std::numeric_limits::infinity(),std::numeric_limits::infinity());free.add_row({{f,1}},1,std::numeric_limits::infinity()); + reset();target=Phase::Farkas;fault=Fault::InfiniteSide;options.request=Request::Farkas; + auto side=O::analyze_lp_evidence(free,options);assert(side.stop_reason==O::Termination::NumericalFailure&&side.evidence->farkas().reason==O::LpEvidenceReason::FailedOriginalChecks); + // Original cancellation must retain the small residual across large terms. + O::Model cancellation_model;auto a=cancellation_model.add_continuous(-std::numeric_limits::infinity(),std::numeric_limits::infinity()); + auto b=cancellation_model.add_continuous(-std::numeric_limits::infinity(),std::numeric_limits::infinity()); + cancellation_model.add_row({{a,1}},1,std::numeric_limits::infinity());cancellation_model.add_row({{b,1}},1e16,std::numeric_limits::infinity()); + cancellation_model.add_row({{a,1},{b,1}},-std::numeric_limits::infinity(),1e16); + reset();target=Phase::Farkas;fault=Fault::CancellationSum; + auto sum=O::analyze_lp_evidence(cancellation_model,options);assert(sum.completion==O::LpEvidenceCompletion::Complete&&sum.evidence->farkas().state==State::Available); + assert(sum.evidence->farkas_data().contradiction_margin==1&&sum.evidence->farkas_data().max_stationarity==0); + // Different valid declared tolerances overlap: reject incompatible conclusions. + O::Model overlap;auto z=overlap.add_continuous(0,0);overlap.add_row({{z,1}},1e-8,std::numeric_limits::infinity()); + reset();options.request=Request::Both;options.checks.minimum_contradiction=1e-10; + auto inconsistent=O::analyze_lp_evidence(overlap,options); + assert(inconsistent.stop_reason==O::Termination::NumericalFailure&&inconsistent.evidence->farkas().reason==O::LpEvidenceReason::InconsistentEvidence); + assert(inconsistent.evidence->primal_ray().state==State::Rejected&&inconsistent.evidence->farkas().state==State::Rejected); + assert(inconsistent.evidence->primal_data().base_check.valid&&inconsistent.evidence->farkas_data().contradiction_margin==1e-8); + for(const auto& item:std::vector>{{"source_copy",0},{"admission",0},{"before_solve",1},{"after_solve",1}, + {"after_check",1},{"stage_cleanup",1},{"evidence_check",1},{"publication",3},{"source_cleanup",3}}){ + reset();cancel_point=item.first;cancel_index=item.second;options.solve.cancellation=cancellation; + auto stopped=O::analyze_lp_evidence(source,options);assert(stopped.stop_reason==O::Termination::Cancelled); + if(stopped.evidence){assert(stopped.evidence->primal_ray().state!=State::Available&&stopped.evidence->farkas().state!=State::Available);} + assert(stopped.attempted_calls==calls); + } + reset();allocation_point="publication";options.solve.cancellation=cancellation; + auto memory=O::analyze_lp_evidence(source,options);assert(memory.stop_reason==O::Termination::MemoryLimit&&memory.evidence->primal_ray().state!=State::Available); + reset();options.solve.cancellation=cancellation;options.limits.max_work=good.work-1; + auto work=O::analyze_lp_evidence(source,options);assert(work.work<=options.limits.max_work&&work.stop_reason==O::Termination::IterationLimit); + assert(!work.evidence||work.evidence->primal_ray().state!=State::Available); + std::cout<<"LP evidence adversarial status, original checks, consistency and lifecycle gates passed\n"; +} diff --git a/test/optimize/lp_integer_backend.cpp b/test/optimize/lp_integer_backend.cpp new file mode 100644 index 0000000000..5c8ca1583c --- /dev/null +++ b/test/optimize/lp_integer_backend.cpp @@ -0,0 +1,87 @@ +// Explicit bounded-integer import, sibling restoration and range proof gates. +#include +#include +#include +#include +#include +#include + +namespace LP=Gecode::Experimental::LpRelaxation; +using I=std::int64_t; +static void check(bool okay,const char* message) {if(!okay)throw std::runtime_error(message);} +static LP::BoundedIntegerModel model(LP::LinearModel dense,std::vector lo,std::vector hi) { + return {LP::sparse_model(dense),std::move(lo),std::move(hi)}; +} +static void invalid_models() { + const auto original=model({{1,1},{-1},{2,9}},{-3,-3},{3,3}); + for(unsigned bad=0;bad<10;++bad) { + auto m=original; + if(bad==0)m.lower.pop_back(); + if(bad==1)m.upper[0]=m.lower[0]-1; + if(bad==2)m.lower[0]=static_cast(Gecode::Int::Limits::min)-1; + if(bad==3)m.upper[0]=static_cast(Gecode::Int::Limits::max)+1; + if(bad==4)m.linear.column[1]=m.linear.column[0]; + if(bad==5)m.linear.a[0]=0; + if(bad==6)m.linear.b[0]=1000000001LL; + if(bad==7)m.linear.a[0]=1000000000LL; // Native row activity overflow. + if(bad==8)m.linear.c[0]=1000000000LL; // Native objective equality overflow. + if(bad==9)m.linear.row_start.back()=1; + bool validator=false,backend=false; + try {LP::validate_integer_model(m);}catch(const std::invalid_argument&){validator=true;} + try {LP::BoundedIntegerBackend unused(m);}catch(const std::invalid_argument&){backend=true;} + check(validator && backend,"unsafe integer model accepted"); + } + const I maximum=Gecode::Int::Limits::max; + LP::validate_integer_model(model({{1},{0},{0}},{-maximum},{maximum})); + LP::validate_integer_model(model({{},{},{1}},{-maximum/2},{maximum/2})); + auto excess=model({{},{},{1}},{0},{maximum/2+1});bool rejected=false; + try {LP::validate_integer_model(excess);}catch(const std::invalid_argument&){rejected=true;} + check(rejected,"objective equality envelope boundary not enforced"); +} +int main() { + try { + static_assert(!std::is_base_of::value, + "integer workspace must not enter strict binary API"); + invalid_models(); + LP::BoundedIntegerBackend triangle(model({{1,1,0,0,1,1,1,0,1},{3,3,3},{1,1,1}}, + {0,0,0},{3,3,3})); + const auto t=triangle.bound({0,0,0},{3,3,3},true); + check(t.valid && t.lower_bound==5 && std::abs(t.lp_objective-4.5)<1e-8 && t.certificate, + "fractional integer LP certificate ceiling"); + I reevaluated=0; + check(t.certificate->lower_bound_integer({-3,-3,-3},{3,3,3},reevaluated) && reevaluated==5, + "integer certificate not independently reusable on a looser signed box"); + + auto source=model({{1,1},{-1},{2,9}},{-3,-3},{3,3}); + LP::BoundedIntegerBackend sibling(source); + source.linear.a[0]=7;source.lower[0]=0;source.upper[1]=0; + check(sibling.model.linear.a[0]==1 && sibling.model.lower[0]==-3 && sibling.model.upper[1]==3, + "input mutation changed immutable integer model"); + auto a=sibling.bound({-3,2},{-3,2},true),b=sibling.bound({2,-3},{2,-3},true); + auto restored=sibling.bound({-3,-3},{3,3},true); + check(a.valid && a.lower_bound==12 && b.valid && b.lower_bound==-23 && + restored.valid && restored.lower_bound==-23,"signed sibling bounds were not fully restored"); + (void)sibling.bound({-3,-3},{-3,-3}); + restored=sibling.bound({-3,-3},{3,3}); + check(restored.valid && restored.lower_bound==-23 && sibling.statistics().infeasible_status>0, + "integer workspace did not recover after numerical infeasibility"); + check(!sibling.bound({-4,-3},{3,3}).valid && !sibling.bound({-3,-3},{3,4}).valid && + !sibling.bound({-3},{3}).valid && !sibling.bound({3,-3},{2,3}).valid, + "integer workspace accepted invalid/outside-original boxes"); + bool first_ok=true,second_ok=true; + std::thread first([&]{for(int i=0;i<40;++i){const auto r=sibling.bound({-3,2},{-3,2});first_ok&=r.valid && r.lower_bound==12;}}); + std::thread second([&]{for(int i=0;i<40;++i){const auto r=sibling.bound({2,-3},{2,-3});second_ok&=r.valid && r.lower_bound==-23;}}); + first.join();second.join();check(first_ok && second_ok,"serialized integer sibling calls changed bounds"); + + LP::BoundedIntegerBackend empty(model({{},{},{}},{},{})); + const auto e=empty.bound({},{});check(e.valid && e.lower_bound==0,"empty integer box"); + LP::BoundedIntegerBackend zero(model({{0,0},{1},{-3,4}},{-2,-3},{2,3})); + const auto z=zero.bound({-2,-3},{2,3},true); + check(z.valid && z.lower_bound==-18 && zero.statistics().lp_calls==0, + "zero matrix skipped original feasibility or attempted invalid HiGHS import"); + LP::Backend binary({{},{},{1}}); + check(!binary.bound({-1},{1}).valid && !binary.bound({0},{2}).valid,"legacy binary contract widened silently"); + std::cout<<"PASS integer backend: signed restored siblings, 80 serialized calls, exact ceiling, " + "original-domain gates, zero matrices and native activity boundaries\n"; + }catch(const std::exception& error){std::cerr< +#include +#include +#include +#include +#include + +namespace C=Gecode::Experimental::LpCertificate; +using I=std::int64_t; +using V=std::vector; +static void check(bool okay,const char* message) {if(!okay)throw std::runtime_error(message);} +struct Matrix { + std::vector start{0},column; + V value;std::size_t n=0; + C::SparseMatrixView view() const {return {start,column,value,n};} +}; +static Matrix sparse(const V& a,std::size_t m,std::size_t n) { + Matrix matrix;matrix.n=n; + for(std::size_t i=0;i0?-5:-upper}) && result.upper==V({cost>0?upper:5}), + "floor/ceil sign or exact-divisibility error"); + check(result.lower_bound==-10,"signed integer box bound"); + I legacy=443; + check(!certificate.lower_bound({-5},{5},legacy) && legacy==443,"legacy binary method widened silently"); + } + C::Certificate constant; + check(C::prepare({2},{3},{1},{0.5},constant),"zero-residual multiplier preparation"); + C::IntegerFilterResult empty; + check(constant.filter_integer({-3},{5},1,empty) && empty.infeasible && empty.lower_bound==2, + "constant cutoff infeasibility or integer ceiling"); + check(constant.filter_integer({-3},{5},2,empty) && !empty.infeasible, + "constant cutoff equality rejected"); + C::Certificate none; + check(C::prepare({}, {}, {}, {},none),"empty model preparation"); + check(none.filter_integer({}, {},-1,empty) && empty.infeasible,"zero-dimensional box cutoff"); + check(none.filter_integer({}, {},0,empty) && !empty.infeasible && empty.lower.empty(),"empty feasible model"); + C::IntegerFilterResult saved{991,{992},{993},true}; + empty=saved; + check(!constant.filter_integer({2},{1},0,empty) && empty.lower_bound==991 && empty.lower==V({992}) && + empty.upper==V({993}) && empty.infeasible,"empty interval failure changed output"); + I bound=444; + check(!constant.lower_bound_integer({}, {},bound) && bound==444,"integer bound dimension failure"); + const auto maximum=std::numeric_limits::max(),minimum=std::numeric_limits::min(); + check(C::integer_lower_bound({}, {},{1},{minimum},{minimum},{},bound) && bound==minimum,"minimum integer endpoint"); + check(C::integer_lower_bound({}, {},{1},{maximum},{maximum},{},bound) && bound==maximum,"maximum integer endpoint"); + bound=444; + check(!C::integer_lower_bound({}, {},{-1},{minimum},{minimum},{},bound) && bound==444, + "unrepresentable objective accepted"); + check(C::integer_lower_bound({}, {},{minimum},{1},{1},{},bound) && bound==minimum,"minimum integer coefficient"); + check(C::integer_lower_bound({}, {},{maximum},{1},{1},{},bound) && bound==maximum,"maximum integer coefficient"); + + C::Certificate large; + check(C::prepare({maximum},{0},{0},{1e8},large),"large residual preparation"); + bound=444; + check(!large.lower_bound_integer({0},{maximum},bound) && bound==444,"residual*endpoint overflow accepted"); + empty=saved; + check(!large.filter_integer({0},{maximum},0,empty) && empty.lower_bound==991 && empty.lower==V({992}), + "overflowing interval filter changed output"); + // Preparation and the integer box numerator fit; the conditional cutoff + // subtraction does not. Reject the whole cut rather than wrap or saturate. + const V huge={minimum,minimum,minimum,minimum+1}; + check(C::prepare(huge,huge,{0},std::vector(4,std::ldexp(1.0,42)),large), + "near-int128-limit affine preparation"); + check(large.lower_bound_integer({1},{2},bound) && bound==0,"cancelled large integer numerator"); + empty=saved; + check(!large.filter_integer({1},{2},maximum,empty) && empty.lower_bound==991 && empty.upper==V({993}), + "conditional subtraction overflow accepted or output changed"); +} +static void oracles() { + std::mt19937 random(139572); + unsigned feasible_models=0,cut_solutions=0,tightened=0; + for(unsigned trial=0;trial<3000;++trial) { + const std::size_t n=1+random()%4,m=random()%5; + V a(n*m),b(m),c(n),lower(n),upper(n),witness(n); + std::vector duals(m); + for(std::size_t j=0;j(random()%7)-4;upper[j]=lower[j]+random()%4; + witness[j]=lower[j]+random()%(upper[j]-lower[j]+1); + c[j]=static_cast(random()%11)-5; + } + for(std::size_t i=0;i(random()%11)-5; + activity+=a[i*n+j]*witness[j]; + } + b[i]=trial%2?static_cast(random()%21)-10:activity-static_cast(random()%5); + duals[i]=(static_cast(random()%41)-10)/7.0; + } + const auto matrix=sparse(a,m,n); + C::Certificate dense,csr; + check(C::prepare(a,b,c,duals,dense) && C::prepare(matrix.view(),b,c,duals,csr),"small integer preparation"); + I db=0,sb=0,direct=0; + check(dense.lower_bound_integer(lower,upper,db) && csr.lower_bound_integer(lower,upper,sb) && db==sb && + C::integer_lower_bound(matrix.view(),b,c,lower,upper,duals,direct) && direct==db,"integer dense/CSR equivalence"); + const I cutoff=static_cast(random()%41)-20; + C::IntegerFilterResult dcut,scut; + check(dense.filter_integer(lower,upper,cutoff,dcut) && csr.filter_integer(lower,upper,cutoff,scut),"small integer filtering"); + check(dcut.lower_bound==scut.lower_bound && dcut.lower==scut.lower && dcut.upper==scut.upper && + dcut.infeasible==scut.infeasible,"dense/CSR integer cuts disagree"); + bool feasible=false,within_cutoff=false; + V assignment(n); + const auto enumerate=[&](auto&& self,std::size_t j)->void { + if(jcutoff)return; + within_cutoff=true;++cut_solutions; + check(!scut.infeasible,"integer cut falsely proved infeasibility"); + for(std::size_t k=0;k=lower[j] && scut.upper[j]<=upper[j] && scut.lower[j]<=scut.upper[j],"cuts loosened/inverted box"); + tightened+=scut.lower[j]!=lower[j] || scut.upper[j]!=upper[j]; + } + // The same certificate also evaluates a looser, signed sibling box. + for(std::size_t j=0;j=1500 && cut_solutions>0 && tightened>0,"integer oracle did not exercise valid cuts"); + std::cout<<"PASS 3000 integer box/cut dense-CSR oracles; "< +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace LP=Gecode::Experimental::LpRelaxation; +using I=std::int64_t; +using Assignment=std::vector; +static void check(bool okay,const char* message) {if(!okay)throw std::runtime_error(message);} +struct Instance { + LP::BoundedIntegerModel model; + std::vector domains; +}; +enum class Mode {Native,Root,Node,RootTight,NodeTight,Throttled}; +static I value(const LP::BoundedIntegerModel& model,const Assignment& x) { + I result=0;for(std::size_t j=0;j enumerate(const Instance& instance) { + const auto& m=instance.model.linear;Assignment current(m.c.size());std::set result; + const auto visit=[&](auto&& self,std::size_t j)->void { + if(jinstance.model.upper[k])return; + for(std::size_t i=0;i& backend, + unsigned interval=4) + : x(*this,static_cast(instance.model.linear.c.size())),z(*this,Gecode::Int::Limits::min,Gecode::Int::Limits::max) { + for(int j=0;j(domain.size()))); + } + if(mode==Mode::Native) LP::post_native_integer(*this,x,z,instance.model); + else { + LP::IntegerOptions options; + options.frequency=mode==Mode::Root || mode==Mode::RootTight ? LP::Frequency::Root : LP::Frequency::EveryNode; + options.bound_tightening=mode==Mode::RootTight || mode==Mode::NodeTight || mode==Mode::Throttled; + options.bound_change_interval=mode==Mode::Throttled?interval:1; + LP::integer_linear_minimize(*this,x,z,backend,options); + } + Gecode::branch(*this,x,Gecode::INT_VAR_SIZE_MIN(),Gecode::INT_VAL_MIN()); + } + Problem(Problem& other):Gecode::Space(other){x.update(*this,other.x);z.update(*this,other.z);} + Gecode::Space* copy() override{return new Problem(*this);} + void constrain(const Gecode::Space& best) override { + Gecode::rel(*this,z,Gecode::IRT_LE,static_cast(best).z.val()); + } + Assignment assignment() const { + Assignment result;for(int j=0;j& backend, + unsigned distance,const std::set& expected) { + Gecode::Search::Options options;options.threads=1;options.c_d=distance;options.a_d=3; + { + auto root=std::make_unique(instance,mode,backend); + Gecode::DFS search(root.get(),options);root.reset();std::set actual; + while(std::unique_ptr found{search.next()}) { + auto assignment=found->assignment(); + check(expected.count(assignment)==1 && actual.insert(assignment).second,"integer DFS infeasible/duplicate witness"); + check(found->z.assigned() && found->z.val()==value(instance.model,assignment),"integer DFS objective mismatch"); + } + check(!search.stopped() && actual==expected,"integer propagation changed complete solution set"); + } + check(backend.use_count()==1,"integer DFS backend leak"); + { + auto root=std::make_unique(instance,mode,backend); + Gecode::BAB search(root.get(),options);root.reset();I best=std::numeric_limits::max(); + while(std::unique_ptr found{search.next()}) { + const auto assignment=found->assignment();const I cost=value(instance.model,assignment); + check(expected.count(assignment)==1 && costz.val()==cost,"integer BAB witness/objective mismatch"); + best=cost; + } + I optimum=std::numeric_limits::max();for(const auto& assignment:expected)optimum=std::min(optimum,value(instance.model,assignment)); + check(!search.stopped() && best==optimum,"integer BAB optimum mismatch"); + } + check(backend.use_count()==1,"integer BAB backend leak"); +} +static Instance instance(LP::LinearModel dense,std::vector lower,std::vector upper) { + Instance result{{LP::sparse_model(dense),std::move(lower),std::move(upper)},{}}; + for(std::size_t j=0;j(v)); + result.domains.push_back(std::move(domain)); + } + return result; +} +static std::vector cases() { + std::vector result; + result.push_back(instance({{1,1,0,0,1,1,1,0,1},{3,3,3},{1,1,1}},{0,0,0},{3,3,3})); + result.push_back(instance({{-1,-1,0,0,-1,-1,-1,0,-1},{1,1,1},{-1,-1,-1}},{-3,-3,-3},{1,1,1})); + result.push_back(instance({{1,-1,2,-2},{0,1},{-2,3}},{-2,-2},{2,2})); + result.push_back(instance({{0,0},{1},{-1,2}},{-2,1},{2,3})); + result.push_back(instance({{},{},{-3,2}},{-2,-2},{2,3})); + result.push_back(instance({{},{},{}},{},{})); + result.push_back(instance({{1,-1},{0},{-1,2}},{-2,-3},{-2,-3})); + auto holes=instance({{1,-1},{0},{-3,2}},{-3,-3},{3,3});holes.domains={{-3,0,3},{-2,2}};result.push_back(holes); + result.push_back(instance({{1000000000,-1000000000},{1000000000},{0,0}},{-1,-1},{1,1})); + const I maximum=Gecode::Int::Limits::max; + result.push_back({{LP::sparse_model({{-1},{1000000000},{0}}),{-maximum},{maximum}}, + {{static_cast(-maximum),0,static_cast(maximum)}}}); + result.push_back({{LP::sparse_model({{},{},{1}}),{-maximum/2},{maximum/2}}, + {{static_cast(-maximum/2),0,static_cast(maximum/2)}}}); + auto intersected=instance({{1,1},{0},{-1,2}},{-2,-1},{2,3}); + intersected.domains={{-4,-1,0,1,4},{-2,0,2,5}};result.push_back(intersected); + auto empty_intersection=instance({{},{},{1}},{0},{1}); + empty_intersection.domains={{-2,-1}};result.push_back(empty_intersection); + std::mt19937 random(113955); + while(result.size()<80) { + const auto n=1+random()%4,m=random()%5; + LP::LinearModel dense;dense.a.resize(n*m);dense.b.resize(m);dense.c.resize(n); + std::vector lo(n),hi(n),witness(n); + for(std::size_t j=0;j(random()%5)-3;hi[j]=lo[j]+random()%4; + witness[j]=lo[j]+random()%(hi[j]-lo[j]+1);dense.c[j]=static_cast(random()%9)-4; + } + for(std::size_t i=0;i(random()%9)-4;activity+=dense.a[i*n+j]*witness[j];} + dense.b[i]=result.size()%2?static_cast(random()%15)-7:activity-static_cast(random()%4); + } + auto model=instance(dense,lo,hi); + for(auto& domain:model.domains) if(domain.size()>2 && random()%3==0)domain.erase(domain.begin()+1); + result.push_back(std::move(model)); + } + return result; +} +static std::uint64_t policies() { + // The three covering rows jointly imply a fractional cost of 4.5. + // Native row-by-row propagation cannot derive the fourth variable's cut. + const auto cutting=instance({{1,1,0,0,0,1,1,0,1,0,1,0},{3,3,3},{1,1,1,2}}, + {0,0,0,-1},{3,3,3,3}); + auto cut_backend=std::make_shared(cutting.model); + auto cut=std::make_unique(cutting,Mode::RootTight,cut_backend); + Gecode::rel(*cut,cut->z,Gecode::IRT_LQ,6); + check(cut->status()==Gecode::SS_BRANCH && cut->x[3].min()==-1 && cut->x[3].max()==0, + "integer certificate failed a strict nonbinary interval cut"); + check(cut_backend->statistics().variable_bound_tightenings>0,"integer interval tightening not recorded"); + { + auto expected=enumerate(cutting); + for(auto it=expected.begin();it!=expected.end();) + if(value(cutting.model,*it)>6)it=expected.erase(it);else ++it; + Gecode::DFS search(cut.get());cut.reset();std::set actual; + while(std::unique_ptr found{search.next()})actual.insert(found->assignment()); + check(!search.stopped() && actual==expected,"integer interval cut removed a cutoff-feasible assignment"); + } + check(cut_backend.use_count()==1,"integer cutting policy lifecycle"); + const auto plain=instance({{},{},{1,2,3}},{-4,-4,-4},{4,4,4}); + for(unsigned interval:{1U,2U}) { + auto backend=std::make_shared(plain.model); + auto root=std::make_unique(plain,Mode::Throttled,backend,interval); + check(root->status()==Gecode::SS_BRANCH && backend->statistics().valid_bounds==1,"integer root scheduling"); + Gecode::rel(*root,root->x[0],Gecode::IRT_GQ,-2); + check(root->status()==Gecode::SS_BRANCH && !root->x[0].assigned(),"nonassignment bound change"); + check(backend->statistics().valid_bounds==(interval==1?2U:1U),"integer scheduling counted assignments instead of bounds"); + Gecode::rel(*root,root->x[1],Gecode::IRT_GQ,-2); + check(root->status()==Gecode::SS_BRANCH && backend->statistics().valid_bounds==(interval==1?3U:2U),"integer bound interval throttling"); + const auto calls=backend->statistics().valid_bounds; + Gecode::rel(*root,root->z,Gecode::IRT_LQ,23); + check(root->status()==Gecode::SS_BRANCH && backend->statistics().valid_bounds==calls,"objective-only change reoptimized LP"); + auto clone=std::unique_ptr(static_cast(root->clone())); + check(clone->status()==Gecode::SS_BRANCH,"integer cloned bound policy"); + clone.reset();root.reset();check(backend.use_count()==1,"integer policy lifecycle"); + } + return cut_backend->statistics().variable_bound_tightenings; +} +int main() { + try { + static_assert(!std::is_base_of::value,"integer backend must not enter binary API by upcast"); + const auto models=cases();unsigned runs=0,feasible=0;std::uint64_t bounds=0,tightened=0; + for(const auto& model:models) { + const auto expected=enumerate(model);feasible+=!expected.empty(); + auto backend=std::make_shared(model.model); + std::weak_ptr weak=backend; + for(auto mode:{Mode::Native,Mode::Root,Mode::Node,Mode::RootTight,Mode::NodeTight,Mode::Throttled}) + for(unsigned distance:{1U,8U}){searches(model,mode,backend,distance,expected);runs+=2;} + bounds+=backend->statistics().valid_bounds;tightened+=backend->statistics().variable_bound_tightenings; + backend.reset();check(weak.expired(),"integer backend outlived searches"); + } + tightened+=policies();check(feasible>0 && feasible0 && tightened>0,"integer integration coverage missing"); + std::cout<<"PASS "< +#include +#include +#include +#include +#include +#include +#include +#include + +namespace O=Gecode::Optimize; +using State=O::LpObservationState; +using Reason=O::LpObservationReason; +constexpr double inf=std::numeric_limits::infinity(); +static void near(double a,double b){assert(std::isfinite(a)&&std::abs(a-b)<1e-6);} +static void optimal(const O::LpObservedResult& out,double objective) { + if(out.result.termination!=O::Termination::Optimal) + std::cerr<<"unexpected solve status: "<dual_point().state!=State::Available) + std::cerr<<"unexpected dual status: "<dual_point().message<<'\n'; + assert(out.observations->primal_rows().state==State::Available); + assert(out.observations->dual_point().state==State::Available&&out.observations->checks().accepted); + near(*out.observations->checks().dual_objective_estimate,objective); + near(*out.observations->checks().normalized_gap,0); +} +static void no_duals(const O::LpObservedResult& out) { + assert(out.observations); + assert(out.observations->dual_point().state!=State::Available); + assert(out.observations->basis().state!=State::Available); + for(const auto& c:out.observations->columns())assert(!c.reduced_cost&&!c.basis); + for(const auto& r:out.observations->rows())assert(!r.dual&&!r.basis); +} +templatestatic void model_error(F&& fn){bool threw=false;try{fn();}catch(const O::ModelError&){threw=true;}assert(threw);} +int main(){ + O::Model model;auto x=model.add_continuous(),y=model.add_continuous(); + auto demand=model.add_row({{x,1},{y,1}},4,inf);model.minimize({{x,2},{y,3}},7); + const auto cap=O::lp_observation_capabilities(); + assert(cap.available==O::capabilities(O::Backend::Highs).available); + for(double bad:{-1.0,inf,std::numeric_limits::quiet_NaN()}) { + O::LpObservationOptions options;options.checks.complementarity=bad; + auto out=O::solve_lp_observed(model,options);assert(out.result.termination==O::Termination::InvalidModel&&!out.observations); + } + O::LpObservationOptions bad;bad.solve.backend=static_cast(99); + assert(O::solve_lp_observed(model,bad).result.termination==O::Termination::InvalidModel); + O::SolveSession session; + for(auto guarantee:{O::Guarantee::Exact,O::Guarantee::Certified}){ + O::LpObservationOptions options;options.solve.guarantee=guarantee; + auto out=session.solve_lp_observed(model,options);assert(out.result.termination==O::Termination::Unsupported); + assert(out.result.guarantee==guarantee);no_duals(out);assert(session.statistics().solve_calls==0); + } + O::LpObservationOptions native;native.solve.backend=O::Backend::Native; + auto unsupported=session.solve_lp_observed(model,native); + assert(unsupported.result.termination==O::Termination::Unsupported);no_duals(unsupported); + for(auto type:{O::VariableType::Integer,O::VariableType::Binary,O::VariableType::SemiContinuous,O::VariableType::SemiInteger}){ + O::Model typed;auto v=typed.add_variable(type,type==O::VariableType::Binary?0:1,1);typed.minimize({{v,1}}); + auto out=session.solve_lp_observed(typed);assert(out.result.termination==O::Termination::Unsupported);no_duals(out); + assert(out.observations->source().variables[0].type==type); + } + // An active empty native constraint must be rejected even without integer columns. + O::Model global;global.add_global(O::AllDifferentData{}); + auto global_out=session.solve_lp_observed(global); + assert(global_out.result.termination==O::Termination::Unsupported);no_duals(global_out); + assert(session.statistics().solve_calls==0); + if(!cap.available){ + auto out=session.solve_lp_observed(model);assert(out.result.termination==O::Termination::Unsupported); + no_duals(out);assert(out.observations->source().model_id==model.id()); + assert(out.observations->rows().size()==1&&out.observations->columns().size()==2); + std::cout<<"LP observations explicit missing-backend/admission contract passed\n";return 0; + } + assert(cap.duals&&cap.basis_export&&!cap.backend_version.empty()); + auto first=session.solve_lp_observed(model);optimal(first,15); + assert(first.observations->basis().state==State::Available); + near(first.result.value(x),4);near(first.result.value(y),0); + near(*first.observations->row(demand).dual,2); + near(*first.observations->column(x).reduced_cost,0);near(*first.observations->column(y).reduced_cost,1); + near(*first.observations->row(demand).activity,4);near(*first.observations->row(demand).lower_slack,0); + assert(!first.observations->row(demand).upper_slack); + assert(first.observations->metadata().backend_primal_tolerance&&first.observations->metadata().backend_dual_tolerance); + const auto original_revision=model.revision(); + model.minimize({{x,2},{y,1}},7); + auto second=session.solve_lp_observed(model);optimal(second,11);near(*second.observations->row(demand).dual,1); + near(*second.observations->column(x).reduced_cost,1); + assert(first.observations->revision()==original_revision&&first.observations->source().objective.terms[1].coefficient==3); + near(*first.observations->row(demand).dual,2); + // Same revision is not sufficient for cache compatibility or historical content identity. + auto snapshot=model.snapshot();snapshot.objective.terms[1].coefficient=5; + auto same_revision=session.solve_lp_observed(snapshot);optimal(same_revision,15); + assert(same_revision.observations->revision()==second.observations->revision()); + assert(same_revision.observations->source().objective.terms[1].coefficient==5); + snapshot.objective.terms[1].coefficient=100; + assert(same_revision.observations->source().objective.terms[1].coefficient==5); + O::LpObservationOptions zero;zero.solve.time_limit_seconds=0; + auto stopped=session.solve_lp_observed(model,zero);assert(stopped.result.termination==O::Termination::TimeLimit);no_duals(stopped); + assert(stopped.observations->primal_rows().reason==Reason::Interrupted); + auto ordinary=session.solve(model);assert(ordinary.termination==O::Termination::Optimal);near(*ordinary.objective,11); + optimal(session.solve_lp_observed(model),11); + O::LpObservationOptions cancelled;cancelled.solve.cancellation=std::make_shared(); + cancelled.solve.cancellation->cancel();auto stop=session.solve_lp_observed(model,cancelled); + assert(stop.result.termination==O::Termination::Cancelled);no_duals(stop); + model.set_bounds(demand,8,inf);model.set_bounds(y,0,3); + optimal(session.solve_lp_observed(model),20); + model.maximize({{x,-2},{y,-1}},-7);auto edited=session.solve_lp_observed(model);optimal(edited,-20); + auto cold=O::solve_lp_observed(model);optimal(cold,-20); + near(*edited.observations->row(demand).dual,*cold.observations->row(demand).dual); + session.reset();near(*first.observations->row(demand).dual,2); + O::SolveSession moved(std::move(session));auto invalid=session.solve_lp_observed(model); + assert(invalid.result.termination==O::Termination::InvalidModel);no_duals(invalid); + optimal(moved.solve_lp_observed(model),-20); + // Original min/max dual conventions, free columns and range sides. + for(int scenario=0;scenario<4;++scenario){ + O::Model m;auto v=m.add_continuous(-inf,inf); + auto r=m.add_row({{v,1}},scenario<2?-inf:1,scenario<2?4:3); + if(scenario==0)m.maximize({{v,2}},7); + else if(scenario==1)m.minimize({{v,-2}},7); + else m.minimize({{v,scenario==2?1.0:-1.0}}); + const double objective[]={15,-1,1,-3},dual[]={2,-2,1,-1}; + auto out=O::solve_lp_observed(m);optimal(out,objective[scenario]);near(*out.observations->row(r).dual,dual[scenario]); + assert(out.observations->basis().state==State::Available); + } + // Constant rows are independently evaluated; backend rows have their own map. + std::shared_ptr retained; + O::Variable historical;O::Constraint historical_row; + { + O::Model m;auto tomb=m.add_continuous();m.remove(tomb); + auto a=m.add_continuous(0,inf,"duplicate"),b=m.add_continuous(0,inf,"duplicate");historical=a; + auto gone=m.add_row({},-1,1);m.remove(gone); + auto r=m.add_row({{a,1}},2,inf);historical_row=r; + auto constant=m.add_row({},-1,1);auto s=m.add_row({{b,1}},3,inf); + m.minimize({{a,2},{b,3}},1);auto out=O::solve_lp_observed(m);optimal(out,14); + retained=out.observations; + assert(!retained->columns()[tomb.id].active&&!retained->rows()[gone.id].active); + near(*retained->row(r).dual,2);near(*retained->row(s).dual,3); + near(*retained->row(constant).activity,0);near(*retained->row(constant).lower_slack,1); + near(*retained->row(constant).upper_slack,1);near(*retained->row(constant).dual,0); + assert(retained->row(constant).dual_source==O::LpDualSource::DerivedConstantRow); + assert(retained->basis().reason==Reason::ElidedConstantRows); + model_error([&]{retained->column(tomb);});model_error([&]{retained->row(gone);}); + model_error([&]{retained->column(x);});model_error([&]{retained->row(demand);}); + model_error([&]{retained->column({m.id(),999});}); + } + near(*retained->column(historical).reduced_cost,0);near(*retained->row(historical_row).dual,2); + O::Model empty;empty.minimize({},7);auto empty_result=O::solve_lp_observed(empty); + assert(empty_result.result.termination==O::Termination::Optimal);near(*empty_result.result.objective,7); + assert(empty_result.observations->primal_rows().state==State::Available&&empty_result.observations->rows().empty()); + assert(empty_result.observations->dual_point().reason==Reason::NoBackendSolve); + O::LpObservationOptions primal_only;primal_only.duals=primal_only.basis=false; + auto p=O::solve_lp_observed(model,primal_only); + assert(p.result.has_solution()&&p.observations->primal_rows().state==State::Available); + assert(p.observations->dual_point().state==State::NotRequested&&p.observations->basis().state==State::NotRequested); + O::Model infeasible;auto iv=infeasible.add_continuous(0,1);infeasible.add_row({{iv,1}},2,inf); + auto infeas=O::solve_lp_observed(infeasible);assert(infeas.result.termination==O::Termination::Infeasible);no_duals(infeas); + O::Model unbounded;auto uv=unbounded.add_continuous();unbounded.minimize({{uv,-1}}); + auto unbound=O::solve_lp_observed(unbounded);assert(unbound.result.termination==O::Termination::Unbounded);no_duals(unbound); + auto malformed=model.snapshot();malformed.rows[0].terms[0].variable.model_id=0; + auto malformed_result=O::solve_lp_observed(malformed);assert(malformed_result.result.termination==O::Termination::InvalidModel&&!malformed_result.observations); + std::cout<<"LP observations real-backend analytic, mapping, ownership and session checks passed\n"; +} diff --git a/test/optimize/lp_observations_c.c b/test/optimize/lp_observations_c.c new file mode 100644 index 0000000000..ba8b055717 --- /dev/null +++ b/test/optimize/lp_observations_c.c @@ -0,0 +1,119 @@ +/* C99 owning LP-observation ABI conformance and independent analytic duals. */ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +#define OK(call) do { int code=(call); if(code)fprintf(stderr,"%s: %s\n",#call,gecode_opt_v1_last_error());assert(code==GECODE_OPT_OK); } while(0) +#define LPINFO(h,i) OK(gecode_opt_v1_lp_observed_result_info(h,&i,sizeof(i))) +static void absent(gecode_opt_optional_number_v1 n){assert(!n.present&&n.value==0&&!n.reserved);} +static void present(gecode_opt_optional_number_v1 n,double x){assert(n.present&&!n.reserved&&fabs(n.value-x)<1e-6);} +static int available; +static void analytic(int maximize,double offset){ + gecode_opt_handle model=0,observed=0,copy=0,session=0; + gecode_opt_id x,y,removed,dead,row;gecode_opt_lp_options_v1 options; + gecode_opt_lp_info_v1 info;gecode_opt_lp_row_v1 r,rows[2];gecode_opt_lp_column_v1 c; + gecode_opt_lp_checks_v1 checks;gecode_opt_lp_group_v1 group;gecode_opt_lp_metadata_v1 metadata; + uint64_t needed=0;double value=0;gecode_opt_result_info_v1 ordinary; + OK(gecode_opt_v1_model_create(&model)); + OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_CONTINUOUS,0,10,"x",&x)); + OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_CONTINUOUS,0,10,"y",&y)); + OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_CONTINUOUS,0,10,"removed",&removed)); + OK(gecode_opt_v1_model_remove_variable(model,removed)); + OK(gecode_opt_v1_model_add_row(model,NULL,0,-INFINITY,INFINITY,"dead",&dead)); + OK(gecode_opt_v1_model_remove_row(model,dead)); + {gecode_opt_term terms[2]={{x,1},{y,1}}; + OK(gecode_opt_v1_model_add_row(model,terms,2,maximize?-INFINITY:3,maximize?3:INFINITY,"demand",&row)); + terms[0].coefficient=maximize?4:1;terms[1].coefficient=maximize?1:2; + OK(gecode_opt_v1_model_set_objective(model,terms,2,maximize?GECODE_OPT_MAXIMIZE:GECODE_OPT_MINIMIZE,offset));} + OK(gecode_opt_v1_lp_options_default(&options,sizeof(options))); + assert(options.struct_size==sizeof(options)&&!options.reserved&&options.duals&&options.basis); + options.solve.backend=GECODE_OPT_HIGHS; + OK(gecode_opt_v1_session_create(&session)); + OK(gecode_opt_v1_session_solve_lp_observed(session,model,&options,&observed)); + LPINFO(observed,info);assert(info.struct_size==sizeof(info)&&!info.reserved&&!info.reserved_flags&&info.has_observations); + assert(info.row_slots==2&&info.column_slots==3&&info.model_id==x.model_id); + assert(gecode_opt_v1_result_info(observed,&ordinary,sizeof(ordinary))==GECODE_OPT_INVALID_HANDLE); + assert(gecode_opt_v1_lp_observed_result_copy_result(model,©)==GECODE_OPT_INVALID_HANDLE&©==0); + assert(gecode_opt_v1_lp_observed_result_info(observed,&info,sizeof(info)-1)==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_lp_observed_result_group(observed,GECODE_OPT_LP_PRIMAL_ROWS,&group,sizeof(group))); + assert(gecode_opt_v1_lp_observed_result_group(observed,99,&group,sizeof(group))==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_lp_observed_result_row(observed,row,&r,sizeof(r))); + assert(gecode_opt_v1_lp_observed_result_row(observed,dead,&r,sizeof(r))==GECODE_OPT_MODEL_ERROR); + {gecode_opt_id foreign=row;foreign.model_id++;assert(gecode_opt_v1_lp_observed_result_row(observed,foreign,&r,sizeof(r))==GECODE_OPT_MODEL_ERROR);} + assert(gecode_opt_v1_lp_observed_result_row(observed,x,&r,sizeof(r))==GECODE_OPT_INVALID_ARGUMENT); + assert(gecode_opt_v1_lp_observed_result_column(observed,removed,&c,sizeof(c))==GECODE_OPT_MODEL_ERROR); + OK(gecode_opt_v1_lp_observed_result_rows(observed,NULL,sizeof(r),0,&needed));assert(needed==2); + memset(rows,0xa5,sizeof(rows)); + assert(gecode_opt_v1_lp_observed_result_rows(observed,rows,sizeof(r),1,&needed)==GECODE_OPT_BUFFER_TOO_SMALL); + {unsigned char expected[sizeof(rows)];memset(expected,0xa5,sizeof(expected));assert(!memcmp(rows,expected,sizeof(rows)));} + assert(gecode_opt_v1_lp_observed_result_rows(observed,rows,sizeof(r)-1,2,&needed)==GECODE_OPT_INVALID_ARGUMENT); + assert(gecode_opt_v1_lp_observed_result_rows(observed,rows,sizeof(r),UINT64_MAX,&needed)==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_lp_observed_result_rows(observed,rows,sizeof(r),2,&needed));assert(!rows[0].active&&rows[1].active);absent(rows[0].activity); + OK(gecode_opt_v1_lp_observed_result_checks(observed,&checks,sizeof(checks))); + OK(gecode_opt_v1_lp_observed_result_metadata(observed,&metadata,sizeof(metadata))); + assert(!checks.reserved&&!metadata.reserved&&metadata.objective_gap==options.objective_gap); + OK(gecode_opt_v1_lp_observed_result_copy_result(observed,©)); + if(available){ + assert(info.result.termination==GECODE_OPT_OPTIMAL&&info.result.has_solution&&checks.accepted); + present(rows[1].activity,3);present(rows[1].dual,maximize?4:1); + if(maximize){absent(rows[1].lower_slack);present(rows[1].upper_slack,0);}else{present(rows[1].lower_slack,0);absent(rows[1].upper_slack);} + assert(rows[1].dual_source==GECODE_OPT_LP_DUAL_BACKEND&&rows[1].has_basis); + OK(gecode_opt_v1_lp_observed_result_column(observed,y,&c,sizeof(c)));present(c.reduced_cost,maximize?-3:1);assert(c.has_basis&&c.basis==GECODE_OPT_LP_BASIS_LOWER); + present(checks.normalized_gap,0);assert(metadata.backend_primal_tolerance.present&&metadata.backend_dual_tolerance.present); + OK(gecode_opt_v1_result_value(copy,x,&value));assert(value==3); + }else{assert(info.result.termination==GECODE_OPT_UNSUPPORTED&&!info.result.has_solution&&!checks.accepted);absent(rows[1].dual);absent(checks.normalized_gap);absent(metadata.backend_dual_tolerance);} + /* Editing and a stopped call must never expose the preceding basis/duals. */ + OK(gecode_opt_v1_model_set_row_bounds(model,row,maximize?-INFINITY:4,maximize?4:INFINITY)); + {gecode_opt_handle stopped=0;gecode_opt_lp_options_v1 zero=options;zero.solve.time_limit_seconds=0; + OK(gecode_opt_v1_session_solve_lp_observed(session,model,&zero,&stopped));LPINFO(stopped,info); + assert(info.result.termination==(available?GECODE_OPT_TIME_LIMIT:GECODE_OPT_UNSUPPORTED)); + OK(gecode_opt_v1_lp_observed_result_row(stopped,row,&r,sizeof(r)));absent(r.activity);absent(r.dual);assert(!r.has_basis); + OK(gecode_opt_v1_lp_observed_result_destroy(stopped));} + OK(gecode_opt_v1_model_destroy(model));OK(gecode_opt_v1_session_destroy(session)); + OK(gecode_opt_v1_lp_observed_result_row(observed,row,&r,sizeof(r)));if(available)present(r.activity,3); + OK(gecode_opt_v1_lp_observed_result_destroy(observed)); + if(available){OK(gecode_opt_v1_result_value(copy,x,&value));assert(value==3);} + OK(gecode_opt_v1_result_destroy(copy)); + assert(gecode_opt_v1_lp_observed_result_destroy(observed)==GECODE_OPT_INVALID_HANDLE); +} +static void states(void){ + gecode_opt_handle model=0,result=0;gecode_opt_id x,row,constant,global; + gecode_opt_lp_options_v1 options;gecode_opt_lp_group_v1 group;gecode_opt_lp_row_v1 observation; + gecode_opt_lp_info_v1 info; + OK(gecode_opt_v1_model_create(&model));OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_CONTINUOUS,0,10,"x",&x)); + {gecode_opt_term term={x,1};OK(gecode_opt_v1_model_add_row(model,&term,1,2,INFINITY,"r",&row));OK(gecode_opt_v1_model_set_objective(model,&term,1,GECODE_OPT_MINIMIZE,7));} + OK(gecode_opt_v1_model_add_row(model,NULL,0,-1,1,"constant",&constant)); + OK(gecode_opt_v1_solve_lp_observed(model,NULL,&result)); + OK(gecode_opt_v1_lp_observed_result_group(result,GECODE_OPT_LP_BASIS,&group,sizeof(group))); + if(available){assert(group.state==GECODE_OPT_LP_UNAVAILABLE&&group.reason==GECODE_OPT_LP_REASON_ELIDED_CONSTANT_ROWS); + OK(gecode_opt_v1_lp_observed_result_row(result,constant,&observation,sizeof(observation)));present(observation.dual,0);assert(observation.dual_source==GECODE_OPT_LP_DUAL_DERIVED_CONSTANT_ROW&&!observation.has_basis);} + OK(gecode_opt_v1_lp_observed_result_destroy(result)); + for(int mode=0;mode<7;++mode){ + OK(gecode_opt_v1_lp_options_default(&options,sizeof(options))); + if(mode==0)options.duals=options.basis=0; + if(mode==1)options.solve.backend=GECODE_OPT_NATIVE; + if(mode==2)options.solve.guarantee=GECODE_OPT_EXACT; + if(mode==3){OK(gecode_opt_v1_model_add_all_different(model,NULL,0,"empty",&global));} + if(mode==4){OK(gecode_opt_v1_model_remove_global(model,global));OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_INTEGER,0,0,"fixed integer",&global));} + if(mode==5){options.reserved=1;result=123;assert(gecode_opt_v1_solve_lp_observed(model,&options,&result)==GECODE_OPT_INVALID_ARGUMENT&&result==0);continue;} + if(mode==6){options.solve.struct_size--;result=123;assert(gecode_opt_v1_solve_lp_observed(model,&options,&result)==GECODE_OPT_INVALID_ARGUMENT&&result==0);continue;} + OK(gecode_opt_v1_solve_lp_observed(model,&options,&result));LPINFO(result,info); + if(mode==0){OK(gecode_opt_v1_lp_observed_result_group(result,GECODE_OPT_LP_DUAL_POINT,&group,sizeof(group)));assert(group.state==GECODE_OPT_LP_NOT_REQUESTED);} + else assert(info.result.termination==GECODE_OPT_UNSUPPORTED&&!info.result.has_solution); + OK(gecode_opt_v1_lp_observed_result_destroy(result)); + } + OK(gecode_opt_v1_model_destroy(model)); +} +int main(void){ + gecode_opt_lp_capabilities_v1 caps;char buffer[128];uint64_t n=0; + OK(gecode_opt_v1_lp_capabilities(&caps,sizeof(caps)));available=caps.available; + assert(caps.struct_size==sizeof(caps)&&!caps.reserved&&!caps.reserved_flags&&caps.limitation_count>0); + OK(gecode_opt_v1_lp_capability_text(GECODE_OPT_LP_CAP_BACKEND,0,buffer,sizeof(buffer),&n));assert(n>1); + assert(gecode_opt_v1_lp_capability_text(GECODE_OPT_LP_CAP_BACKEND,1,buffer,sizeof(buffer),&n)==GECODE_OPT_INVALID_ARGUMENT); + for(int sense=0;sense<2;++sense){analytic(sense,7);analytic(sense,1e16);}states(); + puts("LP observation C99 conformance passed");return 0; +} diff --git a/test/optimize/lp_observations_checks.cpp b/test/optimize/lp_observations_checks.cpp new file mode 100644 index 0000000000..bec77ccacb --- /dev/null +++ b/test/optimize/lp_observations_checks.cpp @@ -0,0 +1,190 @@ +#include +#include +#include +#include +#include +#include + +namespace O=Gecode::Optimize; +namespace D=Gecode::Optimize::Detail; +using State=O::LpObservationState; +using Reason=O::LpObservationReason; +using Basis=O::LpBasisStatus; +constexpr double inf=std::numeric_limits::infinity(); +static void near(double a,double b) { assert(std::isfinite(a)&&std::abs(a-b)<1e-8); } +struct Fixture { + O::ModelSnapshot model; + O::SolveResult result; + D::LpBackendObservations raw; + O::LpObservationOptions options; +}; +static Fixture fixture() { + O::Model model; + auto x=model.add_continuous(0,10),y=model.add_continuous(0,10); + model.add_row({{x,1},{y,1}},4,inf); + model.minimize({{x,2},{y,3}},7); + Fixture f;f.model=model.snapshot(); + f.result.model_id=model.id();f.result.revision=model.revision(); + f.result.backend="test-only owned data";f.result.backend_version="1"; + f.result.termination=O::Termination::Optimal;f.result.values={4,0}; + f.result.active_variables={true,true};f.result.objective=15;f.result.solution_validated=true; + f.raw.attempted=f.raw.timely=f.raw.complete=true; + f.raw.info_valid=f.raw.value_valid=f.raw.primal_feasible=true; + f.raw.dual_valid=f.raw.dual_feasible=f.raw.basis_valid=f.raw.info_basis_valid=true; + f.raw.model_id=model.id();f.raw.revision=model.revision(); + f.raw.column_slots={0,1};f.raw.row_slots={0}; + f.raw.column_duals={0,1};f.raw.row_duals={2}; + f.raw.column_basis={Basis::Basic,Basis::Lower};f.raw.row_basis={Basis::Lower}; + f.raw.primal_tolerance=1e-7;f.raw.dual_tolerance=1e-7; + return f; +} +static std::shared_ptr inspect(const Fixture& f) { + auto out=D::LpObservationAccess::create(f.model,f.options); + O::SolveBudget budget(f.options.solve); + D::LpObservationAccess::finish(*out,f.result,f.raw,budget); + return out; +} +static void rejected_dual(const Fixture& f,Reason reason=Reason::InvalidBackendData) { + auto out=inspect(f); + assert(out->primal_rows().state==State::Available); + assert(out->dual_point().state==State::Rejected&&out->dual_point().reason==reason); + for(const auto& c:out->columns()) assert(!c.reduced_cost); + for(const auto& r:out->rows()) assert(!r.dual&&r.dual_source==O::LpDualSource::None); +} +int main() { + auto f=fixture();auto good=inspect(f); + assert(good->primal_rows().state==State::Available); + assert(good->dual_point().state==State::Available&&good->basis().state==State::Available); + assert(good->checks().accepted);near(*good->checks().normalized_gap,0); + near(*good->checks().dual_objective_estimate,15); + near(*good->rows()[0].activity,4);near(*good->rows()[0].lower_slack,0); + near(*good->columns()[1].reduced_cost,1); + // Each validity flag is independently necessary; unavailable is not a zero dual. + for(int flag=0;flag<7;++flag) { + f=fixture(); + bool* flags[]={&f.raw.info_valid,&f.raw.value_valid,&f.raw.primal_feasible, + &f.raw.dual_valid,&f.raw.dual_feasible,&f.raw.basis_valid,&f.raw.info_basis_valid}; + *flags[flag]=false;auto out=inspect(f); + if(flag<3) {assert(out->dual_point().reason==Reason::NoPrimalPoint);assert(out->basis().reason==Reason::NoPrimalPoint);} + else if(flag<5) {assert(out->dual_point().reason==Reason::NoDualPoint);assert(out->basis().state==State::Available);} + else {assert(out->dual_point().state==State::Available);assert(out->basis().reason==Reason::NoBasis);} + } + for(double bad:{inf,-inf,std::numeric_limits::quiet_NaN()}) { + f=fixture();f.raw.row_duals[0]=bad;rejected_dual(f); + f=fixture();f.raw.column_duals[1]=bad;rejected_dual(f); + } + for(int mode=0;mode<4;++mode) { + f=fixture(); + if(mode==0)f.raw.row_duals.clear(); + if(mode==1)f.raw.row_duals.push_back(0); + if(mode==2)f.raw.column_duals.pop_back(); + if(mode==3)f.raw.column_duals.push_back(0); + rejected_dual(f);assert(inspect(f)->basis().state==State::Available); + } + for(int mode=0;mode<8;++mode) { + f=fixture(); + if(mode==0)std::swap(f.raw.column_slots[0],f.raw.column_slots[1]); + if(mode==1)f.raw.row_slots.clear(); + if(mode==2)f.raw.column_slots.push_back(2); + if(mode==3)++f.raw.model_id; + if(mode==4)++f.raw.revision; + if(mode==5)f.raw.complete=false; + if(mode==6)f.raw.primal_tolerance=-1; + if(mode==7)f.raw.dual_tolerance=std::numeric_limits::quiet_NaN(); + rejected_dual(f);assert(inspect(f)->basis().state==State::Rejected); + } + for(int mode=0;mode<7;++mode) { + f=fixture(); + if(mode==0)++f.result.model_id; + if(mode==1)++f.result.revision; + if(mode==2)f.result.active_variables.pop_back(); + if(mode==3)f.result.active_variables[1]=false; + if(mode==4)f.result.objective=14; + if(mode==5)f.result.values[0]=3; + if(mode==6)f.result.values[0]=11; + auto out=inspect(f);assert(out->primal_rows().state==State::Rejected); + assert(!out->rows()[0].activity&&out->dual_point().state==State::Rejected); + } + for(int mode=0;mode<5;++mode) { + f=fixture(); + if(mode==0)f.raw.column_basis[0]=Basis::Lower; + if(mode==1)f.raw.column_basis[0]=static_cast(99); + if(mode==2)f.raw.row_basis[0]=Basis::Basic; + if(mode==3)f.raw.column_basis[1]=Basis::Zero; + if(mode==4)f.raw.row_basis.clear(); + auto out=inspect(f);assert(out->basis().state==State::Rejected); + assert(out->dual_point().state==State::Available); + assert(!out->columns()[0].basis&&!out->rows()[0].basis); + } + f=fixture();f.raw.column_duals[1]=0;rejected_dual(f,Reason::FailedChecks); + auto out=inspect(f);assert(!out->checks().stationarity_valid);near(*out->checks().normalized_gap,0); + f=fixture();f.raw.row_duals[0]=1;f.raw.column_duals={1,2}; + out=inspect(f);assert(out->checks().stationarity_valid&&!out->checks().dual_signs_valid); + assert(!out->checks().complementarity_valid&&!out->checks().gap_valid); + f=fixture();f.raw.row_duals[0]=3;f.raw.column_duals={0,0}; + out=inspect(f);near(*out->checks().normalized_gap,-4);assert(!out->checks().accepted); + // A tiny multiplier still requires a finite endpoint; never replace inf*epsilon by zero. + f=fixture();f.model.variables[0].upper=inf;f.raw.column_duals[0]=-1e-12; + out=inspect(f);assert(!out->checks().dual_objective_estimate&&!out->checks().normalized_gap); + assert(out->dual_point().state==State::Rejected); + // Large common offsets cannot hide a nonzero normalized primal/dual gap. + f=fixture();f.model.objective.offset=1e16;f.result.objective=1e16+8; + f.raw.row_duals[0]=2.0001;f.raw.column_duals={-0.0001,0.9999}; + out=inspect(f);assert(out->checks().stationarity_valid&&!out->checks().gap_valid); + assert(*out->checks().normalized_gap>0.0005); + assert(*out->checks().dual_objective_estimate==*f.result.objective); + // A^T*pi = 1e16 + 1 - 1e16 must retain the unit residual. + { + O::Model model;auto x=model.add_continuous(1,1); + model.add_row({{x,1e16}},1e16,1e16);model.add_row({{x,1}},1,1); + model.add_row({{x,-1e16}},-1e16,-1e16);model.minimize({{x,1}}); + f=fixture();f.model=model.snapshot();f.result.model_id=model.id();f.result.revision=model.revision(); + f.result.values={1};f.result.active_variables={true};f.result.objective=1; + f.raw.model_id=model.id();f.raw.revision=model.revision();f.raw.column_slots={0};f.raw.row_slots={0,1,2}; + f.raw.column_duals={0};f.raw.row_duals={1,1,1};f.raw.basis_valid=false; + out=inspect(f);assert(out->checks().accepted);near(*out->checks().max_stationarity,0); + } + // Retain a row's residual for slack, normal-cone, and basis checks before narrowing. + { + O::Model model;auto x=model.add_continuous(1,1),y=model.add_continuous(1,1); + model.add_row({{x,1e16},{y,1}},1e16,inf);model.minimize({}); + f=fixture();f.model=model.snapshot();f.result.model_id=model.id();f.result.revision=model.revision(); + f.result.values={1,1};f.result.objective=0; + f.raw.model_id=model.id();f.raw.revision=model.revision();f.raw.row_duals={0};f.raw.column_duals={0,0}; + f.raw.column_basis={Basis::Lower,Basis::Lower};f.raw.row_basis={Basis::Basic}; + out=inspect(f);assert(out->checks().accepted&&out->basis().state==State::Available); + near(*out->rows()[0].lower_slack,1); + f.raw.row_basis={Basis::Lower};f.raw.column_basis[0]=Basis::Basic; + assert(inspect(f)->basis().state==State::Rejected); + f.raw.row_duals={1};f.raw.column_duals={-1e16,-1}; + out=inspect(f);assert(!out->checks().dual_signs_valid);near(*out->checks().max_complementarity,1); + } + f=fixture();f.result.termination=O::Termination::TimeLimit; + out=inspect(f);assert(out->primal_rows().state==State::Available&&out->dual_point().reason==Reason::NotOptimal); + f=fixture();f.raw.timely=false;out=inspect(f);assert(out->dual_point().reason==Reason::Interrupted); + f=fixture();f.raw.attempted=false;out=inspect(f);assert(out->dual_point().reason==Reason::NoBackendSolve); + f=fixture();f.raw.failure=D::LpCaptureFailure::Allocation; + rejected_dual(f,Reason::AllocationFailure); + f=fixture();f.options.duals=f.options.basis=false;out=inspect(f); + assert(out->primal_rows().state==State::Available&&out->dual_point().state==State::NotRequested&&out->basis().state==State::NotRequested); + for(double bad:{-1.0,inf,std::numeric_limits::quiet_NaN()}) { + f=fixture();f.options.checks.stationarity=bad;bool threw=false; + try{inspect(f);}catch(const O::ModelError&){threw=true;}assert(threw); + } + // A deterministic post-collection cancellation models cleanup crossing the final gate. + // Production uses this same gate after raw vectors have been destroyed; no sleeps/hooks. + f=fixture();f.options.solve.cancellation=std::make_shared(); + O::SolveBudget budget(f.options.solve);auto mutable_observation=D::LpObservationAccess::create(f.model,f.options); + {auto raw=f.raw;D::LpObservationAccess::finish(*mutable_observation,f.result,raw,budget);} + assert(mutable_observation->dual_point().state==State::Available); + O::LpObservedResult observed{f.result,mutable_observation}; + f.options.solve.cancellation->cancel(); + D::LpObservationAccess::final_budget(observed,mutable_observation,budget); + assert(observed.result.termination==O::Termination::Cancelled&&observed.result.has_solution()); + assert(observed.observations->primal_rows().reason==Reason::Interrupted); + assert(observed.observations->dual_point().reason==Reason::Interrupted); + assert(!observed.observations->checks().accepted&&!observed.observations->rows()[0].activity); + f=fixture();f.options.solve.time_limit_seconds=0;out=inspect(f); + assert(out->primal_rows().reason==Reason::Interrupted&&!out->rows()[0].activity); + std::cout<<"LP observation independent analytic/corruption checks passed\n"; +} diff --git a/test/optimize/lp_propagator.cpp b/test/optimize/lp_propagator.cpp new file mode 100644 index 0000000000..d5188784c7 --- /dev/null +++ b/test/optimize/lp_propagator.cpp @@ -0,0 +1,307 @@ +// End-to-end correctness of the redundant LP bound actor. +// The oracle enumerates binary assignments independently of both solvers. +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace LP = Gecode::Experimental::LpRelaxation; +using I = std::int64_t; +using V = std::vector; + +static void require(bool condition, const char* message) { + if (!condition) + throw std::runtime_error(message); +} + +enum class Mode { Native, Root, EveryNode, RootFix, EveryFix, ThrottledFix }; + +struct Instance { + LP::LinearModel model; + V lower, upper; + bool sparse=false; +}; + +static I objective(const Instance& instance, unsigned int mask) { + I result=0; + for (std::size_t j=0; j((mask>>j)&1U); + return result; +} + +static std::set enumerate(const Instance& instance) { + const auto& model=instance.model; + const std::size_t n=model.c.size(); + std::set result; + for (unsigned int mask=0; mask<(1U<>j)&1U; + if (xinstance.upper[j]) + feasible=false; + } + for (std::size_t i=0; feasible && i((mask>>j)&1U); + if (activity(0,cost) : std::min(0,cost); + return static_cast(result); +} + +class Problem : public Gecode::Space { +public: + Gecode::IntVarArray x; + Gecode::IntVar z; + + Problem(const Instance& instance, Mode mode, + const std::shared_ptr& backend) + : x(*this,static_cast(instance.model.c.size()),0,1), + z(*this,objective_endpoint(instance.model,false), + objective_endpoint(instance.model,true)) { + for (int j=0; j(instance.lower[j]), + static_cast(instance.upper[j])); + if (mode==Mode::Native) { + if (instance.sparse) LP::post_native(*this,x,z,backend->model); + else LP::post_native(*this,x,z,instance.model); + } else { + LP::Options policy; + policy.frequency=(mode==Mode::Root || mode==Mode::RootFix) + ? LP::Frequency::Root : LP::Frequency::EveryNode; + policy.reduced_cost_fixing=mode==Mode::RootFix || mode==Mode::EveryFix || mode==Mode::ThrottledFix; + policy.assignment_interval=mode==Mode::ThrottledFix ? 4 : 1; + LP::binary_linear_minimize(*this,x,z,backend,policy); + } + Gecode::branch(*this,x,Gecode::INT_VAR_NONE(),Gecode::INT_VAL_MIN()); + } + Problem(Problem& other) : Gecode::Space(other) { + x.update(*this,other.x); + z.update(*this,other.z); + } + Gecode::Space* copy(void) override { return new Problem(*this); } + void constrain(const Gecode::Space& best) override { + const auto& other=static_cast(best); + Gecode::rel(*this,z,Gecode::IRT_LE,other.z.val()); + } + unsigned int mask(void) const { + unsigned int result=0; + for (int j=0; j(x[j].val())<& backend, + const Gecode::Search::Options& options, + const std::set& expected) { + { + Problem* root=new Problem(instance,mode,backend); + Gecode::DFS engine(root,options); + delete root; + std::set actual; + while (Problem* raw=engine.next()) { + std::unique_ptr solution(raw); + const unsigned int assignment=solution->mask(); + require(expected.count(assignment)==1,"DFS returned infeasible assignment"); + require(solution->z.assigned(),"DFS objective is not assigned"); + require(solution->z.val()==objective(instance,assignment), + "DFS returned an incorrect objective"); + require(actual.insert(assignment).second,"DFS returned duplicate assignment"); + } + require(!engine.stopped(),"DFS did not complete"); + require(actual==expected,"LP actor changed the complete solution set"); + } + require(backend.use_count()==1,"backend retained after DFS destruction"); +} + +static void check_bab(const Instance& instance, Mode mode, + const std::shared_ptr& backend, + const Gecode::Search::Options& options, + const std::set& expected) { + I optimum=std::numeric_limits::max(); + for (unsigned int mask : expected) + optimum=std::min(optimum,objective(instance,mask)); + { + Problem* root=new Problem(instance,mode,backend); + Gecode::BAB engine(root,options); + delete root; + I best=std::numeric_limits::max(); + bool found=false; + while (Problem* raw=engine.next()) { + std::unique_ptr solution(raw); + const unsigned int assignment=solution->mask(); + require(expected.count(assignment)==1,"BAB returned infeasible assignment"); + const I candidate=objective(instance,assignment); + require(solution->z.assigned() && solution->z.val()==candidate, + "BAB returned an incorrect objective"); + require(!found || candidate instances(void) { + std::vector result; + // A triangle cover needs two binary variables but has LP optimum 1.5. + result.push_back({{{1,1,0, 0,1,1, 1,0,1},{1,1,1},{1,1,1}}, + {0,0,0},{1,1,1}}); + // Signed objective: independent set on a triangle has LP -1.5, integer -1. + result.push_back({{{-1,-1,0, 0,-1,-1, -1,0,-1},{-1,-1,-1},{-1,-1,-1}}, + {0,0,0},{1,1,1}}); + // Contradictory equalities whose individual rows do not initially fix x. + result.push_back({{{1,1, -1,-1, 1,-1, -1,1},{1,-1,0,0},{-2,3}}, + {0,0},{1,1}}); + result.push_back({{{1},{2},{-1}},{0},{1}}); // Immediately infeasible. + result.push_back({{{},{},{-3,0,2}},{0,0,0},{1,1,1}}); // No rows. + result.push_back({{{1,-1,2},{0},{-3,4,-2}},{1,0,1},{1,0,1}}); // All fixed. + result.push_back({{{0,0},{1},{-1,2}},{0,0},{1,1}}); // Empty impossible row. + result.push_back({{{},{},{}},{},{}}); // Empty feasible model. + + std::mt19937 random(289417); + while (result.size()<120) { + const std::size_t id=result.size(); + const std::size_t n=3+random()%5; + const std::size_t m=1+random()%6; + Instance instance; + auto& model=instance.model; + model.a.resize(n*m); + model.b.resize(m); + model.c.resize(n); + instance.lower.resize(n); + instance.upper.resize(n); + V witness(n); + for (std::size_t j=0; j(random()%17)-8; + } + for (std::size_t i=0; i(random()%11)-5; + activity+=model.a[i*n+j]*witness[j]; + } + model.b[i]=(id%2==0) ? activity-static_cast(random()%4) + : static_cast(random()%17)-8; + } + result.push_back(std::move(instance)); + } + return result; +} + +static void check_lifecycle_and_root_bounds(const std::vector& cases) { + for (std::size_t id : {std::size_t(0),std::size_t(1)}) { + std::shared_ptr backend=std::make_shared(cases[id].model); + std::weak_ptr weak=backend; + const int expected=id==0 ? 2 : -1; + for (Mode mode : {Mode::Root,Mode::EveryNode}) { + { + std::unique_ptr root(new Problem(cases[id],mode,backend)); + require(backend.use_count()>1,"LP actor did not retain its backend"); + } // Destruction before the first propagation call. + require(backend.use_count()==1,"unpropagated actor leaked its backend"); + { + std::unique_ptr root(new Problem(cases[id],mode,backend)); + require(root->status()!=Gecode::SS_FAILED,"feasible root failed"); + require(root->z.min()==expected,"LP root certificate did not tighten objective"); + std::unique_ptr clone(static_cast(root->clone())); + require(clone->status()!=Gecode::SS_FAILED,"feasible cloned root failed"); + } + require(backend.use_count()==1,"cloned actor leaked its backend"); + { + Gecode::Search::Options options; + options.threads=1; + options.c_d=32; + options.a_d=16; + Problem* root=new Problem(cases[id],mode,backend); + Gecode::DFS engine(root,options); + delete root; + std::unique_ptr first(engine.next()); + require(first!=nullptr,"early-stop search found no solution"); + } // Unexplored sibling spaces are destroyed here. + require(backend.use_count()==1,"early-stop search leaked its backend"); + } + require(backend->statistics().lp_calls>0,"no LP calls reached the backend"); + require(backend->statistics().valid_bounds>0,"no bound was certified"); + backend.reset(); + require(weak.expired(),"backend lifetime outlived all its owners"); + } +} + +int main(void) { + try { + const auto cases=instances(); + check_lifecycle_and_root_bounds(cases); + unsigned int runs=0, feasible=0, infeasible=0; + std::uint64_t lp_calls=0, certified=0; + for (const Instance& original : cases) for (bool sparse : {false,true}) { + Instance instance=original; instance.sparse=sparse; + const auto expected=enumerate(instance); + if (expected.empty()) ++infeasible; else ++feasible; + std::shared_ptr backend; + if (sparse) backend=std::make_shared(LP::sparse_model(instance.model)); + else backend=std::make_shared(instance.model); + std::weak_ptr weak=backend; + // Reuse one backend across independent searches, after traversing + // siblings with opposite bound fixings and different recomputation. + for (unsigned int distance : {1U,8U,32U}) + for (Mode mode : {Mode::Native,Mode::Root,Mode::EveryNode, + Mode::RootFix,Mode::EveryFix,Mode::ThrottledFix}) { + Gecode::Search::Options options; + options.threads=1; + options.c_d=distance; + options.a_d=16; + check_dfs(instance,mode,backend,options,expected); + check_bab(instance,mode,backend,options,expected); + runs+=2; + } + lp_calls+=backend->statistics().lp_calls; + certified+=backend->statistics().valid_bounds; + backend.reset(); + require(weak.expired(),"backend leaked after completing an instance"); + } + require(feasible>0 && infeasible>0,"both feasibility outcomes must be tested"); + require(lp_calls>0 && certified>0,"LP integration was not exercised"); + std::cout << "PASS " << cases.size() << " models in two storage forms (" << feasible + << " feasible representations, " << infeasible << " infeasible representations), " << runs + << " exhaustive dense/sparse native/bounds/fixing/throttled DFS/BAB configurations; " + << lp_calls << " LP calls, " << certified + << " certified bounds; backend lifecycle and root tightening\n"; + return EXIT_SUCCESS; + } catch (const std::exception& error) { + std::cerr << "FAIL: " << error.what() << '\n'; + return EXIT_FAILURE; + } +} diff --git a/test/optimize/lp_reduced_cost.cpp b/test/optimize/lp_reduced_cost.cpp new file mode 100644 index 0000000000..4c79c6284a --- /dev/null +++ b/test/optimize/lp_reduced_cost.cpp @@ -0,0 +1,281 @@ +// Certified conditional bounds and throttled LP propagation. +// Feasibility and objective oracles enumerate assignments independently. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace LP=Gecode::Experimental::LpRelaxation; +namespace Cert=Gecode::Experimental::LpCertificate; +using I=std::int64_t; +using V=std::vector; +static void require(bool condition,const char* message) { + if (!condition) throw std::runtime_error(message); +} +struct Instance { LP::LinearModel model; V lower,upper; }; +static I cost(const Instance& p,unsigned mask) { + I value=0; + for (std::size_t j=0;j>j)&1U); + return value; +} +static std::set oracle(const Instance& p,I cutoff) { + std::set result; + const auto n=p.model.c.size(); + for (unsigned mask=0;mask<(1U<>j)&1U; + feasible&=p.lower[j]<=value && value<=p.upper[j]; + } + for (std::size_t i=0;feasible && i>j)&1U); + feasible=sum>=p.model.b[i]; + } + if (feasible) result.insert(mask); + } + return result; +} +static int endpoint(const LP::LinearModel& p,bool upper) { + I result=0; + for (I c:p.c) result+=upper ? std::max(0,c) : std::min(0,c); + return static_cast(result); +} +static Instance random_instance(std::mt19937& random,unsigned id) { + Instance p; + const std::size_t n=2+random()%7,m=random()%7; + p.model.c.resize(n);p.model.b.resize(m);p.model.a.resize(n*m); + p.lower.resize(n);p.upper.resize(n); + V witness(n); + for (std::size_t j=0;j(random()%21)-10; + } + for (std::size_t i=0;i(random()%13)-6; + activity+=p.model.a[i*n+j]*witness[j]; + } + p.model.b[i]=id%2 ? static_cast(random()%25)-12 : activity-random()%5; + } + return p; +} + +static void check_certificate() { + Cert::Certificate empty; + I bound=999; + std::vector forbidden={7}; + require(!empty.filter({0},{1},0,bound,forbidden) && bound==999 && forbidden[0]==7, + "uninitialized certificate changed outputs"); + require(Cert::prepare({-1},{-1},{-2},{0.5},empty),"signed preparation"); + require(empty.filter({0},{1},-1,bound,forbidden) && bound==-2 && forbidden[0]==1, + "signed conditional ceiling"); + require(!empty.filter({-1},{1},-1,bound,forbidden) && bound==-2 && forbidden[0]==1, + "invalid binary box changed outputs"); + require(!Cert::prepare({1},{1},{1},{std::numeric_limits::infinity()},empty), + "nonfinite multiplier accepted"); + require(empty.lower_bound({0},{1},bound) && bound==-2,"failed prepare replaced certificate"); + V huge(32,std::numeric_limits::max()); + require(!Cert::prepare(huge,huge,{1},std::vector(32,1e12),empty), + "128-bit overflow accepted"); + require(!Cert::prepare({1,2},{1},{1},{1},empty),"bad dense dimensions accepted"); + + std::mt19937 random(987651); + for (unsigned id=0;id<1000;++id) { + const Instance p=random_instance(random,id); + std::vector duals(p.model.b.size()); + const double candidates[]={-2,-0.5,0,0.1,0.5,0.99999999,1,1.5,3}; + for (double& d:duals) d=candidates[random()%9]; + Cert::Certificate prepared; + require(Cert::prepare(p.model.a,p.model.b,p.model.c,duals,prepared),"tiny prepare failed"); + I old_bound,new_bound; + require(Cert::lower_bound(p.model.a,p.model.b,p.model.c,p.lower,p.upper,duals,old_bound) && + prepared.lower_bound(p.lower,p.upper,new_bound) && old_bound==new_bound, + "prepared bound differs from legacy certificate"); + for (I cutoff:{I(-7),I(0),I(9)}) { + require(prepared.filter(p.lower,p.upper,cutoff,new_bound,forbidden),"tiny filter failed"); + require(new_bound==old_bound,"filter changed ordinary certificate"); + for (unsigned mask:oracle(p,cutoff)) { + require(new_bound<=cost(p,mask),"bound exceeds a feasible assignment cost"); + for (std::size_t j=0;j>j)&1U))),"conditional bound removed feasible bit"); + } + // Compare every conditional result to the independently retained old + // checker evaluated with an explicit fixed box, including negative costs. + for (std::size_t j=0;jcutoff),"conditional algebra mismatch"); + } + } + } + } +} + +class Problem : public Gecode::Space { +public: + Gecode::IntVarArray x; + Gecode::IntVar z; + Problem(const Instance& p,I cutoff,const std::shared_ptr& backend, + const LP::Options* options) + :x(*this,static_cast(p.model.c.size()),0,1), + z(*this,endpoint(p.model,false),endpoint(p.model,true)) { + for(int j=0;j(p.lower[j]),static_cast(p.upper[j])); + if(options) LP::binary_linear_minimize(*this,x,z,backend,*options); + else LP::post_native(*this,x,z,p.model); + Gecode::rel(*this,z,Gecode::IRT_LQ,static_cast(cutoff)); + Gecode::branch(*this,x,Gecode::INT_VAR_NONE(),Gecode::INT_VAL_MIN()); + } + Problem(Problem& other):Gecode::Space(other) {x.update(*this,other.x);z.update(*this,other.z);} + Gecode::Space* copy() override {return new Problem(*this);} + void constrain(const Gecode::Space& best) override { + Gecode::rel(*this,z,Gecode::IRT_LE,static_cast(best).z.val()); + } + unsigned mask() const { + unsigned result=0; + for(int j=0;j(p.model); + LP::Options bounds,fixing;fixing.reduced_cost_fixing=true; + const I cutoff=negative ? -3:2; + { + Problem baseline(p,cutoff,backend,&bounds); + require(baseline.status()!=Gecode::SS_FAILED && !baseline.x[3].assigned(), + "structured fixing already performed by bound-only baseline"); + Problem improved(p,cutoff,backend,&fixing); + require(improved.status()!=Gecode::SS_FAILED && improved.x[3].assigned() && + improved.x[3].val()==(negative ? 1:0),"conditional bound missed structured fixing"); + } + require(backend->statistics().variable_fixings>0,"fixing statistics not updated"); + require(backend.use_count()==1,"actor leaked backend after fixing"); + } + { + const Instance p=triangle(); + auto backend=std::make_shared(p.model); + LP::Options options;options.frequency=LP::Frequency::Root;options.reduced_cost_fixing=true; + Problem root(p,5,backend,&options); + require(root.status()!=Gecode::SS_FAILED && !root.x[3].assigned(),"uncut root changed"); + const auto calls=backend->statistics().lp_calls; + std::unique_ptr left(static_cast(root.clone())); + std::unique_ptr right(static_cast(root.clone())); + Gecode::rel(*left,left->z,Gecode::IRT_LQ,2); + require(left->status()!=Gecode::SS_FAILED && left->x[3].val()==0, + "objective-only event did not reuse root certificate"); + Gecode::rel(*right,right->x[3],Gecode::IRT_EQ,1); + Gecode::rel(*right,right->z,Gecode::IRT_LQ,4); + require(right->status()!=Gecode::SS_FAILED && right->z.min()==4, + "sibling root certificate used another box's scalar bound"); + require(backend->statistics().lp_calls==calls,"root-only fixing performed another LP solve"); + } + { + // Keep a nonzero redundant row so this fixture exercises LP scheduling. + // A zero matrix correctly uses the checked box bound without an LP call. + Instance p{{V(6,1),{0},V(6,0)},V(6,0),V(6,1)}; + auto backend=std::make_shared(p.model); + LP::Options options;options.reduced_cost_fixing=true;options.assignment_interval=4; + Problem root(p,0,backend,&options); + require(root.status()!=Gecode::SS_FAILED,"throttling root failed"); + const auto initial=backend->statistics().lp_calls; + require(initial==1,"root LP missing"); + for(int j=0;j<4;++j) { + Gecode::rel(root,root.x[j],Gecode::IRT_EQ,0); + require(root.status()!=Gecode::SS_FAILED,"throttled descendant failed"); + require(backend->statistics().lp_calls==initial+(j==3 ? 1:0),"assignment interval not respected"); + } + bool rejected=false;options.assignment_interval=0; + try {Problem bad(p,0,backend,&options);} catch(const std::invalid_argument&) {rejected=true;} + require(rejected,"zero assignment interval accepted"); + } +} + +static void check_search() { + std::vector cases={triangle(),triangle(true),{{{}, {}, {}},{},{}}}; + std::mt19937 random(928371); + while(cases.size()<100) cases.push_back(random_instance(random,cases.size())); + const std::vector policies={ + {LP::Frequency::EveryNode,false,1}, {LP::Frequency::EveryNode,true,1}, + {LP::Frequency::EveryNode,true,4}, {LP::Frequency::Root,true,1}, + {LP::Frequency::EveryNode,false,4}}; + unsigned runs=0; + for(const Instance& p:cases) { + auto backend=std::make_shared(p.model); + const int lo=endpoint(p.model,false),hi=endpoint(p.model,true); + for(I cutoff:{I(hi),I(lo+(hi-lo)/2),I(lo)}) { + const auto expected=oracle(p,cutoff); + I optimum=std::numeric_limits::max(); + for(unsigned mask:expected) optimum=std::min(optimum,cost(p,mask)); + for(unsigned distance:{1U,8U,32U}) + for(std::size_t mode=0;mode<=policies.size();++mode) { + const LP::Options* policy=mode ? &policies[mode-1]:nullptr; + Gecode::Search::Options options;options.threads=1;options.c_d=distance;options.a_d=16; + { + auto root=std::make_unique(p,cutoff,backend,policy); + Gecode::DFS engine(root.get(),options);root.reset(); + std::set actual; + while(Problem* raw=engine.next()) { + std::unique_ptr solution(raw); + const auto mask=solution->mask(); + require(expected.count(mask) && solution->z.val()==cost(p,mask),"DFS solution invalid"); + require(actual.insert(mask).second,"duplicate DFS solution"); + } + require(!engine.stopped() && actual==expected,"conditional propagation changed full solution set"); + } + { + auto root=std::make_unique(p,cutoff,backend,policy); + Gecode::BAB engine(root.get(),options);root.reset(); + I best=std::numeric_limits::max(); + while(Problem* raw=engine.next()) { + std::unique_ptr solution(raw); + const auto mask=solution->mask(); + require(expected.count(mask) && solution->z.val()==cost(p,mask) && cost(p,mask) +#include +#include +#include +#include +#include "lp_sensitivity_oracle.hpp" +#include "lp_sensitivity_fixture.hpp" + +namespace O=Gecode::Optimize; +constexpr double inf=std::numeric_limits::infinity(); +static void near(double a,double b){assert(std::isfinite(a)&&std::abs(a-b)<1e-7*std::max(1.0,std::abs(b)));} +static void endpoint(const O::LpRangeEnd& a,double b){ + if(std::isinf(b)){assert(!a.value);assert(a.kind==(b>0?O::LpRangeEndKind::PositiveInfinity:O::LpRangeEndKind::NegativeInfinity));} + else {assert(a.kind==O::LpRangeEndKind::Finite&&a.value);near(*a.value,b);} +} +static void bounds(const O::LpSensitivityEntry* e,double lo,double hi,double slope){ + assert(e);if(e->group.state!=O::LpSensitivityState::Available)std::cerr<<"range unavailable: "<group.message<<'\n'; + assert(e->group.state==O::LpSensitivityState::Available&&e->interval&&e->interval->checks.accepted); + endpoint(e->interval->lower,lo);endpoint(e->interval->upper,hi);assert(e->interval->objective_slope);near(*e->interval->objective_slope,slope); +} +static void complete(const O::LpSensitivityResult& r){ + if(r.completion!=O::LpSensitivityCompletion::Complete)std::cerr<<"sensitivity failure: "<checks().kkt.accepted); +} +static O::LpObservedResult selected(O::Model& m,std::vector columns, + std::vector rows){ + O::LpBasisData data;data.source=m.snapshot();for(auto s:columns)data.columns.push_back(s);for(auto s:rows)data.rows.push_back(s); + O::LpBasisSolveOptions options;options.basis=O::make_lp_basis(data); + auto out=O::solve_lp_with_basis(m,options); + assert(out.submission.state==O::LpBasisSubmissionState::Accepted); + assert(out.observed.result.termination==O::Termination::Optimal);return out.observed; +} +template static void invalid(F f){bool threw=false;try{f();}catch(const O::ModelError&){threw=true;}assert(threw);} +static void historical_panel(){ + O::LpSensitivityResult history;O::LpObservedResult original;O::Variable x,y,deleted;O::Constraint row,removed,unrequested; + { + O::Model model;deleted=model.add_continuous();model.remove(deleted); + x=model.add_continuous();y=model.add_continuous();removed=model.add_row({{x,1}},0,9);model.remove(removed); + row=model.add_row({{x,1},{y,1}},3,3);unrequested=model.add_row({{x,1}},-inf,4); + model.minimize({{x,2},{y,1}},7);O::SolveSession session;original=session.solve_lp_observed(model); + O::LpSensitivityOptions options;options.parameters={O::LpObjectiveParameter{x},O::LpEqualityRhsParameter{row}}; + history=O::analyze_lp_sensitivity(original,options);complete(history); + assert(!history.sensitivity->objective(y)&&!history.sensitivity->equality_rhs(unrequested)); + invalid([&]{history.sensitivity->objective(deleted);});invalid([&]{history.sensitivity->equality_rhs(removed);}); + invalid([&]{history.sensitivity->objective(O::Variable{model.id()+1,x.id});}); + invalid([&]{history.sensitivity->equality_rhs(O::Constraint{model.id()+1,row.id});}); + for(auto request:{O::LpSensitivityParameter{O::LpObjectiveParameter{deleted}},O::LpSensitivityParameter{O::LpEqualityRhsParameter{removed}}}){ + options.parameters={request};auto rejected=O::analyze_lp_sensitivity(original,options); + assert(rejected.reason==O::LpSensitivityReason::InvalidSource&&!rejected.work.factor_setup_attempted); + } + model.set_objective_coefficient(x,-3);auto changed=session.solve_lp_observed(model);assert(changed.result.termination==O::Termination::Optimal); + session.reset();assert(history.sensitivity->revision()!=model.revision()); + } + bounds(history.sensitivity->objective(x),1,inf,0);bounds(history.sensitivity->equality_rhs(row),0,inf,1); + assert(history.sensitivity->original().observations==original.observations); + assert(history.sensitivity->active_columns()==std::vector({false,true,true})); + assert(history.sensitivity->active_rows()==std::vector({false,true,true})); + for(const auto& entity:history.sensitivity->factor_order())std::visit([&](const auto& e){assert(e.model_id==history.model_id&&e.id!=0);},entity); + original={};assert(history.sensitivity->original().result.objective==10); +} +static void scaled_panel(){ + using B=O::LpBasisStatus; + for(double scale:{1e-6,1e6})for(bool maximize:{false,true}){ + O::Model model;auto x=model.add_continuous(-2,5),y=model.add_continuous(0,4); + auto row=model.add_row({{x,2*scale},{y,-4*scale}},-12*scale,-12*scale); + model.set_objective({{x,maximize?-2.0:2.0},{y,maximize?-3.0:3.0}},maximize?O::ObjectiveSense::Maximize:O::ObjectiveSense::Minimize,17); + auto source=selected(model,{B::Lower,B::Basic},{B::Lower});O::LpSensitivityOptions o; + o.parameters={O::LpObjectiveParameter{x},O::LpObjectiveParameter{y},O::LpEqualityRhsParameter{row}}; + auto result=O::analyze_lp_sensitivity(source,o);complete(result); + bounds(result.sensitivity->objective(x),maximize?-inf:-1.5,maximize?1.5:inf,-2); + bounds(result.sensitivity->objective(y),maximize?-inf:-4,maximize?4:inf,2); + bounds(result.sensitivity->equality_rhs(row),-20*scale,-4*scale,(maximize?3.0:-3.0)/(4*scale)); + } +} +static void cancellation_panel(){ + // Exact original witness: w=1, x=0, y=1-10^14; dual=(1,100), + // reduced cost of w = 10^16 - 1 - 10^16 = -1. Computing the + // dot product first rounds away the 1 and changes x's lower endpoint. + O::Model model;auto x=model.add_continuous(-inf,inf),y=model.add_continuous(-inf,inf),w=model.add_continuous(0,1); + model.add_row({{x,1},{w,1}},1,1);model.add_row({{y,1},{w,1e14}},1,1); + model.minimize({{x,1},{y,100},{w,1e16}});auto source=model.snapshot(); + O::LpObservedResult original;original.result.model_id=source.model_id;original.result.revision=source.revision; + original.result.termination=O::Termination::Optimal;original.result.guarantee=O::Guarantee::Numerical; + original.result.values={0,1-1e14,1};original.result.active_variables={true,true,true};original.result.solution_validated=true; + original.result.objective=original.result.best_bound=100;original.result.absolute_gap=original.result.relative_gap=0; + O::LpObservationOptions options;auto data=O::Detail::LpObservationAccess::create(source,options);original.observations=data; + O::Detail::LpBackendObservations raw;raw.attempted=raw.timely=raw.complete=true; + raw.info_valid=raw.value_valid=raw.primal_feasible=raw.dual_valid=raw.dual_feasible=raw.basis_valid=raw.info_basis_valid=true; + raw.model_id=source.model_id;raw.revision=source.revision;raw.column_slots={0,1,2};raw.row_slots={0,1}; + raw.column_duals={0,0,-1};raw.row_duals={1,100}; + raw.column_basis={O::LpBasisStatus::Basic,O::LpBasisStatus::Basic,O::LpBasisStatus::Upper}; + raw.row_basis={O::LpBasisStatus::Lower,O::LpBasisStatus::Lower};O::SolveBudget budget(options.solve); + O::Detail::LpObservationAccess::finish(*data,original.result,raw,budget);assert(data->checks().accepted); + O::LpSensitivityOptions requested;requested.parameters={O::LpObjectiveParameter{x}}; + auto result=O::analyze_lp_sensitivity(original,requested);complete(result);bounds(result.sensitivity->objective(x),0,inf,0); + assert(result.sensitivity->checks().kkt.normalized_gap==0); +} +static void partial_panel(){ + // The coefficient-y endpoint 100-2e-14 cannot be represented closely enough + // to retain its original-unit limiting equality; the w endpoint is exact. + O::Model model;auto y=model.add_continuous(-inf,inf),w=model.add_continuous(0,1); + model.add_row({{y,1},{w,1e14}},1,1);model.minimize({{y,100},{w,1e16-2}}); + auto original=O::solve_lp_observed(model);assert(original.result.termination==O::Termination::Optimal&&original.observations->checks().accepted); + assert(original.result.objective==98);O::LpSensitivityOptions options; + options.parameters={O::LpObjectiveParameter{y},O::LpObjectiveParameter{w}}; + auto result=O::analyze_lp_sensitivity(original,options);assert(result.completion==O::LpSensitivityCompletion::Partial&&result.sensitivity); + auto rejected=result.sensitivity->objective(y);assert(rejected->group.state==O::LpSensitivityState::Rejected&&rejected->group.reason==O::LpSensitivityReason::FailedIntervalChecks&&!rejected->interval); + bounds(result.sensitivity->objective(w),-inf,1e16,1); +} +static void oracle_panel(){ + using namespace SensitivityOracle;using B=O::LpBasisStatus; + for(bool maximize:{false,true})for(bool at_lower:{false,true})for(int a:{-3,2})for(int b:{-4,2}){ + BoxLine q;q.a=Q(a);q.b=Q(b);q.lx=Q(-2);q.ux=Q(5);q.ly=Q(0);q.uy=Q(4);q.cy=Q(3); + q.reference={at_lower?q.lx:q.ux,Q(2)};q.rhs=q.a*q.reference[0]+q.b*q.reference[1];q.maximize=maximize; + q.cx=q.a*q.cy/q.b+Q((maximize?-1:1)*(at_lower?1:-1)); + O::Model m;auto x=m.add_continuous(q.lx.value(),q.ux.value()),y=m.add_continuous(q.ly.value(),q.uy.value()); + auto row=m.add_row({{x,q.a.value()},{y,q.b.value()}},q.rhs.value(),q.rhs.value()); + m.set_objective({{x,q.cx.value()},{y,q.cy.value()}},maximize?O::ObjectiveSense::Maximize:O::ObjectiveSense::Minimize,1e16); + auto original=selected(m,{at_lower?B::Lower:B::Upper,B::Basic},{B::Lower}); + O::LpSensitivityOptions options;options.parameters={O::LpObjectiveParameter{x},O::LpObjectiveParameter{y},O::LpEqualityRhsParameter{row}}; + auto result=O::analyze_lp_sensitivity(original,options);complete(result); + auto expected=[&](Range r,const O::LpSensitivityEntry* e,double slope){bounds(e,r.lower?r.lower->value():-inf,r.upper?r.upper->value():inf,slope);}; + expected(q.objective(0),result.sensitivity->objective(x),q.reference[0].value()); + expected(q.objective(1),result.sensitivity->objective(y),q.reference[1].value()); + expected(q.equality_with_x_fixed(),result.sensitivity->equality_rhs(row),(q.cy/q.b).value()); + for(std::size_t j=0;j<2;++j){const auto range=q.objective(j);const auto anchor=j?q.cy:q.cx; + assert(q.optimal_at(anchor,j)); + if(range.lower){assert(q.optimal_at(*range.lower,j));assert(q.optimal_at((*range.lower+anchor)/Q(2),j));assert(!q.optimal_at(*range.lower-Q(1,4),j));} + if(range.upper){assert(q.optimal_at(*range.upper,j));assert(q.optimal_at((*range.upper+anchor)/Q(2),j));assert(!q.optimal_at(*range.upper+Q(1,4),j));} + if(!range.lower)assert(q.optimal_at(anchor-Q(100),j)); + if(!range.upper)assert(q.optimal_at(anchor+Q(100),j)); + } + } + // Free basic column, fixed nonbasic column, unreferenced zero-cost variable. + O::Model free;auto x=free.add_continuous(-inf,inf),fixed=free.add_continuous(2,2),zero=free.add_continuous(-5,5); + auto row=free.add_row({{x,1},{fixed,1}},3,3);free.minimize({{x,1}},9); + auto original=selected(free,{B::Basic,B::Lower,B::Lower},{B::Lower}); + O::LpSensitivityOptions options;options.parameters={O::LpObjectiveParameter{x},O::LpObjectiveParameter{fixed},O::LpObjectiveParameter{zero},O::LpEqualityRhsParameter{row}}; + auto r=O::analyze_lp_sensitivity(original,options);complete(r); + bounds(r.sensitivity->objective(x),-inf,inf,1);bounds(r.sensitivity->objective(fixed),-inf,inf,2); + bounds(r.sensitivity->objective(zero),0,inf,-5);bounds(r.sensitivity->equality_rhs(row),-inf,inf,1); + // A redundant equality has a basic fixed logical; its RHS range is singleton. + O::Model redundant;auto v=redundant.add_continuous(0,10);auto a=redundant.add_row({{v,1}},2,2);auto b=redundant.add_row({{v,2}},4,4);redundant.minimize({{v,1}}); + original=selected(redundant,{B::Basic},{B::Lower,B::Basic});options.parameters={O::LpEqualityRhsParameter{a},O::LpEqualityRhsParameter{b},O::LpObjectiveParameter{v}}; + r=O::analyze_lp_sensitivity(original,options);complete(r);bounds(r.sensitivity->equality_rhs(a),2,2,1);bounds(r.sensitivity->equality_rhs(b),4,4,0);bounds(r.sensitivity->objective(v),-inf,inf,2); + // A ranged source row limits equality changes; varying its side is unsupported. + O::Model ranged;v=ranged.add_continuous(-inf,inf);a=ranged.add_row({{v,1}},2,2);b=ranged.add_row({{v,1}},1,3);ranged.minimize({{v,1}}); + original=selected(ranged,{B::Basic},{B::Lower,B::Basic});options.parameters={O::LpEqualityRhsParameter{a}}; + r=O::analyze_lp_sensitivity(original,options);complete(r);bounds(r.sensitivity->equality_rhs(a),1,3,1); + options.parameters={O::LpEqualityRhsParameter{b}};r=O::analyze_lp_sensitivity(original,options);assert(r.reason==O::LpSensitivityReason::Unsupported&&!r.work.factor_setup_attempted); + // A free nonbasic column requires both reduced-cost signs: coefficient zero. + O::Model free_zero;v=free_zero.add_continuous();auto z=free_zero.add_continuous(-inf,inf); + a=free_zero.add_row({{v,1}},2,2);free_zero.minimize({{v,1}}); + original=selected(free_zero,{B::Basic,B::Zero},{B::Lower});options.parameters={O::LpObjectiveParameter{z},O::LpEqualityRhsParameter{a}}; + r=O::analyze_lp_sensitivity(original,options);complete(r);bounds(r.sensitivity->objective(z),0,0,0);bounds(r.sensitivity->equality_rhs(a),0,inf,1); + // The same degenerate optimum has different ranges for different bases. + O::Model degenerate;auto dx=degenerate.add_continuous(),dy=degenerate.add_continuous(); + degenerate.add_row({{dx,1},{dy,1}},0,0);degenerate.minimize({}); + options.parameters={O::LpObjectiveParameter{dx}}; + auto x_basic=O::analyze_lp_sensitivity(selected(degenerate,{B::Basic,B::Lower},{B::Lower}),options); + auto y_basic=O::analyze_lp_sensitivity(selected(degenerate,{B::Lower,B::Basic},{B::Lower}),options); + complete(x_basic);complete(y_basic);bounds(x_basic.sensitivity->objective(dx),-inf,0,0);bounds(y_basic.sensitivity->objective(dx),0,inf,0); +} +int main(){ + O::Model m;auto x=m.add_continuous(),y=m.add_continuous();auto row=m.add_row({{x,1},{y,1}},3,3);m.minimize({{x,2},{y,1}},7); + O::LpSensitivityOptions options;options.parameters={O::LpObjectiveParameter{x},O::LpObjectiveParameter{y},O::LpEqualityRhsParameter{row}}; + auto original=O::solve_lp_observed(m); + if(!O::capabilities().available){ + auto missing=O::analyze_lp_sensitivity(original,options);assert(missing.completion==O::LpSensitivityCompletion::Rejected); + auto explicit_source=SensitivityFixture::two_column_observed(m); + auto unavailable=O::analyze_lp_sensitivity(explicit_source,options); + assert(unavailable.completion==O::LpSensitivityCompletion::Rejected&&unavailable.reason==O::LpSensitivityReason::Unsupported); + assert(unavailable.sensitivity&&unavailable.work.factor_setup_attempted&&unavailable.work.basis_solves==0); + for(const auto& e:unavailable.sensitivity->entries())assert(e.group.state!=O::LpSensitivityState::Available&&!e.interval); + std::cout<<"sensitivity missing-observation and missing-backend admission passed\n";return 0; + } + auto r=O::analyze_lp_sensitivity(original,options);complete(r); + bounds(r.sensitivity->objective(x),1,inf,0);bounds(r.sensitivity->objective(y),-inf,2,3);bounds(r.sensitivity->equality_rhs(row),0,inf,1); + assert(r.work.factor_setup_attempted&&r.work.basis_solves==4); + m.maximize({{x,-2},{y,-1}},11);auto maximal=O::solve_lp_observed(m);auto max=O::analyze_lp_sensitivity(maximal,options);complete(max); + bounds(max.sensitivity->objective(x),-inf,-1,0);bounds(max.sensitivity->objective(y),-2,inf,3);bounds(max.sensitivity->equality_rhs(row),0,inf,-1); + // Historical source/result is copied unchanged even after edits. + assert(r.sensitivity->original().result.objective==original.result.objective); + assert(r.sensitivity->original().observations==original.observations); + assert(r.sensitivity->revision()!=m.revision()); + auto again=O::analyze_lp_sensitivity(original,options);complete(again); + oracle_panel(); + historical_panel();scaled_panel();cancellation_panel();partial_panel(); + for(int fault=0;fault<8;++fault){auto bad=original; + if(fault==0)bad.result.termination=static_cast(99); + if(fault==1)bad.result.revision++; + if(fault==2)bad.result.active_variables[0]=false; + if(fault==3)bad.result.values[1]=4; + if(fault==4)bad.result.objective=inf; + if(fault==5)bad.result.best_bound=-10; + if(fault==6)bad.result.solution_validated=false; + if(fault==7)bad.result.absolute_gap=-1; + auto rejected=O::analyze_lp_sensitivity(bad,options);assert(rejected.completion==O::LpSensitivityCompletion::Rejected&&!rejected.work.factor_setup_attempted);} + auto limited=options;limited.time_limit_seconds=0; + assert(O::analyze_lp_sensitivity(original,limited).stop_reason==O::Termination::TimeLimit); + limited=options;limited.limits.max_basis_solves=2;auto stopped=O::analyze_lp_sensitivity(original,limited); + assert(stopped.completion==O::LpSensitivityCompletion::Interrupted&&stopped.sensitivity); + for(const auto& e:stopped.sensitivity->entries())assert(e.group.state!=O::LpSensitivityState::Available&&!e.interval); + limited=options;limited.backend=O::Backend::Native;assert(O::analyze_lp_sensitivity(original,limited).reason==O::LpSensitivityReason::Unsupported); + limited=options;limited.parameters.push_back(O::LpObjectiveParameter{x});assert(O::analyze_lp_sensitivity(original,limited).reason==O::LpSensitivityReason::InvalidSource); + std::cout<<"LP sensitivity analytic min/max, provenance and admission checks passed\n"; +} diff --git a/test/optimize/lp_sensitivity_binding_cleanup.cpp b/test/optimize/lp_sensitivity_binding_cleanup.cpp new file mode 100644 index 0000000000..3514b8acc5 --- /dev/null +++ b/test/optimize/lp_sensitivity_binding_cleanup.cpp @@ -0,0 +1,55 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +static gecode_opt_handle cancellation=0; +extern "C" void gecode_opt_test_sensitivity_binding_checkpoint(void){ + if(cancellation)assert(gecode_opt_v1_cancellation_cancel(cancellation)==GECODE_OPT_OK); +} +static void ok(int32_t c){if(c)std::cerr<::infinity();gecode_opt_handle m=0,source=0,h=0,copied=0,basis=0; + gecode_opt_id x{},y{},unrequested{},row{};ok(gecode_opt_v1_model_create(&m)); + ok(gecode_opt_v1_model_add_variable(m,GECODE_OPT_CONTINUOUS,0,inf,"x",&x)); + ok(gecode_opt_v1_model_add_variable(m,GECODE_OPT_CONTINUOUS,0,inf,"y",&y)); + ok(gecode_opt_v1_model_add_variable(m,GECODE_OPT_CONTINUOUS,0,1,"unrequested",&unrequested)); + gecode_opt_term terms[2]={{x,1},{y,1}};ok(gecode_opt_v1_model_add_row(m,terms,2,3,3,"balance",&row));terms[0].coefficient=2; + ok(gecode_opt_v1_model_set_objective(m,terms,2,GECODE_OPT_MINIMIZE,7));ok(gecode_opt_v1_solve_lp_observed(m,nullptr,&source)); + gecode_opt_sensitivity_request_v1 requests[2]={{sizeof(requests[0]),0,GECODE_OPT_SENSITIVITY_OBJECTIVE,0,x}, + {sizeof(requests[0]),0,GECODE_OPT_SENSITIVITY_EQUALITY_RHS,0,row}}; + gecode_opt_sensitivity_options_v1 options{};ok(gecode_opt_v1_sensitivity_options_default(&options,sizeof(options)));options.requests=requests;options.request_count=2; + ok(gecode_opt_v1_analyze_lp_sensitivity(source,&options,&h));gecode_opt_sensitivity_info_v1 info{};ok(gecode_opt_v1_sensitivity_info(h,&info,sizeof(info))); + assert(info.completion==GECODE_OPT_SENSITIVITY_COMPLETE);ok(gecode_opt_v1_sensitivity_destroy(h)); + ok(gecode_opt_v1_cancellation_create(&cancellation));options.cancellation=cancellation; + ok(gecode_opt_v1_analyze_lp_sensitivity(source,&options,&h));ok(gecode_opt_v1_sensitivity_info(h,&info,sizeof(info))); + assert(info.completion==GECODE_OPT_SENSITIVITY_INTERRUPTED&&info.reason==GECODE_OPT_SENSITIVITY_REASON_STOPPED); + assert(info.has_sensitivity&&info.has_basis&&info.has_stop_reason&&info.stop_reason==GECODE_OPT_CANCELLED); + gecode_opt_sensitivity_entry_v1 entries[2]{},entry{};uint64_t needed; + ok(gecode_opt_v1_sensitivity_entries(h,entries,sizeof(entries[0]),2,&needed));assert(needed==2); + for(const auto& e:entries){assert(e.requested&&!e.has_interval&&e.group.state==GECODE_OPT_SENSITIVITY_UNAVAILABLE&&e.group.reason==GECODE_OPT_SENSITIVITY_REASON_STOPPED); + assert(!e.objective_slope.present&&!e.checks.accepted&&!e.lower.value.present&&!e.has_lower_limiter);} + ok(gecode_opt_v1_sensitivity_entry(h,0,&entry,sizeof(entry)));assert(!entry.has_interval); + ok(gecode_opt_v1_sensitivity_objective(h,x,&entry,sizeof(entry)));assert(!entry.has_interval); + ok(gecode_opt_v1_sensitivity_equality_rhs(h,row,&entry,sizeof(entry)));assert(!entry.has_interval); + ok(gecode_opt_v1_sensitivity_objective(h,unrequested,&entry,sizeof(entry)));assert(!entry.requested&&entry.group.state==GECODE_OPT_SENSITIVITY_NOT_REQUESTED); + char message[160];for(int field:{GECODE_OPT_SENSITIVITY_ENTRY_MESSAGE,GECODE_OPT_SENSITIVITY_INTERVAL_MESSAGE}){ + ok(gecode_opt_v1_sensitivity_text(h,field,0,message,sizeof(message),&needed));assert(std::strstr(message,"stopped"));} + gecode_opt_sensitivity_reference_checks_v1 checks{};ok(gecode_opt_v1_sensitivity_reference_checks(h,&checks,sizeof(checks))); + assert(checks.primal.valid&&checks.kkt.accepted&&checks.basis_point_matches); // retained diagnostics, not intervals + ok(gecode_opt_v1_sensitivity_copy_source_observed(h,&copied));ok(gecode_opt_v1_sensitivity_copy_basis(h,&basis)); + // Invalid options retain their original admission reason even at this hook. + options.time_limit_seconds=-1;gecode_opt_handle invalid=0;ok(gecode_opt_v1_analyze_lp_sensitivity(source,&options,&invalid)); + ok(gecode_opt_v1_sensitivity_info(invalid,&info,sizeof(info)));assert(info.reason==GECODE_OPT_SENSITIVITY_REASON_INVALID_SOURCE&&!info.has_stop_reason); + ok(gecode_opt_v1_sensitivity_destroy(invalid));ok(gecode_opt_v1_sensitivity_destroy(h));ok(gecode_opt_v1_lp_observed_result_destroy(source)); + ok(gecode_opt_v1_model_destroy(m));ok(gecode_opt_v1_cancellation_destroy(cancellation));cancellation=0; + gecode_opt_lp_info_v1 original{};ok(gecode_opt_v1_lp_observed_result_info(copied,&original,sizeof(original)));assert(original.result.termination==GECODE_OPT_OPTIMAL); + int32_t status;ok(gecode_opt_v1_basis_column(basis,y,&status));assert(status==GECODE_OPT_LP_BASIS_BASIC); + ok(gecode_opt_v1_lp_observed_result_destroy(copied));ok(gecode_opt_v1_basis_destroy(basis)); + std::cout<<"Sensitivity binding cleanup revokes every range view and retains owning source/basis diagnostics\n"; +} diff --git a/test/optimize/lp_sensitivity_c.c b/test/optimize/lp_sensitivity_c.c new file mode 100644 index 0000000000..5ffeabf973 --- /dev/null +++ b/test/optimize/lp_sensitivity_c.c @@ -0,0 +1,121 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +#include +#define OK(call) do {int32_t c_=(call);if(c_){fprintf(stderr,"%s: %s\n",#call,gecode_opt_v1_last_error());assert(!c_);}}while(0) +static int32_t available; +static void near(double a,double b){assert(isfinite(a)&&fabs(a-b)<1e-7);} +static gecode_opt_sensitivity_request_v1 request(int32_t kind,gecode_opt_id id){ + gecode_opt_sensitivity_request_v1 r;memset(&r,0,sizeof(r));r.struct_size=sizeof(r);r.kind=kind;r.entity=id;return r; +} +static gecode_opt_handle analyze(gecode_opt_handle source,const gecode_opt_sensitivity_options_v1* o){ + gecode_opt_handle h=0;OK(gecode_opt_v1_analyze_lp_sensitivity(source,o,&h));assert(h);return h; +} +static void model(gecode_opt_handle* m,gecode_opt_handle* observed,gecode_opt_id ids[6]){ + gecode_opt_term t[2];OK(gecode_opt_v1_model_create(m)); + OK(gecode_opt_v1_model_add_variable(*m,GECODE_OPT_CONTINUOUS,0,1,"dead",&ids[0]));OK(gecode_opt_v1_model_remove_variable(*m,ids[0])); + OK(gecode_opt_v1_model_add_variable(*m,GECODE_OPT_CONTINUOUS,0,INFINITY,"x",&ids[1])); + OK(gecode_opt_v1_model_add_variable(*m,GECODE_OPT_CONTINUOUS,0,INFINITY,"y",&ids[2])); + OK(gecode_opt_v1_model_add_variable(*m,GECODE_OPT_CONTINUOUS,0,1,"unrequested",&ids[3])); + OK(gecode_opt_v1_model_add_row(*m,NULL,0,-INFINITY,INFINITY,"gone",&ids[4]));OK(gecode_opt_v1_model_remove_row(*m,ids[4])); + t[0].variable=ids[1];t[0].coefficient=1;t[1].variable=ids[2];t[1].coefficient=1; + OK(gecode_opt_v1_model_add_row(*m,t,2,3,3,"balance",&ids[5]));t[0].coefficient=2; + OK(gecode_opt_v1_model_set_objective(*m,t,2,GECODE_OPT_MINIMIZE,7));OK(gecode_opt_v1_solve_lp_observed(*m,NULL,observed)); +} +static void analytic_history(void){ + gecode_opt_handle m=0,observed=0,h=0,copy=0,basis=0,ordinary=0;gecode_opt_id ids[6],bad,order[1]; + gecode_opt_sensitivity_options_v1 o;gecode_opt_sensitivity_request_v1 requests[3]; + gecode_opt_sensitivity_info_v1 info;gecode_opt_sensitivity_work_v1 work,before; + gecode_opt_sensitivity_entry_v1 entries[3],single;gecode_opt_sensitivity_reference_checks_v1 checks; + gecode_opt_lp_info_v1 original;gecode_opt_result_info_v1 result;uint64_t n,owner,revision;uint8_t mask[4];int32_t status; + model(&m,&observed,ids);OK(gecode_opt_v1_model_identity(m,&owner,&revision)); + requests[0]=request(0,ids[1]);requests[1]=request(0,ids[2]);requests[2]=request(1,ids[5]); + OK(gecode_opt_v1_sensitivity_options_default(&o,sizeof(o)));o.requests=requests;o.request_count=3; + assert(!o.reserved&&!o.reserved_flags&&!o.checks.reserved&&!o.limits.reserved);h=analyze(observed,&o); + memset(&info,0xa5,sizeof(info));OK(gecode_opt_v1_sensitivity_info(h,&info,sizeof(info))); + assert(info.model_id==owner&&info.revision==revision&&info.guarantee==GECODE_OPT_NUMERICAL&&!info.reserved&&!info.reserved_flags); + OK(gecode_opt_v1_sensitivity_work(h,&before,sizeof(before)));assert(before.preparation_visits==3); + assert(gecode_opt_v1_result_info(h,&result,sizeof(result))==GECODE_OPT_INVALID_HANDLE); + OK(gecode_opt_v1_sensitivity_copy_source_observed(h,©));OK(gecode_opt_v1_lp_observed_result_info(copy,&original,sizeof(original))); + assert(original.result.model_id==owner&&original.result.revision==revision); + if(available){ + assert(info.completion==GECODE_OPT_SENSITIVITY_COMPLETE&&info.has_sensitivity&&info.has_basis&&!info.has_stop_reason); + assert(info.entry_count==3&&info.factor_order_count==1&&info.column_slots==4&&info.row_slots==2); + assert(before.factor_setup_attempted&&before.basis_solves>0&&before.coordinator_visits>0); + OK(gecode_opt_v1_sensitivity_entries(h,NULL,sizeof(entries[0]),0,&n));assert(n==3); + memset(entries,0xa5,sizeof(entries));assert(gecode_opt_v1_sensitivity_entries(h,entries,sizeof(entries[0]),2,&n)==GECODE_OPT_BUFFER_TOO_SMALL); + assert(entries[0].struct_size==UINT64_C(0xa5a5a5a5a5a5a5a5)); + OK(gecode_opt_v1_sensitivity_entries(h,entries,sizeof(entries[0]),3,&n)); + assert(n==3&&entries[0].has_interval&&entries[0].requested&&entries[0].index==0&&entries[2].index==2); + assert(entries[0].group.state==GECODE_OPT_SENSITIVITY_AVAILABLE&&entries[0].checks.accepted); + assert(!entries[0].reserved&&!entries[0].lower.reserved&&!entries[0].checks.reserved_flags); + near(entries[0].anchor,2);near(entries[0].lower.value.value,1);assert(entries[0].lower.value.present); + assert(entries[0].upper.kind==GECODE_OPT_RANGE_POSITIVE_INFINITY&&!entries[0].upper.value.present&&entries[0].upper.value.value==0); + assert(entries[1].lower.kind==GECODE_OPT_RANGE_NEGATIVE_INFINITY&&!entries[1].lower.value.present);near(entries[1].upper.value.value,2); + near(entries[2].lower.value.value,0);assert(entries[2].upper.kind==GECODE_OPT_RANGE_POSITIVE_INFINITY);near(entries[2].objective_slope.value,1); + OK(gecode_opt_v1_sensitivity_objective(h,ids[3],&single,sizeof(single)));assert(!single.requested&&!single.has_interval&&single.group.state==GECODE_OPT_SENSITIVITY_NOT_REQUESTED); + OK(gecode_opt_v1_sensitivity_equality_rhs(h,ids[5],&single,sizeof(single)));assert(single.index==2);near(single.anchor,3); + bad=ids[1];bad.model_id++;assert(gecode_opt_v1_sensitivity_objective(h,bad,&single,sizeof(single))==GECODE_OPT_MODEL_ERROR); + assert(gecode_opt_v1_sensitivity_objective(h,ids[0],&single,sizeof(single))==GECODE_OPT_MODEL_ERROR); + assert(gecode_opt_v1_sensitivity_equality_rhs(h,ids[4],&single,sizeof(single))==GECODE_OPT_MODEL_ERROR); + assert(gecode_opt_v1_sensitivity_objective(h,ids[5],&single,sizeof(single))==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_sensitivity_factor_order(h,order,1,&n));assert(n==1&&order[0].model_id==owner&&order[0].slot==ids[2].slot&&order[0].kind==GECODE_OPT_VARIABLE_ID); + OK(gecode_opt_v1_sensitivity_active_slots(h,GECODE_OPT_VARIABLE_ID,mask,4,&n));assert(n==4&&!mask[0]&&mask[1]&&mask[2]&&mask[3]); + OK(gecode_opt_v1_sensitivity_reference_checks(h,&checks,sizeof(checks)));assert(checks.primal.valid&&checks.primal.model_valid&&checks.kkt.accepted&&checks.basis_point_matches); + assert(checks.max_system_residual.present&&!checks.reserved&&!checks.kkt.reserved); + OK(gecode_opt_v1_sensitivity_copy_basis(h,&basis)); + {char text[1];assert(gecode_opt_v1_sensitivity_text(h,GECODE_OPT_SENSITIVITY_ENTRY_MESSAGE,0,text,1,&n)==GECODE_OPT_BUFFER_TOO_SMALL);assert(n>1);} + assert(gecode_opt_v1_sensitivity_text(h,GECODE_OPT_SENSITIVITY_MESSAGE,1,NULL,0,&n)==GECODE_OPT_INVALID_ARGUMENT); + assert(gecode_opt_v1_sensitivity_entries(h,NULL,sizeof(single)-1,0,&n)==GECODE_OPT_INVALID_ARGUMENT); + }else{ + assert(info.completion==GECODE_OPT_SENSITIVITY_ANALYSIS_REJECTED&&!info.has_sensitivity&&!before.factor_setup_attempted&&!before.basis_solves); + assert(gecode_opt_v1_sensitivity_entry(h,0,&single,sizeof(single))==GECODE_OPT_NO_SENSITIVITY); + assert(gecode_opt_v1_sensitivity_copy_basis(h,&basis)==GECODE_OPT_NO_BASIS&&!basis); + } + OK(gecode_opt_v1_sensitivity_work(h,&work,sizeof(work)));assert(!memcmp(&work,&before,sizeof(work))); + OK(gecode_opt_v1_model_set_objective_offset(m,999));OK(gecode_opt_v1_model_destroy(m));OK(gecode_opt_v1_lp_observed_result_destroy(observed)); + OK(gecode_opt_v1_sensitivity_destroy(h));OK(gecode_opt_v1_lp_observed_result_copy_result(copy,&ordinary)); + OK(gecode_opt_v1_result_info(ordinary,&result,sizeof(result)));assert(result.model_id==owner&&result.revision==revision); + if(available){double value;int32_t present;OK(gecode_opt_v1_result_number(ordinary,GECODE_OPT_OBJECTIVE,&present,&value));assert(present);near(value,10); + OK(gecode_opt_v1_basis_column(basis,ids[2],&status));assert(status==GECODE_OPT_LP_BASIS_BASIC);OK(gecode_opt_v1_basis_destroy(basis));} + OK(gecode_opt_v1_lp_observed_result_destroy(copy));OK(gecode_opt_v1_result_destroy(ordinary)); +} +static void admission(void){ + gecode_opt_handle m=0,observed=0,h=0,token=0;gecode_opt_id ids[6];gecode_opt_sensitivity_request_v1 r,saved; + gecode_opt_sensitivity_options_v1 o,bad;gecode_opt_sensitivity_info_v1 info;gecode_opt_sensitivity_work_v1 work;int i;int32_t cancelled; + model(&m,&observed,ids);r=request(0,ids[1]);saved=r;OK(gecode_opt_v1_sensitivity_options_default(&o,sizeof(o)));o.requests=&r;o.request_count=1; + for(i=0;i<13;i++){ + bad=o;r=saved;switch(i){case 0:bad.struct_size--;break;case 1:bad.reserved=1;break;case 2:bad.reserved_flags=1;break; + case 3:bad.checks.struct_size--;break;case 4:bad.checks.reserved=1;break;case 5:bad.limits.struct_size--;break; + case 6:bad.limits.reserved=1;break;case 7:bad.backend=99;break;case 8:bad.requests=NULL;break; + case 9:r.struct_size--;break;case 10:r.reserved=1;break;case 11:r.kind=99;break;default:r.entity.kind=GECODE_OPT_ROW_ID;break;} + h=99;assert(gecode_opt_v1_analyze_lp_sensitivity(observed,&bad,&h)==GECODE_OPT_INVALID_ARGUMENT&&!h); + } + r=saved;h=99;assert(gecode_opt_v1_analyze_lp_sensitivity(m,&o,&h)==GECODE_OPT_INVALID_HANDLE&&!h); + for(i=0;i<6;i++){ + bad=o;switch(i){case 0:bad.request_count=0;break;case 1:bad.checks.stationarity=NAN;break;case 2:bad.time_limit_seconds=-1;break; + case 3:bad.time_limit_seconds=0;break;case 4:bad.limits.max_work=0;break;default:bad.limits.max_requests=0;break;} + h=analyze(observed,&bad);OK(gecode_opt_v1_sensitivity_info(h,&info,sizeof(info)));OK(gecode_opt_v1_sensitivity_work(h,&work,sizeof(work))); + assert(!work.factor_setup_attempted&&!work.basis_solves&&!work.preparation_visits); + if(i<3){assert(info.completion==GECODE_OPT_SENSITIVITY_ANALYSIS_REJECTED&&info.reason==GECODE_OPT_SENSITIVITY_REASON_INVALID_SOURCE&&!info.has_stop_reason);} + else{assert(info.completion==GECODE_OPT_SENSITIVITY_INTERRUPTED&&info.has_stop_reason); + assert(info.stop_reason==(i==3?GECODE_OPT_TIME_LIMIT:i==4?GECODE_OPT_ITERATION_LIMIT:GECODE_OPT_MEMORY_LIMIT));} + OK(gecode_opt_v1_sensitivity_destroy(h)); + } + OK(gecode_opt_v1_cancellation_create(&token));OK(gecode_opt_v1_cancellation_is_cancelled(token,&cancelled));assert(!cancelled); + assert(gecode_opt_v1_cancellation_is_cancelled(token,NULL)==GECODE_OPT_INVALID_ARGUMENT); + assert(gecode_opt_v1_cancellation_is_cancelled(m,&cancelled)==GECODE_OPT_INVALID_HANDLE); + OK(gecode_opt_v1_cancellation_cancel(token));OK(gecode_opt_v1_cancellation_is_cancelled(token,&cancelled));assert(cancelled); + bad=o;bad.cancellation=token;h=analyze(observed,&bad);OK(gecode_opt_v1_sensitivity_info(h,&info,sizeof(info)));assert(info.stop_reason==GECODE_OPT_CANCELLED);OK(gecode_opt_v1_sensitivity_destroy(h)); + bad.time_limit_seconds=-1;h=analyze(observed,&bad);OK(gecode_opt_v1_sensitivity_info(h,&info,sizeof(info)));assert(info.reason==GECODE_OPT_SENSITIVITY_REASON_INVALID_SOURCE&&!info.has_stop_reason);OK(gecode_opt_v1_sensitivity_destroy(h)); + {gecode_opt_handle copied=0;OK(gecode_opt_v1_cancellation_copy(token,&copied));OK(gecode_opt_v1_cancellation_destroy(token)); + OK(gecode_opt_v1_cancellation_is_cancelled(copied,&cancelled));assert(cancelled);OK(gecode_opt_v1_cancellation_destroy(copied)); + copied=99;assert(gecode_opt_v1_cancellation_copy(m,&copied)==GECODE_OPT_INVALID_HANDLE&&!copied);} + OK(gecode_opt_v1_lp_observed_result_destroy(observed));OK(gecode_opt_v1_model_destroy(m)); +} +int main(void){int32_t lp,mip;OK(gecode_opt_v1_capabilities(GECODE_OPT_HIGHS,&available,&lp,&mip));analytic_history();admission();puts("C99 sensitivity endpoints, source/basis history, counted buffers and admission passed");return 0;} diff --git a/test/optimize/lp_sensitivity_coordinator.cpp b/test/optimize/lp_sensitivity_coordinator.cpp new file mode 100644 index 0000000000..b5ff6557b2 --- /dev/null +++ b/test/optimize/lp_sensitivity_coordinator.cpp @@ -0,0 +1,120 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +#include +#include +#include "lp_sensitivity_fixture.hpp" + +namespace O=Gecode::Optimize; +namespace { +int fault=0;std::size_t calls=0;std::string stop_event; +std::shared_ptr token; +struct Factor : O::Detail::LpSensitivityFactor { + ~Factor() override {if(fault==10&&token)token->cancel();} + std::vector solve(const std::vector& rhs,bool) override { + ++calls;assert(rhs.size()==1); + if(fault==5)return {}; + if(fault==6)return {std::numeric_limits::quiet_NaN()}; + if(fault==7&&calls==1)return {rhs[0]+1}; + if(fault==8&&calls==2)return {rhs[0]+1}; + if(fault==9&&calls==3)return {rhs[0]+1}; + if(fault==12&&calls==3)return {std::numeric_limits::denorm_min()}; + if(fault==13&&calls==3)return {1e-200}; + return rhs; // Selected B=[1]. This is not an optimization backend. + } +}; +void clear(const O::LpSensitivityResult& out) { + if(out.sensitivity)for(const auto& entry:out.sensitivity->entries()) + assert(entry.group.state!=O::LpSensitivityState::Available&&!entry.interval); +} +} +namespace Gecode {namespace Optimize {namespace Detail { +std::unique_ptr lp_sensitivity_test_factor( + const ModelSnapshot& source,const LpBasis&,double,const SolveBudget&) { + calls=0;if(fault==1)return {}; + auto out=std::make_unique();out->version="test factor"; + out->columns={0,1};out->rows={0};out->order={source.variables[1].variable}; + if(fault==2)out->columns={0}; + if(fault==3)out->order={Variable{source.model_id+1,1}}; + if(fault==4)out->order={source.variables[0].variable}; + return out; +} +void lp_sensitivity_test_checkpoint(const char* point,std::size_t index) { + if(stop_event==point&&token)token->cancel(); + if(fault==11&&std::string(point)=="after_interval"&&index==0)throw std::bad_alloc(); + if(fault==14&&std::string(point)=="after_interval"&&index==2)throw std::runtime_error("test backend boundary exception"); +} +}}} +int main() { + O::Model model;auto x=model.add_continuous(),y=model.add_continuous();auto row=model.add_row({{x,1},{y,1}},3,3);model.minimize({{x,2},{y,1}},7); + const auto source=SensitivityFixture::two_column_observed(model);O::LpSensitivityOptions options; + options.parameters={O::LpObjectiveParameter{x},O::LpObjectiveParameter{y},O::LpEqualityRhsParameter{row}}; + auto normal=O::analyze_lp_sensitivity(source,options);assert(normal.completion==O::LpSensitivityCompletion::Complete); + for(fault=1;fault<=8;++fault){auto out=O::analyze_lp_sensitivity(source,options);assert(out.completion==O::LpSensitivityCompletion::Rejected);clear(out);} + fault=9;auto partial=O::analyze_lp_sensitivity(source,options); + assert(partial.completion==O::LpSensitivityCompletion::Partial); + assert(partial.sensitivity->entries()[0].group.state==O::LpSensitivityState::Available); + assert(partial.sensitivity->entries()[1].group.state==O::LpSensitivityState::Rejected&&!partial.sensitivity->entries()[1].interval); + assert(partial.sensitivity->entries()[2].group.state==O::LpSensitivityState::Available); + token=std::make_shared();options.cancellation=token;stop_event="after_cleanup"; + auto stopped_partial=O::analyze_lp_sensitivity(source,options); + assert(stopped_partial.completion==O::LpSensitivityCompletion::Interrupted&&stopped_partial.stop_reason==O::Termination::Cancelled);clear(stopped_partial); + stop_event.clear();token.reset();options.cancellation.reset(); + // Deliberately loosen the linear-system tolerance to admit a corrupt tiny + // derivative. Finite endpoint overflow must still never become infinity. + auto loose=options;loose.checks.system_absolute=2;fault=12; + auto overflow=O::analyze_lp_sensitivity(source,loose); + assert(overflow.completion==O::LpSensitivityCompletion::Partial); + assert(overflow.sensitivity->entries()[1].group.reason==O::LpSensitivityReason::FailedIntervalChecks); + assert(!overflow.sensitivity->entries()[1].interval); + fault=13;auto tiny=O::analyze_lp_sensitivity(source,loose); + assert(tiny.completion==O::LpSensitivityCompletion::Complete); + const auto& finite=tiny.sensitivity->entries()[1].interval->upper; + assert(finite.kind==O::LpRangeEndKind::Finite&&finite.value&&*finite.value>1e199); + fault=0; + for(const char* phase:{"before_copy","after_copy","before_factor","after_factor","before_system","after_system","after_reference","before_interval","after_interval","after_cleanup"}) { + token=std::make_shared();options.cancellation=token;stop_event=phase; + auto stopped=O::analyze_lp_sensitivity(source,options);assert(stopped.completion==O::LpSensitivityCompletion::Interrupted&&stopped.stop_reason==O::Termination::Cancelled);clear(stopped); + } + stop_event.clear();token=std::make_shared();options.cancellation=token;fault=10; + auto cleanup=O::analyze_lp_sensitivity(source,options);assert(cleanup.stop_reason==O::Termination::Cancelled);clear(cleanup); + options.cancellation.reset();token.reset();fault=11; + auto allocation=O::analyze_lp_sensitivity(source,options);assert(allocation.reason==O::LpSensitivityReason::AllocationFailure&&allocation.stop_reason==O::Termination::MemoryLimit);clear(allocation); + fault=14;auto exception=O::analyze_lp_sensitivity(source,options); + assert(exception.reason==O::LpSensitivityReason::BackendFailure&&exception.completion==O::LpSensitivityCompletion::Rejected);clear(exception); + fault=0; + for(int limit=0;limit<7;++limit){auto o=options; + if(limit==0)o.limits.max_rows=0; + if(limit==1)o.limits.max_columns=1; + if(limit==2)o.limits.max_nonzeros=1; + if(limit==3)o.limits.max_factor_entries=0; + if(limit==4)o.limits.max_requests=2; + if(limit==5)o.limits.max_retained_slots=1; + if(limit==6)o.limits.max_work=0; + auto rejected=O::analyze_lp_sensitivity(source,o);assert(rejected.reason==O::LpSensitivityReason::ResourceLimit&&!rejected.work.factor_setup_attempted);clear(rejected);} + for(int option=0;option<7;++option){auto o=options; + if(option==0)o.checks.system_absolute=-1; + if(option==1)o.checks.system_relative=std::numeric_limits::quiet_NaN(); + if(option==2)o.parameters.clear(); + if(option==3)o.backend=static_cast(99); + if(option==4)o.time_limit_seconds=-1; + if(option==5)o.time_limit_seconds=std::numeric_limits::quiet_NaN(); + if(option==6){o.time_limit_seconds=-1;o.cancellation=std::make_shared();o.cancellation->cancel();} + auto rejected=O::analyze_lp_sensitivity(source,o);assert(rejected.completion==O::LpSensitivityCompletion::Rejected&&!rejected.work.factor_setup_attempted);clear(rejected);} + // Inactive semantic payloads are still owning copied history. Empty table + // tuple containers therefore consume retained slots even with no entries. + auto historical=model.snapshot();O::GlobalData inactive; + inactive.global={model.id(),0};inactive.active=false; + O::TableData table;table.tuples.resize(1000);inactive.payload=std::move(table);historical.globals.push_back(std::move(inactive)); + auto history=SensitivityFixture::two_column_observed(std::move(historical));auto retention=options; + retention.limits.max_retained_slots=normal.work.retained_slots+1000; + auto retained=O::analyze_lp_sensitivity(history,retention); + assert(retained.reason==O::LpSensitivityReason::ResourceLimit&&!retained.work.factor_setup_attempted);clear(retained); + assert(O::analyze_lp_sensitivity(source,options).completion==O::LpSensitivityCompletion::Complete); + std::cout<<"LP sensitivity malformed-factor, partial, caps, allocation and cleanup coordinator passed\n"; +} diff --git a/test/optimize/lp_sensitivity_factor.cpp b/test/optimize/lp_sensitivity_factor.cpp new file mode 100644 index 0000000000..aaaa7204c3 --- /dev/null +++ b/test/optimize/lp_sensitivity_factor.cpp @@ -0,0 +1,69 @@ +/* Pinned backend experiment: factor a supplied basis without optimization. */ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include "Highs.h" +#include +#include +#include +#include +#include + +static void near(double actual,double expected) { + assert(std::isfinite(actual)); + assert(std::abs(actual-expected)<=1e-9*std::max(1.0,std::abs(expected))); +} +static HighsLp model(double first,double second,bool singular=false) { + HighsLp lp; + lp.num_col_=2;lp.num_row_=2; + lp.col_cost_={1,2};lp.col_lower_={0,0};lp.col_upper_={10,10}; + lp.row_lower_={-100,-100};lp.row_upper_={100,100}; + lp.a_matrix_.format_=MatrixFormat::kColwise; + lp.a_matrix_.start_={0,2,4};lp.a_matrix_.index_={0,1,0,1}; + lp.a_matrix_.value_={2*first,8*second,4*first,(singular?16:32)*second}; + return lp; +} +static void mixed(double first,double second) { + Highs h;assert(h.setOptionValue("output_flag",false)==HighsStatus::kOk); + assert(h.setOptionValue("threads",1)==HighsStatus::kOk); + assert(h.passModel(model(first,second))==HighsStatus::kOk); + HighsBasis b;b.alien=false;b.valid=true;b.useful=true; + b.col_status={HighsBasisStatus::kBasic,HighsBasisStatus::kLower}; + b.row_status={HighsBasisStatus::kUpper,HighsBasisStatus::kBasic}; + assert(h.setBasis(b,"sensitivity factor-only experiment")==HighsStatus::kOk); + assert(!h.hasInvert()); + std::vector order(2); + assert(h.getBasicVariables(order.data())==HighsStatus::kOk); + assert(h.hasInvert());assert(order[0]==0&&order[1]==-2); + assert(h.getBasis().col_status==b.col_status&&h.getBasis().row_status==b.row_status); + // B = [ A_column_0, +e_1 ]. Logical coordinate z is -row activity. + double rhs[]={6*first,40*second},answer[2]={}; + assert(h.getBasisSolve(rhs,answer)==HighsStatus::kOk); + near(answer[0],3);near(answer[1],16*second); + near(2*first*answer[0],rhs[0]);near(8*second*answer[0]+answer[1],rhs[1]); + const double dual[]={4/first,3/second}; + const double trhs[]={32,3/second}; + assert(h.getBasisTransposeSolve(trhs,answer)==HighsStatus::kOk); + near(answer[0],dual[0]);near(answer[1],dual[1]); + near(2*first*answer[0]+8*second*answer[1],trhs[0]);near(answer[1],trhs[1]); + // Original first-row RHS movement uses +e_0: z_0'=-1, x'=B^-1 e_0. + const double direction_rhs[]={1,0}; + assert(h.getBasisSolve(direction_rhs,answer)==HighsStatus::kOk); + near(2*first*answer[0],1);near(8*second*answer[0]+answer[1],0); + assert(h.getModelStatus()==HighsModelStatus::kNotset); + assert(!h.getInfo().valid); +} +int main() { + mixed(1,1);mixed(1e-6,1e6); + Highs h;assert(h.setOptionValue("output_flag",false)==HighsStatus::kOk); + assert(h.passModel(model(1,1,true))==HighsStatus::kOk); + HighsBasis b;b.alien=false;b.valid=true;b.useful=true; + b.col_status={HighsBasisStatus::kBasic,HighsBasisStatus::kBasic}; + b.row_status={HighsBasisStatus::kLower,HighsBasisStatus::kUpper}; + assert(h.setBasis(b,"singular factor-only experiment")==HighsStatus::kOk); + std::vector order(2); + assert(h.getBasicVariables(order.data())==HighsStatus::kError); + assert(h.getBasis().col_status==b.col_status&&h.getBasis().row_status==b.row_status); + assert(h.getModelStatus()==HighsModelStatus::kNotset); + std::cout<<"factor-only normal/scaled signs and singular rejection passed\n"; +} diff --git a/test/optimize/lp_sensitivity_fixture.hpp b/test/optimize/lp_sensitivity_fixture.hpp new file mode 100644 index 0000000000..ed3f8bc58b --- /dev/null +++ b/test/optimize/lp_sensitivity_fixture.hpp @@ -0,0 +1,24 @@ +/* Explicit immutable O1 data for testing without an optimization backend. */ +#ifndef TEST_OPTIMIZE_LP_SENSITIVITY_FIXTURE_HPP +#define TEST_OPTIMIZE_LP_SENSITIVITY_FIXTURE_HPP +#include +#include +namespace SensitivityFixture { +namespace O=Gecode::Optimize; +inline O::LpObservedResult two_column_observed(O::ModelSnapshot source) { + O::LpObservationOptions options; + O::LpObservedResult out;out.result.model_id=source.model_id;out.result.revision=source.revision; + out.result.termination=O::Termination::Optimal;out.result.guarantee=O::Guarantee::Numerical; + out.result.backend="Test explicit original O1 data";out.result.objective=10;out.result.best_bound=10; + out.result.absolute_gap=0;out.result.relative_gap=0;out.result.values={0,3};out.result.active_variables={true,true};out.result.solution_validated=true; + auto data=O::Detail::LpObservationAccess::create(source,options);out.observations=data; + O::Detail::LpBackendObservations raw;raw.attempted=true;raw.timely=true;raw.complete=true; + raw.info_valid=raw.value_valid=raw.primal_feasible=raw.dual_valid=raw.dual_feasible=raw.basis_valid=raw.info_basis_valid=true; + raw.model_id=source.model_id;raw.revision=source.revision;raw.column_slots={0,1};raw.row_slots={0}; + raw.column_duals={1,0};raw.row_duals={1};raw.column_basis={O::LpBasisStatus::Lower,O::LpBasisStatus::Basic};raw.row_basis={O::LpBasisStatus::Lower}; + O::SolveBudget budget(options.solve);O::Detail::LpObservationAccess::finish(*data,out.result,raw,budget); + assert(data->basis().state==O::LpObservationState::Available&&data->checks().accepted);return out; +} +inline O::LpObservedResult two_column_observed(const O::Model& model) {return two_column_observed(model.snapshot());} +} +#endif diff --git a/test/optimize/lp_sensitivity_oracle.hpp b/test/optimize/lp_sensitivity_oracle.hpp new file mode 100644 index 0000000000..f5ec46d5d4 --- /dev/null +++ b/test/optimize/lp_sensitivity_oracle.hpp @@ -0,0 +1,63 @@ +/* Independent exact vertex oracle for small boxed two-variable equality LPs. + * It compares objectives at enumerated vertices, never uses a simplex tableau. + * Fixtures keep operands small enough for checked int64 arithmetic. */ +#ifndef TEST_OPTIMIZE_LP_SENSITIVITY_ORACLE_HPP +#define TEST_OPTIMIZE_LP_SENSITIVITY_ORACLE_HPP +#include +#include +#include +#include +#include +#include +#include +namespace SensitivityOracle { +struct Q { + std::int64_t n=0,d=1; + Q(std::int64_t a=0,std::int64_t b=1):n(a),d(b){assert(b);if(d<0){n=-n;d=-d;}auto g=std::gcd(n,d);n/=g;d/=g;} + double value() const{return static_cast(n)/d;} +}; +inline std::int64_t product(std::int64_t a,std::int64_t b){assert(std::abs(a)<=100000000&&std::abs(b)<=100000000);return a*b;} +inline Q operator+(Q a,Q b){return {product(a.n,b.d)+product(b.n,a.d),product(a.d,b.d)};} +inline Q operator-(Q a,Q b){return {product(a.n,b.d)-product(b.n,a.d),product(a.d,b.d)};} +inline Q operator*(Q a,Q b){return {product(a.n,b.n),product(a.d,b.d)};} +inline Q operator/(Q a,Q b){assert(b.n);return {product(a.n,b.d),product(a.d,b.n)};} +inline bool operator<(Q a,Q b){return product(a.n,b.d) lower,upper;}; +struct BoxLine { + Q a,b,rhs,lx,ux,ly,uy,cx,cy; + std::array reference; + bool maximize=false; + std::vector> vertices() const { + std::vector> out; + auto add=[&](Q x,Q y){if(x +#include +#include +#include +#include +#include +#include + +namespace C = Gecode::Experimental::LpCertificate; +using I = std::int64_t; +using V = std::vector; +static void check(bool condition,const char* message) { + if (!condition) throw std::runtime_error(message); +} +struct Matrix { + std::vector start{0},column; + V value; + std::size_t n=0; + C::SparseMatrixView view() const { return {start,column,value,n}; } +}; +static Matrix sparse(const V& a,std::size_t m,std::size_t n) { + Matrix result; result.n=n; + for (std::size_t i=0;i bad={ + {{},{0,1},{1,2},2}, // Missing row offsets. + {{1,2},{0,1},{1,2},2}, + {{0,1},{0,1},{1,2},2}, // Wrong terminal offset. + {{0,3},{0,1},{1,2},2}, + {{0,2},{0},{1,2},2}, + {{0,2},{0,2},{1,2},2}, // Out of range. + {{0,2},{0,0},{1,2},2}, // Duplicate column. + {{0,2},{1,0},{1,2},2}, // Unsorted column. + {{0,2},{0,1},{1,0},2}, // Explicit zero. + {{0,2},{0,1},{1,2},3}, // Mismatched width. + }; + for (const auto& matrix:bad) { + C::PreparationStats work{73,74,75}; + check(!C::prepare(matrix.view(),{1},{1,2},{1},kept,&work),"malformed CSR accepted"); + check(work.rows_visited==73 && work.nonzero_products==74 && work.residuals_initialized==75, + "failed preparation changed work output"); + I bound=0; + check(kept.lower_bound({0,0},{1,1},bound) && bound==1,"failed preparation replaced certificate"); + bound=991; + check(!C::lower_bound(matrix.view(),{1},{1,2},{0,0},{1,1},{1},bound) && bound==991, + "failed sparse bound changed output"); + } + Matrix decreasing{{0,2,1,2},{0,1},{1,1},2}; + check(!C::valid_sparse(decreasing.view(),3,2),"decreasing row offsets accepted"); + check(!C::valid_sparse(unit.view(),std::numeric_limits::max(),2), + "row count overflow accepted"); + for (double dual:{std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN(),std::ldexp(1.0,43)}) { + check(!C::prepare(unit.view(),{1},{1,2},{dual},kept),"corrupt dual accepted"); + } + Matrix overflow{{0,1,2,3,4,5},{0,0,0,0,0},V(5,std::numeric_limits::max()),1}; + check(!C::prepare(overflow.view(),V(5,0),{0},std::vector(5,std::ldexp(1.0,42)),kept), + "sparse residual overflow accepted"); + check(!C::prepare(overflow.view(),V(5,std::numeric_limits::max()),{0}, + std::vector(5,std::ldexp(1.0,42)),kept),"sparse constant overflow accepted"); +} +static void equivalence() { + std::mt19937 random(357187); + for (unsigned trial=0;trial<3000;++trial) { + const std::size_t n=random()%8,m=random()%9; + V a(m*n),b(m),c(n),lower(n),upper(n); + std::vector dual(m); + for (auto& coefficient:a) coefficient=random()%4 ? 0 : static_cast(random()%15)-7; + for (auto& rhs:b) rhs=static_cast(random()%15)-7; + for (auto& cost:c) cost=static_cast(random()%21)-10; + for (auto& q:dual) q=(static_cast(random()%83)-20)/11.0; + for (std::size_t j=0;j(random()%31)-15; + I db=0,sb=0; + std::vector df,sf; + check(dense.filter(lower,upper,upper_objective,db,df) && + csr.filter(lower,upper,upper_objective,sb,sf) && db==sb && df==sf, + "dense/sparse bound or filtering disagreement"); + I direct=992; + check(C::lower_bound(matrix.view(),b,c,lower,upper,dual,direct) && direct==db, + "sparse direct/prepared bound disagreement"); + // Independent enumeration: every feasible assignment obeys the bound, + // and every feasible assignment within the cutoff survives filtering. + for (unsigned mask=0;mask<(1U<>j)&1U; + if (xupper[j]) feasible=false; + cost+=c[j]*x; + } + for (std::size_t i=0;i>j)&1U); + if (activity>j)&1U))),"sparse filtering removed feasible assignment"); + } + // The same immutable affine certificate may be applied to a looser sibling. + lower.assign(n,0); upper.assign(n,1); + check(dense.lower_bound(lower,upper,db) && csr.lower_bound(lower,upper,sb) && db==sb, + "sparse sibling box disagreement"); + } +} +static void scaling() { + std::size_t previous_bytes=0; + for (std::size_t n:{std::size_t(32768),std::size_t(65536)}) { + Matrix matrix; matrix.n=n; + matrix.start.reserve(n+1); matrix.column.reserve(n); matrix.value.reserve(n); + for (std::size_t i=0;i(n,1),certificate,&work), + "large sparse certificate failed"); + check(work.rows_visited==n && work.nonzero_products==n && work.residuals_initialized==n, + "large sparse preparation performed more than linear arithmetic"); + I bound=0; + check(certificate.lower_bound(V(n,0),V(n,1),bound) && bound==static_cast(n), + "large sparse certificate bound"); + } +} +int main() { + try { + if (!C::supported) { + Matrix matrix; I bound=123; + check(!C::lower_bound(matrix.view(),{},{},{},{},{},bound) && bound==123, + "unsupported arithmetic produced a bound"); + std::cout << "PASS sparse unsupported-arithmetic fallback\n"; return 0; + } + corruption(); equivalence(); scaling(); + std::cout << "PASS sparse CSR corruption/overflow, 3000 dense/sparse oracle/filter cases, " + "32768/65536 diagonal storage and operation scaling\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} diff --git a/test/optimize/lp_sparse_storage.cpp b/test/optimize/lp_sparse_storage.cpp new file mode 100644 index 0000000000..d9d04b585e --- /dev/null +++ b/test/optimize/lp_sparse_storage.cpp @@ -0,0 +1,118 @@ +// Structural memory regression: dense materialization cannot fit this allocation +// guard. No timing threshold, RSS comparison or performance claim is involved. +#include +#include +#include +#include +#include +#include +#include + +static std::atomic allocation_limit{std::numeric_limits::max()}; +static std::atomic largest_allocation{0}; +void* operator new(std::size_t size) { + if (size>allocation_limit.load()) throw std::bad_alloc(); + auto previous=largest_allocation.load(); + while (previous&,LP::Frequency); + LegacyPost post=&LP::binary_linear_minimize; + (void) post; + struct Derived : LP::Backend { using LP::Backend::Backend; }; + const std::shared_ptr derived; + LP::binary_linear_minimize(home,x,cost,derived); +} + +class Problem : public Gecode::Space { +public: + Gecode::IntVarArray x; + Gecode::IntVar cost; + Problem(const std::shared_ptr& backend) + : x(*this,static_cast(backend->model.c.size()),0,1),cost(*this,0,x.size()) { + LP::binary_linear_minimize(*this,x,cost,backend); + } + Problem(Problem& other) : Gecode::Space(other) { + x.update(*this,other.x); cost.update(*this,other.cost); + } + Gecode::Space* copy() override { return new Problem(*this); } +}; +class IntegerProblem : public Gecode::Space { +public: + Gecode::IntVarArray x; + Gecode::IntVar cost; + IntegerProblem(const std::shared_ptr& backend) + : x(*this,static_cast(backend->model.linear.c.size()),-1,1),cost(*this,-x.size(),x.size()) { + LP::integer_linear_minimize(*this,x,cost,backend); + } + IntegerProblem(IntegerProblem& other):Gecode::Space(other) { + x.update(*this,other.x);cost.update(*this,other.cost); + } + Gecode::Space* copy() override {return new IntegerProblem(*this);} +}; +int main() { + try { + const std::size_t n=16384; + // A dense int64 array is 2 GiB. All stages must work with allocations + // below 16 MiB, including CSR backend import and native actor posting. + allocation_limit.store(16*1024*1024); + LP::SparseLinearModel model; + model.row_start.reserve(n+1); model.column.reserve(n); model.a.reserve(n); + model.b.assign(n,1); model.c.assign(n,1); + for (std::size_t i=0;i(std::move(model)); + check(backend->model.nonzeros()==n,"CSR payload is not sparse"); + { + Problem problem(backend); + check(problem.status()!=Gecode::SS_FAILED,"large sparse posting failed"); + check(problem.cost.assigned() && problem.cost.val()==static_cast(n), + "native original sparse rows/objective not enforced"); + for (int j=0;jbound(std::vector(n,1), + std::vector(n,1),true); + check(bound.valid && bound.lower_bound==static_cast(n), + "large sparse backend certificate failed"); + auto integer_backend=std::make_shared(LP::BoundedIntegerModel{ + backend->model,std::vector(n,-1),std::vector(n,1)}); + { + IntegerProblem problem(integer_backend); + check(problem.status()!=Gecode::SS_FAILED && problem.cost.assigned() && + problem.cost.val()==static_cast(n),"large sparse integer native posting"); + for(int j=0;jbound(std::vector(n,1), + std::vector(n,1),true); + check(integer_bound.valid && integer_bound.lower_bound==static_cast(n), + "large sparse integer backend certificate failed"); + const auto largest=largest_allocation.load(); + allocation_limit.store(std::numeric_limits::max()); + std::cout << "PASS binary and integer 16384x16384 CSR backend/import/native-posting/certificate with " + "16 MiB single-allocation guard; largest request " << largest << " bytes\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} diff --git a/test/optimize/lp_strengthening.cpp b/test/optimize/lp_strengthening.cpp new file mode 100644 index 0000000000..60a19346f2 --- /dev/null +++ b/test/optimize/lp_strengthening.cpp @@ -0,0 +1,219 @@ +// Standalone exhaustive correctness tests for binary linear strengthening. +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace LP=Gecode::Experimental::LpRelaxation; +namespace S=LP::Strengthening; +using I=std::int64_t; +using V=std::vector; + +static void require(bool condition,const char* message) { + if (!condition) throw std::runtime_error(message); +} + +static std::set feasible(const LP::LinearModel& model) { + const std::size_t n=model.c.size(); + std::set result; + for (unsigned int mask=0; mask<(1U<((mask>>j)&1U); + valid=activity>=model.b[i]; + } + if (valid) result.insert(mask); + } + return result; +} + +static bool contains(const LP::LinearModel& model,const V& row,I rhs) { + const auto n=model.c.size(); + for (std::size_t i=0; i=0 && value<=denominator,"fractional witness outside binary box"); + for (std::size_t i=0; i::max(),1,out) && out==17, + "positive addition overflow not rejected"); + require(!S::Detail::add(std::numeric_limits::min(),-1,out) && out==17, + "negative addition overflow not rejected"); + require(!S::Detail::subtract(std::numeric_limits::min(),1,out) && out==17, + "negative subtraction overflow not rejected"); + require(!S::Detail::subtract(std::numeric_limits::max(),-1,out) && out==17, + "positive subtraction overflow not rejected"); + require(S::Detail::add(std::numeric_limits::min(), + std::numeric_limits::max(),out) && out==-1, + "valid extreme addition was rejected"); + + S::Options normalize; + normalize.max_cuts=0; + auto result=equivalent({{6,10},{7},{-2,3}},normalize); + require(contains(result.model,{3,5},4),"positive gcd ceiling is incorrect"); + result=equivalent({{-6,-10},{-7},{-2,3}},normalize); + require(contains(result.model,{-3,-5},-3),"negative gcd ceiling is incorrect"); + result=equivalent({{2,4, 1,2, 1,2},{2,1,2},{-2,3}},normalize); + require(result.model.b.size()==1 && contains(result.model,{1,2},2), + "normalized duplicate/dominated rows not combined"); + require(result.stats.duplicates_removed==2,"wrong duplicate count"); + result=equivalent({{1,-2, 0,0},{-2,0},{-2,3}}); + require(result.model.b.empty(),"tautologies not removed"); + result=equivalent({{1,-2},{2},{-2,3}}); + require(result.stats.infeasible,"impossible row not recognized"); + equivalent({{}, {}, {}}); + + S::Options pairs; + pairs.fixings=false; pairs.cliques=false; pairs.covers=false; + const LP::LinearModel pair_model{{-5,-5,-1},{-9},{-3,4,2}}; + result=equivalent(pair_model,pairs); + require(contains(result.model,{-1,-1,0},-1),"binary pair conflict not posted"); + require(result.stats.pair_cuts>0,"pair cut counter did not increment"); + cuts_off(pair_model,result,{9,9,0},10); + + const LP::LinearModel triangle{ + {-1,-1,0, -1,0,-1, 0,-1,-1},{-1,-1,-1},{-3,4,2}}; + result=equivalent(triangle); + require(contains(result.model,{-1,-1,-1},-1),"clique cut not posted"); + require(result.stats.clique_cuts>0,"clique cut counter did not increment"); + cuts_off(triangle,result,{1,1,1},2); + + const LP::LinearModel complemented_triangle{ + {1,-1,0, 1,0,1, 0,-1,1},{0,1,0},{4,-3,-2}}; + result=equivalent(complemented_triangle); + require(contains(result.model,{1,-1,1},1),"complemented clique translation failed"); + cuts_off(complemented_triangle,result,{1,1,1},2); + + const LP::LinearModel cover{{-4,-4,-3},{-10},{2,-3,5}}; + result=equivalent(cover); + require(contains(result.model,{-1,-1,-1},-2),"minimal cover cut not posted"); + require(result.stats.cover_cuts>0,"cover cut counter did not increment"); + cuts_off(cover,result,{3,3,2},3); + + const LP::LinearModel complemented_cover{{4,-4,3},{-3},{-4,3,2}}; + result=equivalent(complemented_cover); + require(contains(result.model,{1,-1,1},0),"complemented cover translation failed"); + cuts_off(complemented_cover,result,{0,3,1},3); + + result=equivalent({{-2,-3},{-1},{-1,3}}); + require(contains(result.model,{-1,0},0) && contains(result.model,{0,-1},0), + "overweight literals were not fixed"); + require(result.stats.fixing_cuts==2,"wrong fixing cut count"); + result=equivalent({{2,-3},{1,-1},{-1}}); + require(result.stats.infeasible,"opposite forced literals not recognized"); + + bool rejected=false; + try { S::strengthen({{1000000001LL},{1},{0}}); } + catch (const std::invalid_argument&) { rejected=true; } + require(rejected,"out-of-range input coefficient not rejected"); + + std::mt19937 random(973321); + std::size_t configurations=0, feasible_models=0, infeasible_models=0; + std::size_t pair_cuts=0, clique_cuts=0, cover_cuts=0; + constexpr unsigned int trials=2000; + for (unsigned int trial=0; trial(random()%21)-10; + for (std::size_t i=0; i(random()%19)-9)*divisor; + activity+=model.a[i*n+j]*static_cast((witness>>j)&1U); + } + model.b[i]=(trial%2==0) ? activity-static_cast(random()%8) + : static_cast(random()%41)-20; + } + if (feasible(model).empty()) ++infeasible_models; + else ++feasible_models; + auto full=equivalent(model); + pair_cuts+=full.stats.pair_cuts; + clique_cuts+=full.stats.clique_cuts; + cover_cuts+=full.stats.cover_cuts; + ++configurations; + S::Options limited; + limited.gcd=trial%3!=0; + limited.max_cuts=trial%4; + limited.max_graph_variables=trial%2 ? 0 : 128; + limited.max_pair_checks=trial%5; + limited.max_clique_size=2+trial%5; + limited.max_cover_cuts_per_row=trial%3; + limited.max_cover_terms=trial%2 ? 4 : 512; + limited.max_cover_starts=trial%3; + equivalent(model,limited); + ++configurations; + if (trial<20) { + const auto again=S::strengthen(model); + require(full.model.a==again.model.a && full.model.b==again.model.b, + "strengthening is nondeterministic"); + } + } + require(feasible_models>=trials/2 && infeasible_models>0, + "random suite lacks feasible/infeasible coverage"); + require(pair_cuts>0 && clique_cuts>0 && cover_cuts>0, + "random suite failed to exercise all cut families"); + std::cout << "PASS strengthening edge cases, fractional witnesses, and " + << configurations << " exhaustive configurations over " << trials + << " models (" << feasible_models << " feasible, " + << infeasible_models << " infeasible); " + << pair_cuts << " pair, " << clique_cuts << " clique, " + << cover_cuts << " cover cuts\n"; + return EXIT_SUCCESS; + } catch (const std::exception& error) { + std::cerr << "FAIL: " << error.what() << '\n'; + return EXIT_FAILURE; + } +} diff --git a/test/optimize/minizinc-fixtures/mzn-alias-holes.mzn b/test/optimize/minizinc-fixtures/mzn-alias-holes.mzn new file mode 100644 index 0000000000..3f220bdd02 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-alias-holes.mzn @@ -0,0 +1 @@ +var {1,3,7}:x; var 0..9:y=x; constraint y>=2; solve minimize y; output [show([x,y]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-all-different.mzn b/test/optimize/minizinc-fixtures/mzn-all-different.mzn new file mode 100644 index 0000000000..b8952921b5 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-all-different.mzn @@ -0,0 +1 @@ +include "all_different.mzn"; array[1..2] of var 1..2:x; constraint all_different(x); solve minimize x[1]; output [show(x),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-circuit-negative.mzn b/test/optimize/minizinc-fixtures/mzn-circuit-negative.mzn new file mode 100644 index 0000000000..d32c5d49f4 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-circuit-negative.mzn @@ -0,0 +1 @@ +include "circuit.mzn"; array[-2..0] of var -2..0:x; constraint circuit(x); solve minimize x[-2]; output [show(x),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-circuit-offset.mzn b/test/optimize/minizinc-fixtures/mzn-circuit-offset.mzn new file mode 100644 index 0000000000..4ee5d8d840 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-circuit-offset.mzn @@ -0,0 +1 @@ +include "circuit.mzn"; array[2..4] of var 2..4:x; constraint circuit(x); solve minimize x[2]; output [show(x),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-cumulative-fixed-alias.mzn b/test/optimize/minizinc-fixtures/mzn-cumulative-fixed-alias.mzn new file mode 100644 index 0000000000..e8c278191b --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-cumulative-fixed-alias.mzn @@ -0,0 +1 @@ +include "cumulative.mzn"; var 1..1:d; var 1..1:r=d; var 0..2:x; constraint cumulative([x,1,1],[d,1,1],[r,1,1],2); solve minimize x; output [show([x,d,r]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-cumulative-half-open.mzn b/test/optimize/minizinc-fixtures/mzn-cumulative-half-open.mzn new file mode 100644 index 0000000000..3fc8ce8411 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-cumulative-half-open.mzn @@ -0,0 +1 @@ +include "cumulative.mzn"; var 0..2:x; constraint cumulative([x,1,1],[1,1,1],[1,1,1],2); solve minimize x; output [show([x,1]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-cumulative-unsat.mzn b/test/optimize/minizinc-fixtures/mzn-cumulative-unsat.mzn new file mode 100644 index 0000000000..91cdc5807e --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-cumulative-unsat.mzn @@ -0,0 +1 @@ +include "cumulative.mzn"; var 0..2:x; constraint cumulative([x,x,x],[1,1,1],[1,1,1],2); solve satisfy; output [show([x]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-cumulative-zero.mzn b/test/optimize/minizinc-fixtures/mzn-cumulative-zero.mzn new file mode 100644 index 0000000000..a484917f26 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-cumulative-zero.mzn @@ -0,0 +1 @@ +include "cumulative.mzn"; var 0..2:x; constraint cumulative([x,1,1],[0,1,1],[2,1,1],2); solve minimize x; output [show([x]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-element-offset.mzn b/test/optimize/minizinc-fixtures/mzn-element-offset.mzn new file mode 100644 index 0000000000..b71dab5f3e --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-element-offset.mzn @@ -0,0 +1 @@ +array[-1..0] of int:a=array1d(-1..0,[3,2]); var -1..0:i; var 2..3:x; constraint x=a[i]; solve minimize x; output [show([i,x]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-linear-max.mzn b/test/optimize/minizinc-fixtures/mzn-linear-max.mzn new file mode 100644 index 0000000000..28ef6d2038 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-linear-max.mzn @@ -0,0 +1 @@ +var -2..2:x; var -2..2:y; constraint x+y<=1; solve maximize 3*x-y+2; output [show([x,y,3*x-y+2]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-linear-min.mzn b/test/optimize/minizinc-fixtures/mzn-linear-min.mzn new file mode 100644 index 0000000000..26fee3002e --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-linear-min.mzn @@ -0,0 +1 @@ +var 0..3:x; var 0..3:y; constraint x+y>=3; solve minimize 2*x+y-4; output [show([x,y,2*x+y-4]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-native-controls.mzn b/test/optimize/minizinc-fixtures/mzn-native-controls.mzn new file mode 100644 index 0000000000..0e763c75a8 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-native-controls.mzn @@ -0,0 +1,11 @@ +% Multiple weighted rows avoid a single-knapsack special case. Boolean originals +% keep binary reliability and Hamming neighborhoods eligible after flattening. +array[1..6] of var bool: take; +array[1..6] of int: weight = [3,3,2,2,4,1]; +array[1..6] of int: profit = [5,4,3,3,6,1]; +constraint sum(i in 1..6)(weight[i]*bool2int(take[i])) <= 8; +constraint 3*bool2int(take[1])+3*bool2int(take[2]) <= 5; +constraint bool2int(take[3])+bool2int(take[4])+bool2int(take[5]) <= 2; +var int: value = sum(i in 1..6)(profit[i]*bool2int(take[i])); +solve maximize value; +output [show([bool2int(take[i]) | i in 1..6] ++ [value]), "\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-native-knapsack.mzn b/test/optimize/minizinc-fixtures/mzn-native-knapsack.mzn new file mode 100644 index 0000000000..d6c845907e --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-native-knapsack.mzn @@ -0,0 +1,9 @@ +% Integer 0..1 variables and the compiler-introduced objective must reach +% automatic exact knapsack DP after the safe objective equality is eliminated. +array[1..6] of var 0..1: take; +array[1..6] of int: weight = [3,3,2,2,4,1]; +array[1..6] of int: profit = [5,4,3,3,6,1]; +constraint sum(i in 1..6)(weight[i]*take[i]) <= 8; +var int: value = sum(i in 1..6)(profit[i]*take[i]); +solve maximize value; +output [show([take[i] | i in 1..6] ++ [value]), "\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-regular-alias.mzn b/test/optimize/minizinc-fixtures/mzn-regular-alias.mzn new file mode 100644 index 0000000000..483a869a82 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-regular-alias.mzn @@ -0,0 +1 @@ +include "regular.mzn"; var 1..2:x; constraint regular([x,x,x],2,2,[|2,1|1,2|],1,{1}); solve minimize x; output [show([x,x,x]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-regular-complement.mzn b/test/optimize/minizinc-fixtures/mzn-regular-complement.mzn new file mode 100644 index 0000000000..83de412ab9 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-regular-complement.mzn @@ -0,0 +1 @@ +include "regular.mzn"; array[1..2] of var 1..2:x; var bool:b; constraint b <-> regular(x,1,2,[|1,0|],1,{1}); constraint not b; solve minimize 2*x[1]+x[2]; output [show(x),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-regular-dead.mzn b/test/optimize/minizinc-fixtures/mzn-regular-dead.mzn new file mode 100644 index 0000000000..1ed1013ced --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-regular-dead.mzn @@ -0,0 +1 @@ +include "regular.mzn"; var 1..1:x; constraint regular([x],1,1,[|0|],1,{1}); solve satisfy; output [show([x]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-regular-decomposed-reif.mzn b/test/optimize/minizinc-fixtures/mzn-regular-decomposed-reif.mzn new file mode 100644 index 0000000000..0a7694fbfe --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-regular-decomposed-reif.mzn @@ -0,0 +1 @@ +include "regular.mzn"; array[1..2] of var 1..2:x; var bool:b; constraint b <-> regular(x,1,2,[|1,0|],1,{1}); solve maximize bool2int(b); output [show(x),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-regular-empty.mzn b/test/optimize/minizinc-fixtures/mzn-regular-empty.mzn new file mode 100644 index 0000000000..b8b5eb3921 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-regular-empty.mzn @@ -0,0 +1 @@ +include "regular.mzn"; constraint regular([],1,1,[|1|],1,{1}); solve satisfy; output ["[]\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-regular.mzn b/test/optimize/minizinc-fixtures/mzn-regular.mzn new file mode 100644 index 0000000000..68b49b2d1d --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-regular.mzn @@ -0,0 +1 @@ +include "regular.mzn"; array[1..2] of var 1..2:x; constraint regular(x,3,2,[|2,3|0,3|2,0|],1,{3}); solve minimize x[1]; output [show(x),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-reified-le.mzn b/test/optimize/minizinc-fixtures/mzn-reified-le.mzn new file mode 100644 index 0000000000..82453a0246 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-reified-le.mzn @@ -0,0 +1 @@ +var 0..2:x; var bool:b; constraint b <-> (x<=1); constraint b; solve maximize x; output [show([x,bool2int(b)]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-reject-float.mzn b/test/optimize/minizinc-fixtures/mzn-reject-float.mzn new file mode 100644 index 0000000000..a4f0d55ea7 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-reject-float.mzn @@ -0,0 +1 @@ +var 0.0..2.0:x; constraint x>=0.5; solve minimize x; output [show(x),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-reject-reif-eq.mzn b/test/optimize/minizinc-fixtures/mzn-reject-reif-eq.mzn new file mode 100644 index 0000000000..84aa2eee9e --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-reject-reif-eq.mzn @@ -0,0 +1 @@ +var 0..2:x; var 0..2:y; var bool:b; constraint b <-> (x=y); solve maximize bool2int(b); output [show([x,y]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-reject-search.mzn b/test/optimize/minizinc-fixtures/mzn-reject-search.mzn new file mode 100644 index 0000000000..d0b16e363e --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-reject-search.mzn @@ -0,0 +1 @@ +var 0..2:x; solve :: int_search([x],input_order,indomain_min,complete) satisfy; output [show(x),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-reject-set.mzn b/test/optimize/minizinc-fixtures/mzn-reject-set.mzn new file mode 100644 index 0000000000..2768d6a769 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-reject-set.mzn @@ -0,0 +1 @@ +var set of 1..3:s; constraint card(s)=2; solve satisfy; output [show(s),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-reject-times.mzn b/test/optimize/minizinc-fixtures/mzn-reject-times.mzn new file mode 100644 index 0000000000..19017fcd92 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-reject-times.mzn @@ -0,0 +1 @@ +var 1..3:x; var 1..3:y; var 1..9:z; constraint z=x*y; solve minimize z; output [show([x,y,z]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-reject-variable-cumulative.mzn b/test/optimize/minizinc-fixtures/mzn-reject-variable-cumulative.mzn new file mode 100644 index 0000000000..e140f3b963 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-reject-variable-cumulative.mzn @@ -0,0 +1 @@ +include "cumulative.mzn"; var 1..2:d; var 0..2:x; constraint cumulative([x,1,2],[d,1,1],[1,1,1],2); solve satisfy; output [show([x,d]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-satisfy.mzn b/test/optimize/minizinc-fixtures/mzn-satisfy.mzn new file mode 100644 index 0000000000..7396cc7181 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-satisfy.mzn @@ -0,0 +1 @@ +var 1..2:x; solve satisfy; output [show([x]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-table-alias.mzn b/test/optimize/minizinc-fixtures/mzn-table-alias.mzn new file mode 100644 index 0000000000..b5a3085d24 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-table-alias.mzn @@ -0,0 +1 @@ +include "table.mzn"; var 1..2:x; constraint table([x,x],[|1,1|2,2|]); solve minimize x; output [show([x,x]),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-table.mzn b/test/optimize/minizinc-fixtures/mzn-table.mzn new file mode 100644 index 0000000000..b24ee1ccd3 --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-table.mzn @@ -0,0 +1 @@ +include "table.mzn"; array[1..2] of var 1..2:x; constraint table(x,[|1,2|2,1|]); solve minimize x[1]; output [show(x),"\n"]; diff --git a/test/optimize/minizinc-fixtures/mzn-unsat.mzn b/test/optimize/minizinc-fixtures/mzn-unsat.mzn new file mode 100644 index 0000000000..e807fe4bfc --- /dev/null +++ b/test/optimize/minizinc-fixtures/mzn-unsat.mzn @@ -0,0 +1 @@ +include "all_different.mzn"; array[1..3] of var 1..2:x; constraint all_different(x); solve satisfy; output [show(x),"\n"]; diff --git a/test/optimize/minizinc_configure.py b/test/optimize/minizinc_configure.py new file mode 100644 index 0000000000..e3623eb039 --- /dev/null +++ b/test/optimize/minizinc_configure.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Pure CMake -P JSON encoder tests; no compiler, solver, or downloads.""" +import argparse +import json +from pathlib import Path +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[2] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cmake", required=True) + args = parser.parse_args() + script = ROOT / "tools/flatzinc/configure-optimize-msc.cmake" + template = ROOT / "tools/flatzinc/gecode-optimize.msc.in" + with tempfile.TemporaryDirectory(prefix="gecode-msc-escape-") as tmp: + output = Path(tmp)/"directory with spaces"/"solver.msc" + cases = [ + ("6.4.0", "../../../bin/fzn-gecode-optimize", "../gecode-optimize-experimental"), + ('version"quote', 'C:\\solver path\\a"b;ç/driver.exe', '../library space/雪;z'), + ("v"+"".join(map(chr,range(1,32))), 'drive\\name\nline\rreturn\ttab', 'lib\bback\fform'), + ('@GECODE_OPTIMIZE_MSC_EXECUTABLE@', '@GECODE_VERSION@/binary', '@GECODE_OPTIMIZE_MSC_MZNLIB@'), + ] + for version,driver,library in cases: + command = [args.cmake, "-DTEMPLATE="+str(template), "-DVERSION="+version, + "-DDRIVER="+driver, "-DMZNLIB="+library, "-DOUTPUT="+str(output), "-P", str(script)] + subprocess.run(command,check=True,capture_output=True,timeout=10) + encoded = output.read_bytes() + value = json.loads(encoded) + assert value["version"] == version + assert value["executable"] == [driver,"--minizinc"] + assert value["mznlib"] == library + # Generated build/install registrations retain the public controls; + # configuring paths must not erase their types, defaults or help. + expected_flags = json.loads(template.read_text())["extraFlags"] + assert value["extraFlags"] == expected_flags + assert len({flag[0] for flag in value["extraFlags"]}) == len(expected_flags) == 21 + assert value["stdFlags"] == ["-t"] + assert not list(output.parent.glob(output.name+".*.tmp")) + prior = output.read_bytes() + for omitted in ("VERSION","DRIVER","MZNLIB","OUTPUT","TEMPLATE"): + inputs = {"VERSION":"1","DRIVER":"driver","MZNLIB":"lib","OUTPUT":str(output),"TEMPLATE":str(template)} + del inputs[omitted] + run = subprocess.run([args.cmake,*[f"-D{k}={v}" for k,v in inputs.items()],"-P",str(script)], + capture_output=True,timeout=10) + assert run.returncode != 0 and output.read_bytes() == prior + run = subprocess.run([args.cmake,"-DTEMPLATE="+str(Path(tmp)/"absent.in"),"-DVERSION=1", + "-DDRIVER=driver","-DMZNLIB=lib","-DOUTPUT="+str(output),"-P",str(script)], + capture_output=True,timeout=10) + assert run.returncode != 0 and output.read_bytes() == prior + print("MiniZinc JSON encoder: 10 cases passed") + + +if __name__ == "__main__": + main() diff --git a/test/optimize/minizinc_registration.py b/test/optimize/minizinc_registration.py new file mode 100644 index 0000000000..7d6d52549b --- /dev/null +++ b/test/optimize/minizinc_registration.py @@ -0,0 +1,469 @@ +#!/usr/bin/env python3 +"""Pinned MiniZinc-to-driver compatibility gate; no download, build, or benchmarks. + +Requires an actual MiniZinc 2.10.1 compiler with its matching standard library and +an actual native-enabled driver. Each required child and the suite have deadlines. +""" +from __future__ import annotations +import argparse +import hashlib +import itertools +import json +import math +import os +from pathlib import Path +import re +import signal +import subprocess +import sys +import tempfile +import time + +ROOT = Path(__file__).resolve().parents[2] +IDENTITY = "org.gecode.optimize.experimental" +# Exact complete-predicate admission, deliberately independent of registry aliases. +PRIMITIVES = set("int_eq int_le int_lt int_ge int_gt int_plus int_minus int_lin_eq int_lin_le bool_eq bool_le bool_not bool_and bool_or array_bool_and array_bool_or bool_clause bool_lin_eq bool_lin_le bool2int int_in int_le_reif int_le_imp int_eq_imp int_lin_le_reif int_lin_le_imp array_int_element array_var_int_element all_different_int gecode_table_int gecode_regular gecode_circuit gecode_cumulatives cumulatives".split()) + + +def require(condition, message): + if not condition: + raise AssertionError(message) + + +def digest(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +class Suite: + def __init__(self, args, directory): + self.args, self.directory = args, directory + self.deadline = time.monotonic() + args.timeout + self.checks, self.sources, self.native_results = [], {}, [] + self.msc = args.registration or directory / "gecode-optimize.msc" + if args.registration is None: + config = json.loads((ROOT / "tools/flatzinc/gecode-optimize.msc.in").read_text()) + config["version"] = "6.4.0-experimental-test" + config["mznlib"] = str((ROOT / "tools/flatzinc/mznlib-optimize").resolve()) + config["executable"] = [str(args.binary), "--minizinc"] + self.msc.write_text(json.dumps(config, indent=2) + "\n") + # Read the actual artifact in place, preserving all relative paths. + self.configuration_bytes = self.msc.read_bytes() + config = json.loads(self.configuration_bytes) + require(config.get("id") == IDENTITY, "Wrong experimental solver identity") + executable = config.get("executable") + require(isinstance(executable,list) and len(executable) == 2 and + isinstance(executable[0],str) and executable[1] == "--minizinc", + "Registration must invoke exactly the driver and --minizinc") + def relative_path(value): + require(isinstance(value,str) and value, "Registration path must be a nonempty string") + path = Path(value) + return (path if path.is_absolute() else self.msc.parent/path).resolve() + executable_path = relative_path(executable[0]) + require(executable_path.is_file() and executable_path.samefile(args.binary), + "Registration executable does not resolve to --binary") + require(isinstance(config.get("mznlib"),str) and not config["mznlib"].startswith("-G"), + "Registration must name its dedicated library directory, not a -G alias") + self.library = relative_path(config["mznlib"]) + require(self.library.is_dir(), "Registration library directory does not exist") + self.library_hashes = self.hash_library() + require(self.library_hashes, "Registration library contains no .mzn files") + # Only this process's environment changes; no system/user solver prefs. + self.old_solver_path = os.environ.get("MZN_SOLVER_PATH") + os.environ["MZN_SOLVER_PATH"] = str(self.msc.parent) + + def hash_library(self): + return {str(path.relative_to(self.library)).replace(os.sep,"/"):digest(path) + for path in sorted(self.library.rglob("*.mzn"))} + + def close(self): + if self.old_solver_path is None: + os.environ.pop("MZN_SOLVER_PATH", None) + else: + os.environ["MZN_SOLVER_PATH"] = self.old_solver_path + + def run(self, label, command, *, cwd=None): + remaining = self.deadline - time.monotonic() + require(remaining > 1, f"Aggregate deadline before {label}") + start = time.monotonic() + with tempfile.TemporaryFile() as out, tempfile.TemporaryFile() as err: + process = None + try: + if os.name == "nt": + from process_containment import WindowsJobProcess + process = WindowsJobProcess() + process.start([str(x) for x in command], out, err, str(cwd or self.directory)) + else: + process = subprocess.Popen([str(x) for x in command], cwd=cwd or self.directory, + stdin=subprocess.DEVNULL, stdout=out, stderr=err, + start_new_session=True) + code = process.wait(timeout=min(10, remaining - 1)) + finally: + if process is not None: + if os.name == "nt": + process.cleanup(timeout=1) + else: + # MiniZinc starts the driver; terminate the whole group, + # even if MiniZinc exited while leaving a child behind. + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + process.wait(timeout=1) + require(out.tell() <= 1048576 and err.tell() <= 1048576, f"Excessive output: {label}") + out.seek(0); err.seek(0) + stdout, stderr = out.read().decode(), err.read().decode() + require(time.monotonic() < self.deadline, f"Aggregate deadline after {label}") + self.checks.append({"case": label, "returncode": code, "elapsed_seconds": time.monotonic()-start}) + return code, stdout, stderr + + def mzn(self, label, *options): + return self.run(label, [self.args.minizinc, "--solver", self.msc, *options]) + + def fixture(self, name): + path = ROOT / "test/optimize/minizinc-fixtures" / ("mzn-"+name+".mzn") + require(path.is_file(), f"Missing fixture: {path}") + self.sources[path.name] = digest(path) + return path + + def positive(self, name, allowed, *, mode="optimal", required=None): + source = self.fixture(name) + fzn = self.directory / (name+".fzn") + code, stdout, stderr = self.mzn("compile-"+name, "--compile", "--output-fzn-to-file", fzn, + "--output-ozn-to-file", self.directory/(name+".ozn"), source) + require(code == 0 and not stderr, f"Compile {name}: {code} {stdout!r} {stderr!r}") + text = fzn.read_text() + emitted = set(re.findall(r"\bconstraint\s+(\w+)\s*\(", text)) + require(emitted <= PRIMITIVES, f"Unadmitted predicate lowering in {name}: {emitted-PRIMITIVES}") + if required: + require(required in emitted, f"Missing native lowering {required} in {name}") + code, stdout, stderr = self.mzn("solve-"+name, source) + require(code == 0 and not stderr, f"Solve {name}: {code} {stdout!r} {stderr!r}") + lines = [line for line in stdout.splitlines() if line and not line.startswith("%")] + if mode == "unsat": + require(lines == ["=====UNSATISFIABLE====="], f"False completion in {name}: {lines}") + else: + markers = ["----------", "=========="] if mode == "optimal" else ["----------"] + require(lines[1:] == markers, f"Wrong markers in {name}: {lines}") + require(tuple(json.loads(lines[0])) in allowed, f"Original-model oracle failed in {name}: {lines[0]}") + require("% guarantee: exact integer search" in stdout, f"Wrong backend provenance: {name}") + + def rejected(self, name, contains): + code, stdout, stderr = self.mzn("reject-"+name, self.fixture(name)) + require(code != 0 and contains in stderr, f"Missing rejection {name}: {code} {stdout!r} {stderr!r}") + require("----------" not in stdout and "=====UNSATISFIABLE=====" not in stdout, + f"Rejected model published a witness/proof: {name}") + + +def regular(word, states, alphabet, transitions, initial, finals): + for symbol in word: + if not 1 <= symbol <= alphabet or not 1 <= initial <= states: + return False + initial = transitions[(initial-1)*alphabet+symbol-1] + if initial == 0: + return False + return initial in finals + + +def circuit(values, offset): + visited, node = set(), offset + for _ in values: + if node in visited or not offset <= node < offset+len(values): + return False + visited.add(node) + node = values[node-offset] + return node == offset and len(visited) == len(values) + + +def cumulative(starts, durations, heights, capacity): + # Independent half-open integer-time oracle for these tiny source models. + return all(sum(h for s,d,h in zip(starts,durations,heights) if s <= t < s+d) <= capacity + for t in range(-2, 6)) + + +def tests(s): + code, stdout, stderr = s.run("compiler-version", [s.args.minizinc, "--version"]) + require(code == 0 and "version 2.10.1," in stdout and not stderr, "Required pinned MiniZinc 2.10.1 unavailable") + code, stdout, stderr = s.run("solver-discovery", [s.args.minizinc, "--solvers-json"]) + require(code == 0 and not stderr, "Solver discovery failed") + # Another installed build may advertise the same solver identity. Validate + # discovery of this exact artifact, which every solve selects explicitly. + configs = [x for x in json.loads(stdout) if x["id"] == IDENTITY and + Path(x["extraInfo"]["configFile"]).resolve() == s.msc.resolve()] + require(len(configs) == 1, "Configured solver artifact absent or duplicated") + require(configs[0]["stdFlags"] == ["-t"] and configs[0]["tags"] == ["cp","int","experimental"], + "Registration overstates supported flags/types") + require("default" not in configs[0]["tags"], "Experimental registration changed the default") + advertised = {flag[0]: flag for flag in configs[0]["extraFlags"]} + expected = set("mode auto-presolve auto-components auto-symmetry auto-knapsack race-seconds race-nodes lp root-cuts bound-tightening lp-interval search branching branching-probes max-open-nodes neighborhood neighborhood-radius neighborhood-nodes neighborhood-seconds node-limit diagnostics".split()) + require(set(advertised) == {"--native-"+name for name in expected}, "Missing or unexpected native controls") + require(advertised["--native-mode"][2:] == ["opt:auto:race:plain:configured", "auto"], "Wrong mode contract") + require("CPU" in advertised["--native-race-seconds"][1] and "solve time" in advertised["--native-race-seconds"][1], + "Registration must disclose race overhead") + code, stdout, stderr = s.run("solver-native-help", [s.args.minizinc, "--help", s.msc]) + require(code == 0 and not stderr and all(flag in stdout for flag in advertised), "Native controls absent from solver help") + a = [(x,y,2*x+y-4) for x,y in itertools.product(range(4),repeat=2) if x+y >= 3] + s.positive("linear-min", {min(a,key=lambda p:p[2])}) + a = [(x,y,3*x-y+2) for x,y in itertools.product(range(-2,3),repeat=2) if x+y <= 1] + s.positive("linear-max", {max(a,key=lambda p:p[2])}) + s.positive("satisfy", {(1,),(2,)}, mode="satisfy") + s.positive("unsat", set(), mode="unsat", required="all_different_int") + s.positive("alias-holes", {(3,3)}) + s.positive("all-different", {(1,2)}, required="all_different_int") + s.positive("table", {(1,2)}, required="gecode_table_int") + s.positive("table-alias", {(1,1)}, required="gecode_table_int") + words = [x for x in itertools.product((1,2),repeat=2) if regular(x,3,2,[2,3,0,3,2,0],1,{3})] + s.positive("regular", {min(words,key=lambda x:x[0])}, required="gecode_regular") + words = [(x,x,x) for x in (1,2) if regular((x,x,x),2,2,[2,1,1,2],1,{1})] + s.positive("regular-alias", set(words), required="gecode_regular") + s.positive("regular-empty", {()}, mode="satisfy") + s.positive("regular-dead", set(), mode="unsat", required="gecode_regular") + for name,offset in (("negative",-2),("offset",2)): + values = [x for x in itertools.product(range(offset,offset+3),repeat=3) if circuit(x,offset)] + s.positive("circuit-"+name, {min(values,key=lambda x:x[0])}, required="gecode_circuit") + s.positive("element-offset", {(0,2)}, required="array_int_element") + starts = [(x,1) for x in range(3) if cumulative([x,1,1],[1,1,1],[1,1,1],2)] + s.positive("cumulative-half-open", {min(starts)}, required="gecode_cumulatives") + s.positive("cumulative-zero", {(0,)}, required="gecode_cumulatives") + s.positive("cumulative-unsat", set(), mode="unsat", required="gecode_cumulatives") + s.positive("cumulative-fixed-alias", {(0,1,1)}, required="gecode_cumulatives") + s.positive("reified-le", {(1,1)}) + accepted = {x for x in itertools.product((1,2),repeat=2) if regular(x,1,2,[1,0],1,{1})} + s.positive("regular-decomposed-reif", accepted, required="gecode_regular") + rejected = [x for x in itertools.product((1,2),repeat=2) if not regular(x,1,2,[1,0],1,{1})] + s.positive("regular-complement", {min(rejected,key=lambda x:2*x[0]+x[1])}, required="gecode_regular") + for name,text in (("times","multiplication is unsupported"),("reif-eq","equality is unsupported"), + ("float","float and set variables"),("set","float and set variables"),("search","search annotations"), + ("variable-cumulative","original singleton")): + s.rejected("reject-"+name,text) + native_controls(s) + # Test flags through the actual MiniZinc parser, not only the .msc JSON. + for flag in ("--all-solutions","--intermediate-solutions"): + args = [flag] + code, stdout, stderr = s.mzn("unadvertised-"+flag, *args, s.fixture("satisfy")) + require(code != 0 and "Unrecognized option" in stderr, f"Unsupported standard flag accepted: {flag}") + # MiniZinc owns compiler/output statistics and may consume --parallel even + # when neither solver flag is advertised. Force forwarding to test the + # driver's strict boundary without misrepresenting the outer compiler. + for flag in ("-s", "-p"): + code, stdout, stderr = s.mzn("forwarded-"+flag,"--fzn-flag",flag,s.fixture("satisfy")) + require(code != 0 and "Unsupported MiniZinc protocol option" in stderr, + f"Unsupported forwarded solver flag accepted: {flag}") + for milliseconds in ("0","1000"): + code, stdout, stderr = s.mzn("time-"+milliseconds,"--solver-time-limit",milliseconds,s.fixture("satisfy")) + require(code == 0 and "----------" in stdout and not stderr, "MiniZinc time convention mismatch") + # Filename ordering and zero semantics are isolated from direct CLI behavior. + fzn = s.directory/"protocol.fzn" + fzn.write_text("var 0..2: x :: output_var; solve minimize x;\n") + for args in (("-t","0",str(fzn)),(str(fzn),"-t","0"),("--",str(fzn))): + code, stdout, stderr = s.run("protocol-order",[s.args.binary,"--minizinc",*args]) + require(code == 0 and "x = 0;" in stdout and "==========" in stdout and not stderr, "Protocol option order/zero failed") + for args in (("-t",),("-t","-1",str(fzn)),("-t","1.5",str(fzn)),("-t","18446744073709551616",str(fzn)), + ("-t","0","-t","1",str(fzn)),(str(fzn),str(fzn)),("--backend","highs",str(fzn)),("-a",str(fzn))): + code, stdout, stderr = s.run("protocol-rejection",[s.args.binary,"--minizinc",*args]) + require(code == 2 and not stdout and stderr, f"Malformed protocol accepted: {args}") + code, stdout, stderr = s.run("direct-zero",[s.args.binary,fzn,"--time-limit","0"]) + require(code == 1 and "=====UNKNOWN=====" in stdout, "Direct zero deadline changed") + # A finite parser workload (no CP benchmark) makes a 1ms budget expire in + # capture on supported CI machines, verifying UNKNOWN is normal exit0. + large = s.directory/"capture-deadline.fzn" + large.write_text("".join(f"var 0..1: x{i};\n" for i in range(20000))+"solve satisfy;\n") + for prefix in ([s.args.binary,"--minizinc","-t","1"], + [s.args.minizinc,"--solver",s.msc,"--solver-time-limit","1"], + [s.args.minizinc,"--solver",s.msc,"--native-mode","race","--native-race-seconds","5","--solver-time-limit","1"], + [s.args.minizinc,"--solver",s.msc,"--native-mode","configured","--native-branching","reliability","--native-neighborhood","hamming","--solver-time-limit","1"]): + code, stdout, stderr = s.run("actual-timeout",[*prefix,large]) + require(code == 0 and "=====UNKNOWN=====" in stdout and "=====ERROR=====" not in stdout, + f"Timeout became solver error: {code} {stdout!r} {stderr!r}") + + +def native_controls(s): + # Independent original-model oracle, including all optimal ties. No native + # result or flattened model is used to calculate this answer. + weights, profits = (3,3,2,2,4,1), (5,4,3,3,6,1) + feasible = {x+(sum(p*v for p,v in zip(profits,x)),) for x in itertools.product((0,1), repeat=6) + if sum(w*v for w,v in zip(weights,x)) <= 8 and 3*x[0]+3*x[1] <= 5 and sum(x[2:5]) <= 2} + optimum = max(x[-1] for x in feasible) + optimal = {x for x in feasible if x[-1] == optimum} + source = s.fixture("native-controls") + s.positive("native-controls", optimal, required="bool2int") + + def invoke(label, mode="auto", *flags, source=source): + return s.mzn("native-"+label, "--native-mode", mode, "--native-diagnostics", "on", *flags, source) + + def completed(label, mode="auto", *flags, source=source, allowed=optimal, configuration=(), backend=None, policy=None): + code, stdout, stderr = invoke(label, mode, *flags, source=source) + require(code == 0 and not stderr, f"Native {label}: {code} {stdout!r} {stderr!r}") + lines = [line for line in stdout.splitlines() if line and not line.startswith("%")] + require(len(lines) == 3 and lines[1:] == ["----------", "=========="], f"Native markers {label}: {lines}") + require(tuple(json.loads(lines[0])) in allowed, f"Original oracle failed for {label}: {lines[0]}") + require("% guarantee: exact integer search" in stdout and f"% native-mode: {mode}" in stdout, + f"Native route provenance absent for {label}: {stdout}") + config = next((line for line in stdout.splitlines() if line.startswith("% native-configuration: ")), "") + require(config and all(item in config.split() for item in configuration), f"Settings not forwarded for {label}: {config}") + if backend: + require(f"% native-backend: {backend}" in stdout, f"Wrong actual backend for {label}: {stdout}") + if policy: + require(any(policy in line for line in stdout.splitlines() if line.startswith("% native-policy: ")), + f"Wrong actual policy for {label}: {stdout}") + work = next((line for line in stdout.splitlines() if line.startswith("% native-work: ")), "") + counters = {key:int(value) for key,value in re.findall(r"([a-z-]+)=(\d+)", work)} + s.native_results.append({"case": label, "mode": mode, "solution": json.loads(lines[0]), + "configuration": config[len("% native-configuration: "):], "work": counters, + "policy": next(line[len("% native-policy: "):] for line in stdout.splitlines() if line.startswith("% native-policy: ")), + "backend": next(line[len("% native-backend: "):] for line in stdout.splitlines() if line.startswith("% native-backend: "))}) + return counters, stdout + + completed("auto") + knapsack_points = {x+(sum(p*v for p,v in zip(profits,x)),) for x in itertools.product((0,1), repeat=6) + if sum(w*v for w,v in zip(weights,x)) <= 8} + knapsack_best = max(x[-1] for x in knapsack_points) + knapsack_optimal = {x for x in knapsack_points if x[-1] == knapsack_best} + knapsack = s.fixture("native-knapsack") + completed("knapsack-auto", source=knapsack, allowed=knapsack_optimal, policy="eligible exact knapsack DP") + _, stdout = completed("knapsack-disabled", "auto", "--native-auto-knapsack", "off", source=knapsack, + allowed=knapsack_optimal, configuration=("auto-knapsack=off",)) + require("eligible exact knapsack DP" not in stdout, "Disabled knapsack DP was still selected") + for name in ("presolve", "components", "symmetry", "knapsack"): + completed("auto-no-"+name, "auto", "--native-auto-"+name, "off", configuration=("auto-"+name+"=off",)) + disabled = [item for name in ("presolve", "components", "symmetry", "knapsack") + for item in ("--native-auto-"+name, "off")] + completed("auto-all-disabled", "auto", *disabled, configuration=tuple("auto-"+name+"=off" for name in ("presolve", "components", "symmetry", "knapsack"))) + completed("plain", "plain", backend="Gecode native", configuration=("ordinary-native",)) + completed("uint64-limit", "plain", "--native-node-limit", "18446744073709551615", configuration=("node-limit=18446744073709551615",)) + completed("race", "race", "--native-race-seconds", "0.05", "--native-race-nodes", "4", + configuration=("race-seconds=0.05", "race-nodes=4"), policy="Native sequential race:") + completed("race-zero", "race", "--native-race-seconds", "0", configuration=("race-seconds=0",), policy="skipped") + completed("race-global", "race", source=s.fixture("all-different"), allowed={(1,2)}, policy="skipped") + for order in ("bab", "dfs", "best-bound"): + completed("search-"+order, "configured", "--native-search", order, + configuration=("search="+order,), backend="Gecode native" if order == "bab" else "Gecode native frontier") + work, _ = completed("reliability", "configured", "--native-branching", "reliability", + "--native-branching-probes", "32", "--native-max-open-nodes", "128", + configuration=("branching=reliability", "branching-probes=32", "max-open-nodes=128")) + require(0 < work.get("branching-probes",0) <= 32, "Reliability accepted without executing its probes") + work, _ = completed("reliability-zero", "configured", "--native-branching", "reliability", "--native-branching-probes", "0") + require(work.get("branching-probes") == 0, "Zero reliability probe cap ignored") + work, _ = completed("hamming", "configured", "--native-neighborhood", "hamming", "--native-neighborhood-radius", "1", + "--native-neighborhood-nodes", "32", "--native-neighborhood-seconds", "0.1", + configuration=("neighborhood=hamming", "neighborhood-radius=1", "neighborhood-nodes=32", "neighborhood-seconds=0.1")) + require(work.get("neighborhood-attempts",0) > 0, "Hamming accepted without executing an attempt") + for cap in ("nodes", "seconds"): + work, _ = completed("hamming-zero-"+cap, "configured", "--native-neighborhood", "hamming", "--native-neighborhood-"+cap, "0") + require(work.get("neighborhood-attempts") == 0, "Zero neighborhood cap ignored") + completed("combined-search", "configured", "--native-search", "best-bound", "--native-branching", "reliability", "--native-neighborhood", "hamming") + + # This gate also runs in native-only builds. Discover unavailable checked LP + # through its explicit rejection, never through a successful silent fallback. + code, stdout, stderr = invoke("lp-capability", "configured", "--native-search", "bab", "--native-lp", "root") + checked_lp = code == 0 + if not checked_lp: + require(code != 0 and "checked LP requires HiGHS" in stderr and "----------" not in stdout, + f"Unexpected checked LP failure: {code} {stdout!r} {stderr!r}") + lp_cases = [ + ("lp-root", ("--native-search", "bab", "--native-lp", "root")), + ("lp-root-covers", ("--native-lp", "root", "--native-root-cuts", "on")), + ("lp-updated", ("--native-search", "best-bound", "--native-lp", "updated", "--native-lp-interval", "2", "--native-bound-tightening", "off")), + ("lp-combined", ("--native-lp", "updated", "--native-root-cuts", "on", "--native-branching", "reliability", "--native-neighborhood", "hamming")), + ] + for label, flags in lp_cases: + if checked_lp: + work, stdout = completed(label, "configured", *flags, + configuration=tuple(flags[i][len("--native-"):]+"="+flags[i+1] for i in range(0,len(flags),2))) + if label in ("lp-root-covers", "lp-combined"): + require(work.get("root-cuts",0) > 0, f"Root covers enabled without generating a verified cut: {label}") + require(work.get("lp-calls",0) > 0 and work.get("checked-bounds",0) > 0, + f"Checked LP accepted without checked deductions: {label} {stdout}") + else: + code, stdout, stderr = invoke(label, "configured", *flags) + require(code != 0 and "checked LP requires HiGHS" in stderr and "----------" not in stdout, + f"Explicit LP silently fell back: {label}") + + # Shared finite budgets must never publish a false witness or completion. + # Both the optional algorithms and the ordinary frontier consume this cap. + for mode in ("auto", "race", "plain", "configured"): + for cap in ("0", "1"): + flags = ["--native-node-limit", cap] + if mode == "configured": + flags += ["--native-branching", "reliability", "--native-neighborhood", "hamming"] + code, stdout, stderr = invoke("node-"+mode+"-"+cap, mode, *flags) + require(code == 0 and "=====ERROR=====" not in stdout and "=====UNSATISFIABLE=====" not in stdout, + f"Node cap became error/false infeasibility: {mode} {cap} {stdout!r} {stderr!r}") + lines = [line for line in stdout.splitlines() if line and not line.startswith("%")] + if lines and lines[0].startswith("["): + witness = tuple(json.loads(lines[0])) + require(witness in feasible, "Limited run published invalid original-model witness") + require("==========" not in lines or witness in optimal, "Limited run claimed false optimum") + else: + require(lines == ["=====UNKNOWN====="], f"Limited run protocol: {lines}") + if mode == "configured": + work_line = next((line for line in stdout.splitlines() if line.startswith("% native-work: ")), "") + work = {key:int(value) for key,value in re.findall(r"([a-z-]+)=(\d+)", work_line)} + require("budget-nodes" in work and work["budget-nodes"] <= int(cap), f"Shared node cap exceeded: {work}") + require(work["budget-nodes"] == work.get("frontier-admitted",0)+work.get("branching-probes",0)+work.get("neighborhood-status-attempts",0), + f"Frontier/probe/neighborhood accounting mismatch: {work}") + if cap == "0": + require("==========" not in lines, "Zero node budget incorrectly completed nontrivial model") + code, stdout, stderr = invoke("zero-open-spaces", "configured", "--native-max-open-nodes", "0") + require(code == 0 and "=====UNKNOWN=====" in stdout and "----------" not in stdout and "==========" not in stdout, + "Zero frontier space cap ignored or became a solver error") + + # Rejected controls must not be ignored by MiniZinc or the driver. Test both + # malformed values and meaningful but incompatible combinations. + rejected = [ + ("--native-mode", "bad"), ("--native-diagnostics", "yes"), + ("--native-node-limit", "-1"), ("--native-node-limit", "18446744073709551616"), + ("--native-mode", "race", "--native-race-seconds", "nan"), + ("--native-mode", "race", "--native-race-nodes", "0"), + ("--native-mode", "plain", "--native-auto-presolve", "off"), + ("--native-race-seconds", "0.1"), ("--native-lp", "root"), + ("--native-mode", "configured", "--native-root-cuts", "on"), + ("--native-mode", "configured", "--native-bound-tightening", "off"), + ("--native-mode", "configured", "--native-lp", "root", "--native-lp-interval", "2"), + ("--native-mode", "configured", "--native-lp", "updated", "--native-lp-interval", "0"), + ("--native-mode", "configured", "--native-search", "bab", "--native-branching", "reliability"), + ("--native-mode", "configured", "--native-search", "bab", "--native-max-open-nodes", "1"), + ("--native-mode", "configured", "--native-search", "bab", "--native-neighborhood", "hamming"), + ("--native-mode", "configured", "--native-branching-probes", "0"), + ("--native-mode", "configured", "--native-neighborhood-radius", "1"), + ("--native-mode", "configured", "--native-neighborhood-nodes", "0"), + ("--native-mode", "configured", "--native-neighborhood-seconds", "0"), + ("--native-mode", "configured", "--native-neighborhood", "hamming", "--native-neighborhood-seconds", "-1"), + ("--native-mode", "configured", "--native-search", "bfs"), + ] + for i, flags in enumerate(rejected): + code, stdout, stderr = s.mzn("native-reject-"+str(i), *flags, source) + require(code != 0 and stderr and "----------" not in stdout and "==========" not in stdout and "=====UNSATISFIABLE=====" not in stdout, + f"Invalid controls accepted or emitted proof: {flags} {code} {stdout!r} {stderr!r}") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--minizinc", type=Path, required=True) + parser.add_argument("--binary", type=Path, required=True) + parser.add_argument("--registration", type=Path, help="Test this configured .msc in place without rewriting it") + parser.add_argument("--timeout", type=float, default=120) + args = parser.parse_args() + require(math.isfinite(args.timeout) and args.timeout > 0, "Finite positive suite timeout required") + args.minizinc, args.binary = args.minizinc.resolve(), args.binary.resolve() + args.registration = args.registration.resolve() if args.registration is not None else None + require(args.minizinc.is_file() and args.binary.is_file(), "Required executable is missing") + with tempfile.TemporaryDirectory(prefix="gecode-minizinc-") as tmp: + suite = Suite(args,Path(tmp)) + try: + tests(suite) + require(suite.msc.read_bytes() == suite.configuration_bytes, "Registration artifact changed during the test") + require(suite.hash_library() == suite.library_hashes, "Registration library changed during the test") + report = {"status":"passed","compiler_version":"2.10.1","compiler_sha256":digest(args.minizinc), + "driver_sha256":digest(args.binary),"cases":len(suite.checks),"checks":suite.checks, + "configuration_sha256":hashlib.sha256(suite.configuration_bytes).hexdigest(), + "configuration_provided":args.registration is not None, + "library_files_sha256":suite.library_hashes, + "library_sha256":hashlib.sha256(json.dumps(suite.library_hashes,sort_keys=True,separators=(",",":")).encode()).hexdigest(), + "source_sha256":suite.sources,"native_controls":suite.native_results} + print(json.dumps(report,sort_keys=True)) + finally: + suite.close() + + +if __name__ == "__main__": + main() diff --git a/test/optimize/model.cpp b/test/optimize/model.cpp new file mode 100644 index 0000000000..c30ec1dc07 --- /dev/null +++ b/test/optimize/model.cpp @@ -0,0 +1,250 @@ +/* Standalone tests: compile with model.cpp; no native Gecode dependencies. */ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +#include +#include + +using namespace Gecode::Optimize; + +namespace { +constexpr double infinity = std::numeric_limits::infinity(); + +template +void rejects(Action action) { + bool caught = false; + try { + action(); + } catch (const ModelError&) { + caught = true; + } + assert(caught); +} + +void defaults_and_ownership() { + static_assert(!std::is_copy_constructible::value, "No implicit model copy"); + static_assert(!std::is_copy_assignable::value, "No implicit model assignment"); + static_assert(std::is_nothrow_move_constructible::value, "Move preserves handles"); + Model a, b; + assert(a.id() != 0 && b.id() != 0 && a.id() != b.id()); + auto initial = a.snapshot(); + assert(initial.revision == 0 && initial.variables.empty() && initial.rows.empty()); + assert(initial.objective.terms.empty() && initial.objective.offset == 0.0); + assert(initial.objective.sense == ObjectiveSense::Minimize); + Variable x = a.add_binary("x"); + Variable y = b.add_binary("y"); + Constraint row = a.add_row({{x, 1.0}}, 0.0, 1.0, "row"); + Constraint foreign = b.add_row({{y, 1.0}}, 0.0, 1.0); + const auto revision = a.revision(); + rejects([&] { a.add_row({{y, 1.0}}, 0.0, 1.0); }); + rejects([&] { a.minimize({{y, 0.0}}); }); + rejects([&] { a.set_bounds(y, 0.0, 1.0); }); + rejects([&] { a.set_bounds(foreign, 0.0, 1.0); }); + rejects([&] { a.set_coefficient(row, y, 1.0); }); + rejects([&] { a.set_coefficient(foreign, x, 1.0); }); + rejects([&] { a.remove(y); }); + rejects([&] { a.remove(foreign); }); + rejects([&] { a.variable(Variable{}); }); + rejects([&] { a.row(Constraint{a.id(), 1000}); }); + assert(a.revision() == revision); +} + +void sparse_terms_and_objective() { + Model model; + Variable x = model.add_integer(-2, 2, "x"); + Variable y = model.add_continuous(-infinity, infinity, "y"); + const std::vector terms{{y, 3}, {x, 4}, {y, -1}, {x, -2}, + {x, 0}, {y, -2}, {x, -1}}; + Constraint row = model.add_row(terms, -3, 4, "range"); + const auto normalized = model.row(row).terms; + assert(normalized.size() == 1 && normalized[0].variable == x); + assert(normalized[0].coefficient == 1.0); + // Check preservation on a small independent domain, including cancellations. + for (int xv = -2; xv <= 2; ++xv) + for (int yv = -2; yv <= 2; ++yv) { + double original = 0, canonical = 0; + for (const auto& term : terms) + original += term.coefficient * (term.variable == x ? xv : yv); + for (const auto& term : normalized) + canonical += term.coefficient * (term.variable == x ? xv : yv); + assert(original == canonical); + } + model.maximize({{y, 2}, {x, 3}, {x, -1}}, -7.5); + auto snapshot = model.snapshot(); + assert(snapshot.objective.sense == ObjectiveSense::Maximize); + assert(snapshot.objective.offset == -7.5); + assert(snapshot.objective.terms.size() == 2); + assert(snapshot.objective.terms[0].variable == x); + assert(snapshot.objective.terms[0].coefficient == 2); + assert(snapshot.objective.terms[1].variable == y); + const Revision before = model.revision(); + model.minimize({}, 3.0); + assert(model.revision() == before + 1); + assert(model.snapshot().objective.terms.empty()); + assert(model.snapshot().objective.sense == ObjectiveSense::Minimize); + assert(snapshot.objective.terms.size() == 2); // The old snapshot owns its data. + + // Cancellation should not depend on the caller's duplicate insertion order. + std::vector values{-1e16, 1.0, 1e16}; + do { + model.minimize({{x, values[0]}, {x, values[1]}, {x, values[2]}}); + const auto objective = model.snapshot().objective; + assert(objective.terms.size() == 1 && objective.terms[0].coefficient == 1.0); + } while (std::next_permutation(values.begin(), values.end())); +} + +void edits_and_tombstones() { + Model model; + Variable x = model.add_binary("x"), y = model.add_integer(-4, 7, "y"); + Constraint first = model.add_row({{x, 2}}, 0, 2, "first"); + Constraint second = model.add_row({{y, 1}}, -4, 7, "second"); + model.minimize({{y, 3}}, 1); + auto old = model.snapshot(); + auto revision = model.revision(); + model.set_coefficient(first, y, 4); + assert(model.revision() == ++revision); + assert(model.row(first).terms.size() == 2); + model.set_coefficient(first, x, -3); + assert(model.revision() == ++revision); + assert(model.row(first).terms[0].coefficient == -3); + model.set_bounds(x, 1, 1); + assert(model.revision() == ++revision && model.variable(x).lower == 1); + model.set_bounds(first, -2, 3); + assert(model.revision() == ++revision && model.row(first).upper == 3); + model.set_name(x, "renamed"); + assert(model.revision() == ++revision && model.variable(x).name == "renamed"); + model.set_name(first, "renamed row"); + assert(model.revision() == ++revision && model.row(first).name == "renamed row"); + model.set_objective_coefficient(x, 5); + assert(model.revision() == ++revision); + model.set_objective_coefficient(y, 0); + assert(model.revision() == ++revision); + model.set_objective_offset(-2); + assert(model.revision() == ++revision && model.snapshot().objective.offset == -2); + rejects([&] { model.remove(x); }); // Objective still refers to x. + rejects([&] { model.remove(y); }); // Active rows still refer to y. + assert(model.revision() == revision); + model.set_objective_coefficient(x, 0); + model.set_coefficient(first, x, 0); + model.remove(x); + assert(!model.snapshot().variables[0].active); + rejects([&] { model.variable(x); }); + rejects([&] { model.set_bounds(x, 0, 1); }); + rejects([&] { model.remove(x); }); + rejects([&] { model.add_row({{x, 0}}, 0, 1); }); + Variable z = model.add_binary("new"); + assert(z.id == 2 && z != x); + model.remove(first); + model.remove(second); + model.remove(y); + rejects([&] { model.row(first); }); + rejects([&] { model.remove(first); }); + Constraint third = model.add_row({{z, 1}}, 0, 1); + assert(third.id == 2); + const auto current = model.snapshot(); + assert(current.rows.size() == 3 && !current.rows[0].active && !current.rows[1].active); + assert(current.rows[0].constraint.model_id == model.id()); + assert(current.rows[0].constraint.id == 0 && current.rows[0].terms.empty()); + assert(old.variables[0].active && old.variables[0].lower == 0); + assert(old.variables[0].name == "x" && old.rows[0].active); + assert(old.rows[0].terms.size() == 1 && old.objective.offset == 1); +} + +void malformed_data_and_rollback() { + Model model; + const double nan = std::numeric_limits::quiet_NaN(); + const double huge = std::numeric_limits::max(); + Variable x = model.add_continuous(); + Constraint row = model.add_row({{x, 2}}, -infinity, 4, "valid"); + model.maximize({{x, 3}}, 7); + const auto revision = model.revision(); + rejects([&] { model.add_continuous(nan, 1); }); + rejects([&] { model.add_integer(2, 1); }); + rejects([&] { model.add_continuous(infinity, infinity); }); + rejects([&] { model.add_continuous(-infinity, -infinity); }); + rejects([&] { model.add_variable(static_cast(99), 0, 1); }); + rejects([&] { model.add_variable(VariableType::Binary, -1, 1); }); + rejects([&] { model.add_variable(VariableType::Binary, 0, 2); }); + rejects([&] { model.add_variable(VariableType::SemiContinuous, 0, 2); }); + rejects([&] { model.add_variable(VariableType::SemiInteger, -1, 2); }); + rejects([&] { model.add_row({{x, nan}}, 0, 1); }); + rejects([&] { model.add_row({{x, infinity}}, 0, 1); }); + rejects([&] { model.add_row({{x, -infinity}}, 0, 1); }); + rejects([&] { model.add_row({{x, huge}, {x, huge}}, 0, 1); }); + rejects([&] { model.add_row({}, nan, 1); }); + rejects([&] { model.set_bounds(row, 0, nan); }); + rejects([&] { model.set_bounds(x, 1, 0); }); + rejects([&] { model.set_coefficient(row, x, infinity); }); + rejects([&] { model.minimize({{x, huge}, {x, huge}}); }); + rejects([&] { model.minimize({}, nan); }); + rejects([&] { model.set_objective({}, static_cast(99)); }); + rejects([&] { model.set_objective_coefficient(x, nan); }); + rejects([&] { model.set_objective_offset(infinity); }); + const auto after = model.snapshot(); + assert(after.revision == revision && after.variables.size() == 1 && after.rows.size() == 1); + assert(after.rows[0].terms.size() == 1 && after.rows[0].terms[0].coefficient == 2); + assert(after.rows[0].upper == 4 && after.rows[0].lower == -infinity); + assert(after.objective.sense == ObjectiveSense::Maximize && after.objective.offset == 7); + assert(after.objective.terms.size() == 1 && after.objective.terms[0].coefficient == 3); + assert(model.add_continuous().id == 1); // Failed insertion did not consume a slot. +} + +void domain_and_empty_models() { + Model model; + auto unbounded = model.add_integer(-infinity, infinity); + assert(model.variable(unbounded).lower == -infinity); + // No integer lies here: it is a valid but infeasible model, not malformed data. + model.add_integer(0.2, 0.8); + auto semi = model.add_variable(VariableType::SemiContinuous, 2, infinity); + auto semi_integer = model.add_variable(VariableType::SemiInteger, 2.2, 5.8); + assert(model.variable(semi).lower == 2 && model.variable(semi_integer).upper == 5.8); + rejects([&] { model.set_bounds(semi, 0, 5); }); + Model empty; + empty.minimize({}, 42); + empty.add_row({}, 1, infinity); // Structurally valid contradiction 0 >= 1. + assert(empty.snapshot().variables.empty() && empty.snapshot().rows.size() == 1); + assert(empty.snapshot().objective.offset == 42); +} + +void move_identity() { + Model original; + Variable x = original.add_binary("x"); + Constraint row = original.add_row({{x, 1}}, 0, 1); + const auto id = original.id(); + const auto revision = original.revision(); + Model moved(std::move(original)); + assert(original.id() == 0 && original.revision() == 0); + rejects([&] { original.snapshot(); }); + rejects([&] { original.add_binary(); }); + assert(moved.id() == id && moved.revision() == revision); + assert(moved.variable(x).name == "x" && moved.row(row).terms.size() == 1); + Model assigned; + Variable obsolete = assigned.add_binary(); + const auto historical = assigned.snapshot(); + assigned = std::move(moved); + assert(assigned.id() == id && moved.id() == 0); + assert(assigned.variable(x).name == "x"); + rejects([&] { assigned.variable(obsolete); }); + assert(historical.variables[0].variable == obsolete); + Model& same = assigned; + assigned = std::move(same); + assert(assigned.id() == id && assigned.variable(x).active); + original = Model{}; + assert(original.id() != 0 && original.id() != id); + assert(original.add_binary().id == 0); +} +} // namespace + +int main() { + defaults_and_ownership(); + sparse_terms_and_objective(); + edits_and_tombstones(); + malformed_data_and_rollback(); + domain_and_empty_models(); + move_identity(); +} diff --git a/test/optimize/native.cpp b/test/optimize/native.cpp new file mode 100644 index 0000000000..80186ea769 --- /dev/null +++ b/test/optimize/native.cpp @@ -0,0 +1,273 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace Gecode::Optimize; +#ifdef GECODE_OPTIMIZE_WITH_NATIVE +namespace { +constexpr double inf = std::numeric_limits::infinity(); + +bool feasible(const ModelSnapshot& model, const std::vector& values) { + const auto row = [&](const std::vector& terms, double lower, double upper) { + long long activity = 0; + for (const auto& term : terms) + activity += static_cast(term.coefficient) * static_cast(values[term.variable.id]); + return activity >= lower && activity <= upper; + }; + for (const auto& constraint : model.rows) + if (constraint.active && !constraint.indicator_origin && + !row(constraint.terms, constraint.lower, constraint.upper)) return false; + for (const auto& indicator : model.indicators) { + if (!indicator.active) continue; + const bool enabled = values[indicator.activator.id] == (indicator.active_value ? 1 : 0); + if (enabled && !row(indicator.terms, indicator.lower, indicator.upper)) return false; + if (indicator.inactive_gate && values[indicator.inactive_gate->id] != (enabled ? 0 : 1)) return false; + } + return true; +} + +std::optional oracle(const ModelSnapshot& model) { + std::optional optimum; + std::vector values(model.variables.size()); + std::function visit = [&](std::size_t slot) { + if (slot == model.variables.size()) { + if (!feasible(model, values)) return; + long long objective = static_cast(model.objective.offset); + for (const auto& term : model.objective.terms) + objective += static_cast(term.coefficient) * static_cast(values[term.variable.id]); + if (!optimum || (model.objective.sense == ObjectiveSense::Minimize ? objective < *optimum : objective > *optimum)) + optimum = objective; + return; + } + const auto& variable = model.variables[slot]; + if (!variable.active) { visit(slot + 1); return; } + if (variable.type == VariableType::SemiInteger) { values[slot] = 0; visit(slot + 1); } + for (int value = static_cast(variable.lower); value <= variable.upper; ++value) { + values[slot] = value; visit(slot + 1); + } + }; + visit(0); return optimum; +} + +void matches_oracle(const Model& model) { + const auto snapshot = model.snapshot(); + const auto expected = oracle(snapshot); + for (auto guarantee : {Guarantee::Numerical, Guarantee::Exact}) { + SolveOptions options; options.backend = Backend::Native; options.guarantee = guarantee; + auto result = solve_native(snapshot, options); + if (result.termination != (expected ? Termination::Optimal : Termination::Infeasible)) { + std::cerr << "Native oracle mismatch: " << to_string(result.termination) << " " << result.message << '\n'; + assert(false); + } + assert(result.model_id == model.id() && result.revision == model.revision()); + assert(result.guarantee == guarantee && result.elapsed_seconds >= 0); + assert(result.has_solution() == expected.has_value()); + if (expected) { + assert(*result.objective == static_cast(*expected)); + assert(result.best_bound == result.objective && result.absolute_gap == 0 && result.relative_gap == 0); + assert(feasible(snapshot, result.values)); + for (const auto& variable : snapshot.variables) { + if (!variable.active) continue; + const auto value = result.value(variable.variable); + assert(value == std::trunc(value)); + assert((variable.type == VariableType::SemiInteger && value == 0) || + (value >= variable.lower && value <= variable.upper)); + } + } else { + assert(!result.objective && !result.best_bound && !result.solution_validated); + } + } +} + +void exhaustive_models() { + for (int scenario = 0; scenario < 36; ++scenario) { + Model model; + const auto x = model.add_integer(-3, 3); + const auto y = model.add_integer(-2, 2); + const auto b = model.add_binary(); + model.add_row({{x, scenario % 5 - 2.0}, {y, scenario % 3 - 1.0}, {b, 2}}, -2, 4); + model.add_row({{x, -1}, {y, 1}}, -inf, scenario % 4 - 1.0); + const std::vector objective{{x, scenario % 7 - 3.0}, {y, -2}, {b, 3}}; + model.set_objective(objective, scenario % 2 ? ObjectiveSense::Maximize : ObjectiveSense::Minimize, + scenario - 20.0); + matches_oracle(model); + } + Model empty; empty.minimize({}, -9); matches_oracle(empty); + empty.add_row({}, 1, inf); matches_oracle(empty); + Model no_cost; no_cost.add_integer(-2, 2); matches_oracle(no_cost); +} + +void semi_indicators_and_aliases() { + for (bool activation : {false, true}) + for (bool maximize : {false, true}) { + Model model; + auto b = model.add_binary(); + auto x = model.add_integer(-3, 3); + auto semi = model.add_variable(VariableType::SemiInteger, 2, 4); + const auto indicator = add_indicator(model, b, activation, + {{x, 2}, {semi, -1}, {b, 1}}, -2, 1); + assert(indicator.inactive_gate); + model.add_row({{*indicator.inactive_gate, 1}, {semi, 1}}, -inf, 4); + model.set_objective({{x, -2}, {semi, 3}, {*indicator.inactive_gate, 1}}, + maximize ? ObjectiveSense::Maximize : ObjectiveSense::Minimize, -7); + matches_oracle(model); + // Mutated public snapshots must not bypass indicator domain guards. + auto changed = model.snapshot(); changed.variables[x.id].upper = 4; + assert(solve_native(changed).termination == Termination::InvalidModel); + changed = model.snapshot(); changed.indicators.clear(); + assert(solve_native(changed).termination == Termination::InvalidModel); + } + Model redundant; + auto b = redundant.add_binary(); + auto x = redundant.add_integer(0, 2); + auto condition = add_indicator(redundant, b, true, {{x, 1}}, 0, inf); + assert(!condition.inactive_gate); + add_indicator(redundant, b, false, {}, 1, inf); + add_indicator(redundant, b, true, {{x, 1}}, -inf, inf); + matches_oracle(redundant); + + // Large derived M is nonintegral after outward rounding. Native reification + // solves original integral rows, so these generated M coefficients are not + // incorrectly interpreted as an unsupported original fractional model. + Model big; + b = big.add_binary(); x = big.add_integer(-1000000, 1000000); + auto large = add_indicator(big, b, true, {{x, 1}}, 1000000, inf); + big.set_bounds(b, 1, 1); big.minimize({{x, 1}}); + SolveOptions exact; exact.guarantee = Guarantee::Exact; + const auto solution = solve_native(big, exact); + assert(solution.termination == Termination::Optimal && solution.value(x) == 1000000); + assert(solution.value(*large.inactive_gate) == 0 && solution.guarantee == Guarantee::Exact); +} + +void identities_and_history() { + Model model; + auto removed = model.add_integer(-1, 1); model.remove(removed); + auto x = model.add_integer(-2, 2); + auto row = model.add_row({{x, 1}}, -1, inf); model.remove(row); + model.maximize({{x, -3}}, 4); + const auto old = solve_native(model); + assert(old.termination == Termination::Optimal && old.value(x) == -2 && *old.objective == 10); + assert(!old.active_variables[removed.id] && std::isnan(old.values[removed.id])); + model.set_bounds(x, 1, 2); + assert(solve_native(model).value(x) == 1 && old.value(x) == -2); + Model other; auto foreign = other.add_binary(); + bool rejected = false; try { (void) old.value(foreign); } catch (const ModelError&) { rejected = true; } + assert(rejected); + rejected = false; try { (void) old.value(removed); } catch (const ModelError&) { rejected = true; } + assert(rejected); + auto bad = model.snapshot(); bad.rows[0].constraint.model_id = other.id(); + assert(solve_native(bad).termination == Termination::InvalidModel); + Model moved(std::move(model)); + assert(solve_native(model).termination == Termination::InvalidModel); + assert(solve_native(moved).termination == Termination::Optimal); +} + +void options_and_limits() { + Model model; auto x = model.add_integer(-3, 3); model.minimize({{x, 1}}); + for (auto reason : {Termination::TimeLimit, Termination::NodeLimit, Termination::Cancelled}) { + SolveOptions options; + if (reason == Termination::TimeLimit) options.time_limit_seconds = 0; + if (reason == Termination::NodeLimit) options.node_limit = 0; + if (reason == Termination::Cancelled) { + options.cancellation = std::make_shared(); options.cancellation->cancel(); + } + const auto stopped = solve_native(model, options); + assert(stopped.termination == reason && !stopped.has_solution() && !stopped.best_bound); + } + SolveOptions cancelled; cancelled.node_limit = 0; cancelled.time_limit_seconds = 0; + cancelled.cancellation = std::make_shared(); cancelled.cancellation->cancel(); + assert(solve_native(model, cancelled).termination == Termination::Cancelled); + for (std::uint64_t limit : {1, 2, 3, 4, 14, 20}) { + Model hard; std::vector objective; + for (int i = 0; i < 12; ++i) objective.push_back({hard.add_binary(), 1}); + hard.maximize(objective); + SolveOptions limited; limited.node_limit = limit; limited.guarantee = Guarantee::Exact; + const auto stopped = solve_native(hard, limited); + assert(stopped.termination == Termination::NodeLimit && !stopped.best_bound); + if (limit >= 14) assert(stopped.has_solution()); + if (stopped.has_solution()) assert(feasible(hard.snapshot(), stopped.values)); + } + SolveOptions options; options.guarantee = Guarantee::Certified; + assert(solve_native(model, options).termination == Termination::Unsupported); + options = {}; options.threads = 2; + assert(solve_native(model, options).termination == Termination::Unsupported); + options = {}; options.random_seed = 1; + assert(solve_native(model, options).termination == Termination::Unsupported); + options = {}; options.backend = Backend::Highs; + assert(solve_native(model, options).termination == Termination::Unsupported); + options = {}; options.primal_start = {{x, 1}}; + const auto seeded = solve_native(model, options); + assert(seeded.termination == Termination::Optimal && seeded.start_submitted && seeded.objective == -3); + options = {}; options.feasibility_tolerance = -1; + assert(solve_native(model, options).termination == Termination::InvalidModel); +} + +void unsupported_models() { + const auto unsupported = [](Model& model) { + const auto result = solve_native(model); + assert(result.termination == Termination::Unsupported && !result.has_solution() && !result.best_bound); + }; + Model continuous; continuous.add_continuous(0, 0); unsupported(continuous); + Model unbounded; unbounded.add_integer(); unsupported(unbounded); + Model fractional_domain; fractional_domain.add_integer(0.5, 3); unsupported(fractional_domain); + Model fractional_row; auto x = fractional_row.add_integer(0, 2); + fractional_row.add_row({{x, 0.5}}, 0, 1); unsupported(fractional_row); + Model fractional_side; x = fractional_side.add_integer(0, 2); + fractional_side.add_row({{x, 1}}, 0.5, 1); unsupported(fractional_side); + Model fractional_objective; x = fractional_objective.add_integer(0, 2); + fractional_objective.minimize({{x, 0.5}}); unsupported(fractional_objective); + Model offset; offset.minimize({}, 0.5); unsupported(offset); + Model too_big; x = too_big.add_integer(-2000000000, 2000000000); + too_big.add_row({{x, 2}}, 0, 10); unsupported(too_big); + Model objective_overflow; x = objective_overflow.add_integer(0, 2000000000); + objective_overflow.minimize({{x, 1}}); unsupported(objective_overflow); + Model double_limit; x = double_limit.add_integer(0, 2); + double_limit.minimize({{x, 1}}, 9007199254740992.0); unsupported(double_limit); + Model exact_offset; exact_offset.minimize({}, -9007199254740992.0); + SolveOptions exact; exact.guarantee = Guarantee::Exact; + const auto result = solve_native(exact_offset, exact); + assert(result.termination == Termination::Optimal && *result.objective == -9007199254740992.0); +} +} +#endif + +int main() { + const auto capabilities = native_capabilities(); + assert(Gecode::Optimize::capabilities(Backend::Native).available == capabilities.available); + Model routed; + auto route_x = routed.add_integer(-2, 5); + routed.maximize({{route_x,-3}}, 7); + SolveOptions route_options; + route_options.backend = Backend::Native; + route_options.guarantee = Guarantee::Exact; + auto route_result = solve(routed, route_options); + if (capabilities.available) { + assert(route_result.termination == Termination::Optimal && route_result.has_solution()); + assert(route_result.guarantee == Guarantee::Exact && *route_result.objective == 13); + } else assert(route_result.termination == Termination::Unsupported); +#ifdef GECODE_OPTIMIZE_WITH_NATIVE + assert(capabilities.available && capabilities.exact_solving && !capabilities.linear_programming); + exhaustive_models(); semi_indicators_and_aliases(); identities_and_history(); + options_and_limits(); unsupported_models(); + std::cout << "PASS native bridge and exhaustive original integer oracle\n"; +#else + assert(!capabilities.available && !capabilities.exact_solving); + Model model; model.add_integer(0, 1); + assert(solve_native(model).termination == Termination::Unsupported); + auto malformed = model.snapshot(); malformed.model_id = 0; + assert(solve_native(malformed).termination == Termination::InvalidModel); + Model moved(std::move(model)); + assert(solve_native(model).termination == Termination::InvalidModel); + std::cout << "PASS disabled native bridge contract\n"; +#endif +} diff --git a/test/optimize/native_auto.cpp b/test/optimize/native_auto.cpp new file mode 100644 index 0000000000..13f3ccbbee --- /dev/null +++ b/test/optimize/native_auto.cpp @@ -0,0 +1,211 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +#include +using namespace Gecode::Optimize; +namespace { +constexpr double inf = std::numeric_limits::infinity(); +Model fixture(int kind) { + Model m; std::vector x; std::vector sum, cost; + for (int i=0;i<10;++i) { + x.push_back(m.add_binary()); sum.push_back({x.back(),kind>=3 ? double(i+1):1.0}); + cost.push_back({x.back(),double((i*7)%13+1)}); + } + if (kind>=3) { + if (kind==4) for (auto& t:sum) t.coefficient*=10000; + m.add_row(sum,-inf,kind==4 ? 70000:17);m.maximize(cost); + } else { + m.add_row(sum,3,7);m.minimize(cost); + if(kind==0) m.add_row({{x[0],1},{x[1],1}},1,inf); + if(kind==1) for(int i=0;i<9;++i)m.add_row({{x[i],1},{x[i+1],-1}},-inf,0); + if(kind==2)m.add_row({{x[0],3},{x[1],2},{x[3],4},{x[5],-2}},2,6); + } + return m; +} +// Independent exhaustive binary oracle, without solver or common validator. +std::pair> oracle(const ModelSnapshot& m) { + double best=m.objective.sense==ObjectiveSense::Minimize ? inf:-inf; + std::vector witness; + for(unsigned mask=0;mask<(1U< point(m.variables.size()); + for(unsigned i=0;i>i)&1U; + bool valid=true; + for(const auto& variable:m.variables) + valid=valid && point[variable.variable.id]>=variable.lower && + point[variable.variable.id]<=variable.upper; + for(const auto& row:m.rows)if(row.active){ + double value=0;for(auto t:row.terms)value+=t.coefficient*point[t.variable.id]; + valid=valid && value>=row.lower && value<=row.upper; + } + if(!valid)continue; + double value=m.objective.offset; + for(auto t:m.objective.terms)value+=t.coefficient*point[t.variable.id]; + if(witness.empty() || (m.objective.sense==ObjectiveSense::Minimize ? valuebest)){ + best=value;witness=point; + } + } + return {best,witness}; +} +void check(const ModelSnapshot& s,const SolveResult& r,double optimum){ + if(r.termination!=Termination::Optimal)std::cerr<=v.lower && r.values[v.variable.id]<=v.upper); + } + for(const auto& row:s.rows){double a=0;for(auto t:row.terms)a+=t.coefficient*r.values[t.variable.id];assert(a>=row.lower && a<=row.upper);} +} + +void configuration(const SolveOptions& solve,bool lp) { + NativeAutoOptions options;options.solve=solve; + // Isolate the transformations so a disabled feature cannot be masked by a + // different coordinator solving the fixture first. + for(int feature=0;feature<3;++feature){ + Model m;std::vector x;std::vector sum,cost; + for(int i=0;i<6;++i){ + x.push_back(feature==0 && i==0 ? m.add_variable(VariableType::Binary,1,1):m.add_binary()); + sum.push_back({x.back(),1});cost.push_back({x.back(),feature==2?1.0:double(i+1)}); + } + if(feature==1){ + m.add_row({{x[0],1},{x[1],1},{x[2],1}},1,2); + m.add_row({{x[3],1},{x[4],1},{x[5],1}},1,2); + } else m.add_row(sum,2,4); + m.minimize(cost,-9); + const auto source=m.snapshot();const auto expected=oracle(source).first; + const char* markers[]={"Exact integer presolve (","independent components","duplicate-column symmetry"}; + for(bool enabled:{false,true}){ + options.settings={false,false,false,false}; + if(feature==0)options.settings.presolve=enabled; + if(feature==1)options.settings.components=enabled; + if(feature==2)options.settings.symmetry=enabled; + const auto result=solve_native_auto_configured(source,options); + check(source,result,expected); + assert((result.message.find(markers[feature])!=std::string::npos)==enabled); + } + } + auto knapsack=fixture(3);const auto source=knapsack.snapshot(); + const auto expected=oracle(source).first; + options.settings={false,false,false,true}; + const auto enabled=solve_native_auto_configured(knapsack,options);check(source,enabled,expected); + options.settings.knapsack=false; + const auto disabled=solve_native_auto_configured(source,options);check(source,disabled,expected); + // Turning off DP must also remove its selection priority: checked LP and + // reliability become available for this eligible weighted binary knapsack. + if(lp){ + assert(enabled.backend=="Gecode native"); + assert(enabled.message.find("eligible exact knapsack DP")!=std::string::npos); + assert(disabled.backend.find("checked LP")!=std::string::npos); + assert(disabled.message.find("eligible exact knapsack DP")==std::string::npos); + } + // A fixed column makes presolve produce a DP-eligible reduced model. Keep + // components disabled so the selected reduced leaf remains observable. + auto reduced=fixture(3);const auto fixed=reduced.add_variable(VariableType::Binary,1,1); + auto cost=reduced.snapshot().objective.terms;cost.push_back({fixed,13});reduced.maximize(cost,-7); + const auto reduced_source=reduced.snapshot(); + options.settings={true,false,true,false}; + const auto reduced_result=solve_native_auto_configured(reduced,options); + check(reduced_source,reduced_result,oracle(reduced_source).first); + assert(reduced_result.message.find("Exact integer presolve (")!=std::string::npos); + if(lp)assert(reduced_result.backend.find("checked LP")!=std::string::npos); + + // All combinations keep the same original feasible set/objective, including + // a disconnected model whose component leaves are eligible for knapsack DP. + Model split;std::vector split_cost; + for(int group=0;group<2;++group){std::vector row; + for(int i=0;i<5;++i){const auto x=split.add_binary();row.push_back({x,double(i+1)}); + split_cost.push_back({x,double(i*3+group+1)});} + split.add_row(row,-inf,7); + } + split.maximize(split_cost,11);const auto split_source=split.snapshot(); + const auto split_expected=oracle(split_source).first; + for(unsigned mask=0;mask<16;++mask){ + options.settings={bool(mask&1),bool(mask&2),bool(mask&4),bool(mask&8)}; + check(split_source,solve_native_auto_configured(split,options),split_expected); + auto stopped=options;stopped.solve.time_limit_seconds=0; + assert(solve_native_auto_configured(split_source,stopped).termination==Termination::TimeLimit); + stopped=options;stopped.solve.cancellation=std::make_shared(); + stopped.solve.cancellation->cancel(); + assert(solve_native_auto_configured(split_source,stopped).termination==Termination::Cancelled); + } +} +} +int main(){ + SolveOptions o;o.backend=Backend::Native;o.guarantee=Guarantee::Exact; + o.relative_gap=o.absolute_gap=0;o.time_limit_seconds=5; + if(!native_capabilities().available){ + auto m=fixture(0); + assert(solve_native_auto(m,o).termination==Termination::Unsupported); + NativeAutoOptions configured;configured.solve=o;configured.settings={false,false,false,false}; + assert(solve_native_auto_configured(m,configured).termination==Termination::Unsupported); + assert(solve_native_auto_configured(m.snapshot(),configured).termination==Termination::Unsupported); + configured.solve.threads=0; + assert(solve_native_auto_configured(m,configured).termination==Termination::InvalidModel); + assert(solve(m,o).termination==Termination::Unsupported); + std::cout<<"Automatic native disabled-backend checks passed\n";return 0; + } + const bool lp=native_lp_capabilities().available; + configuration(o,lp); + for(int kind=0;kind<5;++kind){ + auto m=fixture(kind);auto s=m.snapshot();auto expected=oracle(s); + for(bool snapshot:{false,true}){ + auto r=snapshot ? solve(s,o):solve(m,o);check(s,r,expected.first); + if(lp && kind!=3)assert(r.backend.find("checked LP")!=std::string::npos); + else assert(r.backend==solve_native(s,o).backend); + if(lp && (kind==2 || kind==4))assert(r.backend.find("frontier")!=std::string::npos); + } + auto started=o;for(unsigned i=0;i();stopped.cancellation->cancel(); + assert(solve_native_auto(s,stopped).termination==Termination::Cancelled); + auto invalid=o;invalid.threads=0; + assert(solve_native_auto(s,invalid).termination==Termination::InvalidModel); + auto certified=o;certified.guarantee=Guarantee::Certified; + assert(solve_native_auto(s,certified).termination==Termination::Unsupported); + auto limited=o;limited.node_limit=1;auto r=solve_native_auto(s,limited); + assert(r.termination==Termination::Optimal || r.termination==Termination::NodeLimit); + if(r.best_bound)assert(s.objective.sense==ObjectiveSense::Minimize ? *r.best_bound<=expected.first:*r.best_bound>=expected.first); + s.rows[0].terms[0].variable.id+=100; + assert(solve_native_auto(s,o).termination==Termination::InvalidModel); + } + Model global;auto a=global.add_integer(0,2),b=global.add_integer(0,2); + add_all_different(global,{a,b});global.minimize({{a,1},{b,1}}); + auto automatic=o;automatic.backend=Backend::Auto;auto g=solve(global,automatic); + assert(g.termination==Termination::Optimal && g.objective==1 && g.backend=="Gecode native"); + Model large;for(int i=0;i<4100;++i)large.add_variable(VariableType::Binary,0,0); + auto l=solve_native_auto(large,o);assert(l.termination==Termination::Optimal && l.objective==0); + auto numeric=fixture(0);auto ns=numeric.snapshot(); + numeric.add_row({{ns.variables[0].variable,1000000001}},-inf,1000000001); + auto nr=solve_native_auto(numeric,o);check(numeric.snapshot(),nr,oracle(numeric.snapshot()).first); + // Presolve can now remove the redundant oversized row. A supplied start + // deliberately skips transformations, still exercising numeric LP fallback. + auto numeric_start=o;auto numeric_snapshot=numeric.snapshot(); + auto numeric_witness=oracle(numeric_snapshot).second; + for(unsigned i=0;i +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace Gecode::Optimize; +namespace { +constexpr double inf=std::numeric_limits::infinity(); +std::uint64_t runs=0; +thread_local std::function hook; +// Independent finite product oracle: no native compiler, propagator or common +// validator is used to compute feasible points or objective bounds. +bool feasible(const ModelSnapshot& model,const std::vector& point) { + const auto row=[&](const std::vector& terms,double lower,double upper) { + std::int64_t sum=0; + for(const auto& term:terms) sum+=static_cast(term.coefficient)*static_cast(point[term.variable.id]); + return sum>=lower && sum<=upper; + }; + for(const auto& variable:model.variables) if(variable.active) { + const auto value=point.at(variable.variable.id); + if(value!=std::trunc(value) || !std::isfinite(value)) return false; + if(!(variable.type==VariableType::SemiInteger && value==0) && (valuevariable.upper)) return false; + } + for(const auto& original:model.rows) + if(original.active && !original.indicator_origin && !row(original.terms,original.lower,original.upper)) return false; + for(const auto& indicator:model.indicators) if(indicator.active) { + const bool enabled=point[indicator.activator.id]==(indicator.active_value?1:0); + if(enabled && !row(indicator.terms,indicator.lower,indicator.upper)) return false; + if(indicator.inactive_gate && point[indicator.inactive_gate->id]!=(enabled?0:1)) return false; + } + for(const auto& global:model.globals) if(global.active) { + const bool valid=std::visit([&](const auto& data) { + using T=std::decay_t; + const auto value=[&](Variable v) {return static_cast(point[v.id]);}; + if constexpr(std::is_same_v) { + std::set seen; + for(auto v:data.variables) if(!seen.insert(value(v)).second) return false; + return true; + } else if constexpr(std::is_same_v) { + const auto index=value(data.index)-data.index_base; + return index>=0 && static_cast(index)) { + std::vector tuple;for(auto v:data.variables) tuple.push_back(value(v)); + return std::find(data.tuples.begin(),data.tuples.end(),tuple)!=data.tuples.end(); + } else if constexpr(std::is_same_v) { + std::set starts; + for(std::size_t i=0;idata.capacity) return false; + } + return true; + } else if constexpr(std::is_same_v) { + auto state=data.initial_state; + for(auto variable:data.variables){ + const auto edge=std::find_if(data.transitions.begin(),data.transitions.end(),[&](const RegularTransition& t){return t.from==state&&t.symbol==value(variable);}); + if(edge==data.transitions.end())return false; + state=edge->to; + } + return std::find(data.final_states.begin(),data.final_states.end(),state)!=data.final_states.end(); + } else { + if(data.successors.empty()) return false; + std::set seen;std::int64_t next=0; + for(std::size_t i=0;i(next)>=data.successors.size() || !seen.insert(next).second) return false; + next=value(data.successors[next])-data.index_base; + } + return next==0; + } + },global.payload); + if(!valid) return false; + } + return true; +} +std::int64_t objective(const ModelSnapshot& model,const std::vector& point) { + auto value=static_cast(model.objective.offset); + for(const auto& term:model.objective.terms) value+=static_cast(term.coefficient)*static_cast(point[term.variable.id]); + return value; +} +std::optional oracle(const ModelSnapshot& model) { + std::vector point(model.variables.size());std::optional best; + const auto visit=[&](const auto& self,std::size_t slot)->void { + if(slot==model.variables.size()) { + if(!feasible(model,point)) return; + const auto value=objective(model,point); + if(!best || (model.objective.sense==ObjectiveSense::Minimize ? value<*best:value>*best)) best=value; + return; + } + const auto& variable=model.variables[slot]; + if(!variable.active) {self(self,slot+1);return;} + if(variable.type==VariableType::SemiInteger) {point[slot]=0;self(self,slot+1);} + for(int value=static_cast(variable.lower);value<=variable.upper;++value) {point[slot]=value;self(self,slot+1);} + }; + visit(visit,0);return best; +} + +void verify(const ModelSnapshot& source,const NativeSearchOptions& options,const NativeSearchResult& answer,bool operational=false) { + ++runs;const auto best=oracle(source);const auto& r=answer.result;const auto& b=answer.branching; + const bool min=source.objective.sense==ObjectiveSense::Minimize; + assert(r.model_id==source.model_id && r.revision==source.revision && r.guarantee==options.solve.guarantee); + assert(b.requested==bool(options.branching)); + assert(b.budget_nodes==answer.frontier.admitted_nodes+b.probe_status_calls); + assert(b.manual_splits+b.fallback_decisions<=b.decisions); + assert(b.finite_samples==2*b.published_pairs && b.zero_gain_samples<=b.finite_samples); + assert(b.published_pairs<=b.completed_pairs && b.completed_pairs*2<=b.probe_status_calls); + assert(b.failed_directions<=b.completed_pairs*2 && b.probe_lp_calls<=answer.relaxation.lp_calls); + assert(b.probe_lp_seconds<=answer.relaxation.lp_seconds+1e-9); + assert(answer.frontier.peak_open_nodes<=options.max_open_nodes); + if(options.solve.node_limit)assert(b.budget_nodes<=*options.solve.node_limit); + if(options.branching){assert(b.work<=options.branching->max_branching_work);assert(b.history_entries<=options.branching->max_history_entries);assert(b.probe_status_calls<=options.branching->max_probe_status_calls);} + else assert(!b.decisions && !b.probe_status_calls && !b.history_entries && !b.work); + if(r.has_solution()) { + assert(best && feasible(source,r.values) && r.objective==static_cast(objective(source,r.values))); + assert(min ? *r.objective>=*best:*r.objective<=*best); + for(const auto& v:source.variables) {assert(r.active_variables[v.variable.id]==v.active);if(!v.active)assert(std::isnan(r.values[v.variable.id]));} + } + if(r.best_bound) { + assert(std::isfinite(*r.best_bound)); + if(best)assert(min ? *r.best_bound<=*best:*r.best_bound>=*best); + if(r.has_solution())assert(min ? *r.best_bound<=*r.objective:*r.best_bound>=*r.objective); + } + if(r.termination==Termination::Optimal)assert(best && r.has_solution() && *r.objective==*best && r.best_bound==r.objective && !answer.frontier.unresolved_regions); + else if(r.termination==Termination::Infeasible)assert(!best && !r.has_solution() && !r.best_bound && !answer.frontier.unresolved_regions); + else assert(r.termination==Termination::NodeLimit || r.termination==Termination::MemoryLimit || r.termination==Termination::Cancelled || r.termination==Termination::TimeLimit || (operational && r.termination==Termination::BackendError)); +} +NativeSearchOptions settings(){NativeSearchOptions o;o.solve.guarantee=Guarantee::Exact;o.branching=NativeBranchingSettings{};return o;} +void limits(const Model& model,bool lp=false) { + const auto source=model.snapshot(); + for(auto order:{NativeSearchOrder::DepthFirst,NativeSearchOrder::BestBound}) { + auto o=settings();o.order=order; + if(lp){o.relaxation=NativeLpSettings{};o.relaxation->frequency=NativeLpFrequency::AfterBoundChanges;o.relaxation->root_cover_cuts=NativeRootCoverSettings{};} + const auto full=solve_native_search(source,o);verify(source,o,full); + assert(full.result.termination==Termination::Optimal || full.result.termination==Termination::Infeasible); + for(std::uint64_t n=0;n<=full.branching.budget_nodes+1;++n){o.solve.node_limit=n;verify(source,o,solve_native_search(source,o));} + o.solve.node_limit.reset(); + for(std::size_t n=0;n<=3;++n){o.max_open_nodes=n;verify(source,o,solve_native_search(source,o));} + o.max_open_nodes=100000; + for(unsigned n=0;n<11;++n){o.branching=NativeBranchingSettings{};auto& s=*o.branching; + switch(n){case 0:s.max_candidates_per_decision=0;break;case 1:s.max_probe_status_calls=0;break;case 2:s.max_probe_status_calls=1;break;case 3:s.max_probe_status_calls=2;break;case 4:s.max_probe_status_calls_per_decision=1;break;case 5:s.max_branching_work=0;break;case 6:s.max_history_entries=0;break;case 7:s.max_history_entries=1;break;case 8:s.max_nonimproving_pairs=0;break;case 9:s.max_candidates_per_decision=1;break;case 10:s.reliability_samples=1;break;} + const auto result=solve_native_search(source,o);verify(source,o,result); + assert(result.result.termination==Termination::Optimal || result.result.termination==Termination::Infeasible); + } + } + assert(model.revision()==source.revision); +} +void fixtures() { + for(int i=0;i<24;++i){Model m;auto dead=m.add_binary();m.remove(dead);const auto a=m.add_binary(),b=m.add_binary(),c=m.add_binary(); + m.add_row({{a,double(i%5-2)},{b,2},{c,-1}},-1,double(i%3)); + m.set_objective({{a,double(i%4-2)},{b,-3},{c,2}},i%2?ObjectiveSense::Minimize:ObjectiveSense::Maximize,i-11);limits(m);} + Model empty;empty.minimize({},-7);limits(empty);empty.add_row({},1,inf);limits(empty); + for(bool min:{false,true})for(bool activation:{false,true}){ + Model m;const auto a=m.add_binary(),b=m.add_binary(),c=m.add_binary(),s=m.add_variable(VariableType::SemiInteger,2,3); + m.add_row({{a,3},{b,3},{c,1}},-inf,5);add_all_different(m,{a,b});add_indicator(m,c,activation,{{s,1},{a,-1}},2,3); + m.set_objective({{a,-2},{b,-1},{c,1},{s,1}},min?ObjectiveSense::Minimize:ObjectiveSense::Maximize,17);limits(m,native_lp_capabilities().available); + } + {Model m;const auto a=m.add_binary(),b=m.add_binary();add_table(m,{a,b},{{0,0},{1,1}});m.minimize({{a,1},{b,-2}});limits(m);} + {Model m;const auto a=m.add_binary(),b=m.add_binary(),i=m.add_integer(-1,0),r=m.add_binary();add_element(m,i,{a,b},r,-1);m.maximize({{r,2},{a,-1}});limits(m);} + {Model m;const auto a=m.add_binary(),b=m.add_binary();add_cumulative(m,{a,b},{1,1},{1,1},1);m.minimize({{a,2},{b,-1}});limits(m);} + {Model m;const auto a=m.add_integer(0,2),b=m.add_integer(0,2),c=m.add_integer(0,2),z=m.add_binary();add_circuit(m,{a,b,c},0);m.add_row({{a,1},{z,-1}},0,1);m.minimize({{a,1},{z,-2}});limits(m);} + {Model m;const auto x=m.add_variable(VariableType::SemiInteger,3,4),y=m.add_integer(-2,2);m.add_row({{x,1},{y,1}},1,3);m.minimize({{x,1},{y,-2}});limits(m);} +} +Model rank_fixture(){Model m;const auto a=m.add_binary(),b=m.add_binary(),c=m.add_binary();m.minimize({{a,1},{b,4},{c,2}},17);return m;} +void boundaries() { + Model m=rank_fixture();const auto source=m.snapshot();auto o=settings(); + auto bad=o;bad.branching->reliability_samples=0;assert(solve_native_search(source,bad).result.termination==Termination::InvalidModel); + bad=o;bad.branching->policy=static_cast(99);assert(solve_native_search(source,bad).result.termination==Termination::InvalidModel); + for(int kind=0;kind<3;++kind){auto z=o;if(kind==0)z.solve.node_limit=0;else if(kind==1)z.solve.time_limit_seconds=0;else{z.solve.cancellation=std::make_shared();z.solve.cancellation->cancel();}auto result=solve_native_search(source,z);verify(source,z,result);assert(!result.branching.decisions && !result.branching.probe_status_calls && !result.result.has_solution());} + auto full=solve_native_search(source,o);verify(source,o,full);assert(full.branching.probe_status_calls && full.branching.manual_splits && full.branching.published_pairs && full.branching.reliable_candidates); + for(std::size_t work=0;work<110;++work){auto limited=o;limited.branching->max_branching_work=work;auto r=solve_native_search(source,limited);verify(source,limited,r);assert(r.result.objective==17);} + for(std::uint64_t n=1;n<5;++n){auto limited=o;limited.solve.node_limit=n;auto r=solve_native_search(source,limited);verify(source,limited,r);assert(!r.branching.probe_status_calls);} + auto baseline=o;baseline.branching.reset();const auto base=solve_native_search(source,baseline);verify(source,baseline,base); + auto zero=o;zero.branching->max_branching_work=0;const auto skipped=solve_native_search(source,zero);verify(source,zero,skipped); + assert(base.frontier.admitted_nodes==skipped.frontier.admitted_nodes && base.frontier.expanded_nodes==skipped.frontier.expanded_nodes && !skipped.branching.manual_splits); + std::vector> tasks;for(int i=0;i<4;++i)tasks.push_back(std::async(std::launch::async,[&]{return solve_native_search(source,o);})); + for(auto& task:tasks){auto r=task.get();verify(source,o,r);assert(r.branching.probe_status_calls==full.branching.probe_status_calls && r.branching.manual_splits==full.branching.manual_splits);} + m.set_bounds(m.snapshot().variables[0].variable,1,1);auto edited=m.snapshot();verify(edited,o,solve_native_search(edited,o));verify(source,o,full); + if(native_lp_capabilities().available){Model lp;auto a=lp.add_binary(),b=lp.add_binary(),c=lp.add_binary();lp.add_row({{a,3},{b,3}},-inf,5);lp.minimize({{a,-2},{b,-2},{c,1}},17); + for(bool covers:{false,true}){auto opts=settings();opts.relaxation=NativeLpSettings{};opts.relaxation->frequency=NativeLpFrequency::AfterBoundChanges;if(covers)opts.relaxation->root_cover_cuts=NativeRootCoverSettings{};const auto snapshot=lp.snapshot();const auto r=solve_native_search(snapshot,opts);verify(snapshot,opts,r);assert(r.result.objective==15 && r.branching.probe_lp_calls>0);} + } + // Two contradictory clause pairs only fail after probing/branching. + {Model impossible;auto x=impossible.add_binary(),a=impossible.add_binary(),b=impossible.add_binary(); + impossible.add_row({{x,1},{a,1}},1,inf);impossible.add_row({{x,1},{a,-1}},0,inf); + impossible.add_row({{x,-1},{b,1}},0,inf);impossible.add_row({{x,-1},{b,-1}},-1,inf); + auto options=settings();options.branching->max_candidates_per_decision=1;const auto snapshot=impossible.snapshot();const auto r=solve_native_search(snapshot,options);verify(snapshot,options,r); + assert(r.result.termination==Termination::Infeasible && r.branching.failed_directions==2 && r.branching.completed_pairs==1 && !r.branching.published_pairs && !r.branching.finite_samples); + } +} +#ifdef GECODE_NATIVE_BRANCHING_TEST_HOOKS +void faults_and_traces() { + Model m=rank_fixture();const auto source=m.snapshot(); + const std::vector events={"candidate_scan","pair_begin","probe_clone","probe_cloned","probe_posted","probe_before_status","probe_after_status","probe_destroyed","pair_before_publication","pair_published","candidate_score","branch_selected"}; + for(const auto& event:events)for(int fault=0;fault<3;++fault){ + auto o=settings();o.solve.cancellation=std::make_shared();bool fired=false; + hook=[&](const char* name,std::size_t,double&,double&){if(!fired && name==event){fired=true;if(fault==0)o.solve.cancellation->cancel();else if(fault==1)throw std::bad_alloc();else throw std::runtime_error("probe injected backend error");}}; + const auto r=solve_native_search(source,o);hook={};assert(fired);verify(source,o,r,true); + assert(r.result.termination==(fault==0?Termination::Cancelled:fault==1?Termination::MemoryLimit:Termination::BackendError)); + assert(!r.result.has_solution() && r.result.best_bound==17 && r.frontier.unresolved_regions==1); + if(event=="pair_before_publication")assert(!r.branching.published_pairs && !r.branching.finite_samples && !r.branching.history_entries); + } + // Interrupt in the second direction: no half-pair history, no proof reuse. + {auto o=settings();o.solve.cancellation=std::make_shared();int statuses=0;hook=[&](const char* e,std::size_t,double&,double&){if(std::string(e)=="probe_after_status" && ++statuses==2)o.solve.cancellation->cancel();};auto r=solve_native_search(source,o);hook={};verify(source,o,r);assert(r.branching.probe_status_calls==2 && !r.branching.published_pairs && r.result.best_bound==17 && !r.result.has_solution());} + {auto o=settings();o.solve.time_limit_seconds=1;bool fired=false;hook=[&](const char* e,std::size_t,double&,double&){if(!fired && std::string(e)=="pair_before_publication"){fired=true;std::this_thread::sleep_for(std::chrono::milliseconds(1050));}};auto r=solve_native_search(source,o);hook={};verify(source,o,r);assert(fired && r.result.termination==Termination::TimeLimit && !r.branching.published_pairs && r.result.best_bound==17 && !r.result.has_solution());} +#ifdef GECODE_NATIVE_SEARCH_TEST_HOOKS + const std::vector transfers={"child_clone","child_committed","before_propagation","after_propagation","child_enqueued","parent_discharged","before_validation","after_validation"}; + for(const auto& event:transfers)for(int fault=0;fault<3;++fault){ + auto o=settings();o.solve.cancellation=std::make_shared();bool armed=false,fired=false; + hook=[&](const char* name,std::size_t,double&,double&){if(std::string(name)=="branch_selected")armed=true;if(armed && !fired && name==event){fired=true;if(fault==0)o.solve.cancellation->cancel();else if(fault==1)throw std::bad_alloc();else throw std::runtime_error("manual transfer injected failure");}}; + const auto r=solve_native_search(source,o);hook={};assert(armed && fired);verify(source,o,r,true); + assert(r.result.termination==(fault==0?Termination::Cancelled:fault==1?Termination::MemoryLimit:Termination::BackendError)); + assert(!r.result.has_solution() && r.result.best_bound==17); + } + {auto o=settings();o.solve.cancellation=std::make_shared();int children=0;hook=[&](const char* e,std::size_t,double&,double&){if(std::string(e)=="child_clone" && ++children==2)o.solve.cancellation->cancel();};auto r=solve_native_search(source,o);hook={};verify(source,o,r);assert(children==2 && r.frontier.unresolved_regions==2 && r.result.best_bound==17 && !r.result.has_solution());} +#endif + // Scores can change traversal, never feasibility/bounds. Nonfinite scores fall back. + for(int mode=0;mode<4;++mode){auto o=settings();std::vector selected; + hook=[&](const char* e,std::size_t slot,double& down,double& up){if(std::string(e)=="candidate_score"){if(mode==0)down=up=0;else if(mode==1)down=up=std::numeric_limits::quiet_NaN();else if(mode==2)down=up=slot==0?1e300:1;else down=up=-1;}if(std::string(e)=="branch_selected")selected.push_back(slot);}; + auto r=solve_native_search(source,o);hook={};verify(source,o,r);assert(r.result.objective==17);if(mode==2)assert(!selected.empty() && selected.front()==0);else assert(selected.empty()); + } + // Repeated clones followed by built-in fallback respect Choice lifecycle. + {auto o=settings();std::size_t cloned=0,fallback=0;hook=[&](const char* e,std::size_t,double& d,double& u){const std::string name=e;if(name=="probe_cloned")++cloned;if(name=="candidate_score")d=u=0;if(name=="branch_fallback")++fallback;};auto r=solve_native_search(source,o);hook={};verify(source,o,r);assert(cloned && fallback && !r.branching.manual_splits);} + // Gate columns never enter the candidate panel, even when declared Binary. + {Model g;auto a=g.add_binary(),x=g.add_integer(-1,2);auto formulated=add_indicator(g,a,true,{{x,1}},0,1);(void)formulated;auto snapshot=g.snapshot();assert(snapshot.indicators[0].inactive_gate);const auto gate=snapshot.indicators[0].inactive_gate->id; + hook=[&](const char* e,std::size_t slot,double&,double&){if(std::string(e)=="pair_begin" || std::string(e)=="branch_selected")assert(slot!=gate);};auto o=settings();auto r=solve_native_search(snapshot,o);verify(snapshot,o,r); + remove_indicator(g,formulated.indicator);g.minimize({{*formulated.inactive_gate,-9},{a,1}});snapshot=g.snapshot();assert(snapshot.variables[gate].indicator_origin);r=solve_native_search(snapshot,o);hook={};verify(snapshot,o,r);} +} +#endif +} +namespace Gecode {namespace Optimize { +#ifdef GECODE_NATIVE_BRANCHING_TEST_HOOKS +void native_branching_test_event(const char* e,std::size_t slot,double& down,double& up){if(hook)hook(e,slot,down,up);} +#endif +#ifdef GECODE_NATIVE_SEARCH_TEST_HOOKS +void native_search_test_event(const char* e) {double down=0,up=0;if(hook)hook(e,std::numeric_limits::max(),down,up);} +#endif +#ifdef GECODE_NATIVE_ROOT_CUT_TEST_HOOKS +void native_root_cut_test_event(const char*,NativeRootCoverCompletion&) {} +#endif +}} +int main(){ + if(!native_capabilities().available){Model m;auto o=settings();auto r=solve_native_search(m,o);assert(r.result.termination==Termination::Unsupported && r.branching.requested);o.branching->reliability_samples=0;assert(solve_native_search(m,o).result.termination==Termination::InvalidModel);std::cout<<"Native branching unavailable: explicit boundaries pass\n";return 0;} + fixtures();boundaries(); +#ifdef GECODE_NATIVE_BRANCHING_TEST_HOOKS + faults_and_traces(); +#endif + std::cout< +#include +#include +#include +#include +#include +#include + +using namespace Gecode::Optimize; +namespace { +constexpr double inf=std::numeric_limits::infinity(); +// A tiny independent exhaustive continuation; every enumeration consumes the +// supplied budget. No solver implementation or common validator chooses values. +SolveResult oracle(const ModelSnapshot& m,const SolveOptions& o,SolveBudget& budget) { + SolveResult r;r.model_id=m.model_id;r.revision=m.revision;r.guarantee=o.guarantee; + r.backend="test exhaustive oracle";r.backend_version="1"; + std::vector values(m.variables.size());bool found=false,stopped=false; + double best=m.objective.sense==ObjectiveSense::Minimize?inf:-inf; + std::function visit=[&](std::size_t index) { + if(stopped)return; + if(auto reason=budget.stop_reason()){r.termination=*reason;stopped=true;return;} + if(indexrow.upper)return; + } + double cost=m.objective.offset;for(auto t:m.objective.terms)cost+=t.coefficient*values[t.variable.id]; + if(!found || (m.objective.sense==ObjectiveSense::Minimize?costbest)) { + found=true;best=cost;r.values=values; + } + }; + visit(0); + if(stopped){r.values.clear();return r;} + r.termination=found?Termination::Optimal:Termination::Infeasible; + if(found){r.objective=best;r.best_bound=best;r.solution_validated=true;r.active_variables.assign(values.size(),true);r.update_gaps(m.objective.sense);} + return r; +} + +SolveOptions options() { + SolveOptions o;o.backend=Backend::Native;o.guarantee=Guarantee::Exact; + o.relative_gap=o.absolute_gap=0;return o; +} +Model fixture(ObjectiveSense sense) { + Model m;auto gone=m.add_binary();m.remove(gone); + auto a=m.add_integer(-2,3),b=m.add_binary(),c=m.add_integer(-3,2),d=m.add_binary(); + m.add_row({{a,1},{b,2}},1,3); + m.add_row({{c,1}},-2,1); + auto removed=m.add_row({{a,1},{c,1}},-inf,100);m.remove(removed); + m.add_row({},-1,1); + m.set_objective({{a,-3},{b,2},{c,4},{d,-7}},sense,19); + return m; +} +} + +int main() { + auto o=options(); + for(auto sense:{ObjectiveSense::Minimize,ObjectiveSense::Maximize}) { + auto m=fixture(sense);auto s=m.snapshot();SolveBudget budget(o);unsigned calls=0; + auto r=Detail::native_components(s,o,budget,[&](const ModelSnapshot& part,const SolveOptions& same,SolveBudget& shared){ + assert(&shared==&budget && &same==&o);assert(part.objective.offset==0);++calls; + return oracle(part,same,shared); + }); + assert(r && calls==3 && r->termination==Termination::Optimal && r->has_solution()); + assert(r->model_id==s.model_id && r->revision==s.revision && r->values.size()==5); + assert(!r->active_variables[0] && std::isnan(r->values[0])); + // min: (a,b)=(3,0), c=-2,d=1; max: (a,b)=(-1,1), c=1,d=0. + assert(r->objective==(sense==ObjectiveSense::Minimize?-5:28)); + assert(r->best_bound==r->objective && r->absolute_gap==0 && r->relative_gap==0); + assert(validate(s,r->values,0,0).valid); + } + Model coupled;auto a=coupled.add_binary(),b=coupled.add_binary();coupled.add_row({{a,1},{b,-1}},0,0); + SolveBudget coupled_budget(o);unsigned calls=0; + auto no_call=[&](const ModelSnapshot&,const SolveOptions&,SolveBudget&)->SolveResult{++calls;assert(false);return {};}; + assert(!Detail::native_components(coupled.snapshot(),o,coupled_budget,no_call) && calls==0); + auto m=fixture(ObjectiveSense::Minimize);auto s=m.snapshot(); + auto start=o;start.primal_start.push_back({s.variables[1].variable,1});SolveBudget start_budget(start); + assert(!Detail::native_components(s,start,start_budget,no_call)); + auto fractional=s;fractional.objective.offset=0.5;SolveBudget fractional_budget(o); + assert(!Detail::native_components(fractional,o,fractional_budget,no_call)); + Model many;for(unsigned i=0;i<65;++i)many.add_binary();SolveBudget many_budget(o); + assert(!Detail::native_components(many.snapshot(),o,many_budget,no_call)); + Model impossible;impossible.add_binary();impossible.add_binary();impossible.add_row({},1,inf); + SolveBudget impossible_budget(o); + auto bad=Detail::native_components(impossible.snapshot(),o,impossible_budget,oracle); + assert(bad && bad->termination==Termination::Infeasible && !bad->has_solution() && !bad->best_bound); + auto cancelled=o;cancelled.cancellation=std::make_shared();cancelled.cancellation->cancel();SolveBudget cancel_budget(cancelled); + auto stop=Detail::native_components(s,cancelled,cancel_budget,no_call); + assert(stop && stop->termination==Termination::Cancelled && !stop->has_solution()); + auto limited=o;limited.node_limit=13;SolveBudget node_budget(limited);calls=0; + auto partial=Detail::native_components(s,limited,node_budget,[&](const ModelSnapshot& part,const SolveOptions& same,SolveBudget& shared){ + ++calls;return oracle(part,same,shared); + }); + assert(partial && calls==2 && partial->termination==Termination::NodeLimit); + assert(node_budget.nodes()==13 && !partial->has_solution() && partial->values.empty() && !partial->objective && !partial->best_bound); + // A second component must not turn its local witness into an original point. + SolveBudget partial_budget(o);calls=0; + auto interrupted=Detail::native_components(s,o,partial_budget,[&](const ModelSnapshot& part,const SolveOptions& same,SolveBudget& shared){ + auto local=oracle(part,same,shared);if(++calls==2)local.termination=Termination::TimeLimit;return local; + }); + assert(interrupted && interrupted->termination==Termination::TimeLimit && !interrupted->has_solution() && !interrupted->best_bound); + // Independently reject a claimed optimum carrying an infeasible local point. + SolveBudget dishonest_budget(o); + auto dishonest=Detail::native_components(s,o,dishonest_budget,[&](const ModelSnapshot& part,const SolveOptions& same,SolveBudget& shared){ + auto local=oracle(part,same,shared);local.values[0]=100;return local; + }); + assert(dishonest && dishonest->termination==Termination::BackendError && !dishonest->has_solution()); + std::cout<<"Independent component optima, offsets, mapping, infeasibility and shared budgets passed\n"; +} diff --git a/test/optimize/native_knapsack.cpp b/test/optimize/native_knapsack.cpp new file mode 100644 index 0000000000..c9c23746b4 --- /dev/null +++ b/test/optimize/native_knapsack.cpp @@ -0,0 +1,399 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Gecode::Optimize; +namespace { +constexpr double inf=std::numeric_limits::infinity(); +std::uint64_t runs=0; + +// Independent finite product oracle: no native compiler, propagator or common +// validator is used to compute feasible points or objective bounds. +bool feasible(const ModelSnapshot& model,const std::vector& point) { + const auto row=[&](const std::vector& terms,double lower,double upper) { + std::int64_t sum=0; + for(const auto& term:terms) sum+=static_cast(term.coefficient)*static_cast(point[term.variable.id]); + return sum>=lower && sum<=upper; + }; + for(const auto& variable:model.variables) if(variable.active) { + const auto value=point.at(variable.variable.id); + if(value!=std::trunc(value) || !std::isfinite(value)) return false; + if(!(variable.type==VariableType::SemiInteger && value==0) && (valuevariable.upper)) return false; + } + for(const auto& original:model.rows) + if(original.active && !original.indicator_origin && !row(original.terms,original.lower,original.upper)) return false; + for(const auto& indicator:model.indicators) if(indicator.active) { + const bool enabled=point[indicator.activator.id]==(indicator.active_value?1:0); + if(enabled && !row(indicator.terms,indicator.lower,indicator.upper)) return false; + if(indicator.inactive_gate && point[indicator.inactive_gate->id]!=(enabled?0:1)) return false; + } + for(const auto& global:model.globals) if(global.active) { + const bool valid=std::visit([&](const auto& data) { + using T=std::decay_t; + const auto value=[&](Variable v) {return static_cast(point[v.id]);}; + if constexpr(std::is_same_v) { + std::set seen; + for(auto v:data.variables) if(!seen.insert(value(v)).second) return false; + return true; + } else if constexpr(std::is_same_v) { + const auto index=value(data.index)-data.index_base; + return index>=0 && static_cast(index)) { + std::vector tuple;for(auto v:data.variables) tuple.push_back(value(v)); + return std::find(data.tuples.begin(),data.tuples.end(),tuple)!=data.tuples.end(); + } else if constexpr(std::is_same_v) { + std::set starts; + for(std::size_t i=0;idata.capacity) return false; + } + return true; + } else if constexpr(std::is_same_v) { + auto state=data.initial_state; + for(auto variable:data.variables){ + const auto edge=std::find_if(data.transitions.begin(),data.transitions.end(),[&](const RegularTransition& t){return t.from==state&&t.symbol==value(variable);}); + if(edge==data.transitions.end())return false; + state=edge->to; + } + return std::find(data.final_states.begin(),data.final_states.end(),state)!=data.final_states.end(); + } else { + if(data.successors.empty()) return false; + std::set seen;std::int64_t next=0; + for(std::size_t i=0;i(next)>=data.successors.size() || !seen.insert(next).second) return false; + next=value(data.successors[next])-data.index_base; + } + return next==0; + } + },global.payload); + if(!valid) return false; + } + return true; +} +std::int64_t objective(const ModelSnapshot& model,const std::vector& point) { + auto value=static_cast(model.objective.offset); + for(const auto& term:model.objective.terms) value+=static_cast(term.coefficient)*static_cast(point[term.variable.id]); + return value; +} +std::optional oracle(const ModelSnapshot& model) { + std::vector point(model.variables.size());std::optional best; + const auto visit=[&](const auto& self,std::size_t slot)->void { + if(slot==model.variables.size()) { + if(!feasible(model,point)) return; + const auto value=objective(model,point); + if(!best || (model.objective.sense==ObjectiveSense::Minimize ? value<*best:value>*best)) best=value; + return; + } + const auto& variable=model.variables[slot]; + if(!variable.active) {self(self,slot+1);return;} + if(variable.type==VariableType::SemiInteger) {point[slot]=0;self(self,slot+1);} + for(int value=static_cast(variable.lower);value<=variable.upper;++value) {point[slot]=value;self(self,slot+1);} + }; + visit(visit,0);return best; +} +SolveOptions exact() { + SolveOptions options; options.backend = Backend::Native; options.guarantee = Guarantee::Exact; + return options; +} +void check(const ModelSnapshot& source, const SolveResult& result, + const std::optional& best, bool completed = false) { + ++runs; + const bool minimize = source.objective.sense == ObjectiveSense::Minimize; + assert(result.model_id == source.model_id && result.revision == source.revision); + assert(result.guarantee == Guarantee::Exact); + if (result.has_solution()) { + assert(best && feasible(source, result.values)); + assert(result.objective == static_cast(objective(source, result.values))); + assert(minimize ? *result.objective >= *best : *result.objective <= *best); + for (const auto& v : source.variables) { + assert(result.active_variables.at(v.variable.id) == v.active); + if (!v.active) assert(std::isnan(result.values.at(v.variable.id))); + } + } + if (result.best_bound && best) + assert(minimize ? *result.best_bound <= *best : *result.best_bound >= *best); + if (result.termination == Termination::Optimal) { + assert(best && result.objective == *best && result.best_bound == result.objective); + } else if (result.termination == Termination::Infeasible) { + assert(!best && !result.has_solution() && !result.best_bound); + } else { + assert(!completed && result.termination == Termination::NodeLimit); + } +} +void exercise(const Model& model) { + const auto source = model.snapshot(); const auto best = oracle(source); + auto options = exact(); + check(source, solve_native(source, options), best, true); + for (std::uint64_t limit : {0, 1, 2, 3, 5, 8}) { + options.node_limit = limit; const auto result = solve_native(source, options); + check(source, result, best); + if (result.termination == Termination::NodeLimit) assert(!result.best_bound); + } + for (auto order : {NativeSearchOrder::DepthFirst, NativeSearchOrder::BestBound}) { + NativeSearchOptions search; search.solve = exact(); search.order = order; + check(source, solve_native_search(source, search).result, best, true); + for (std::uint64_t limit = 0; limit <= 6; ++limit) { + search.solve.node_limit = limit; const auto result = solve_native_search(source, search); + check(source, result.result, best); + assert(result.frontier.admitted_nodes <= limit); + } + } +} +Model capacity_model(bool negative = false, bool maximum = false, + int capacity = 3, bool redundant_side = false) { + Model m; const auto dead = m.add_binary(); m.remove(dead); + const auto a = m.add_binary(), b = m.add_binary(); + const double sign = negative ? -1 : 1; + m.add_row({{a, 2 * sign}, {b, 2 * sign}}, + negative ? -capacity : (redundant_side ? -1 : -inf), + negative ? (redundant_side ? 1 : inf) : capacity); + m.set_objective({{a, maximum ? 3.0 : -3.0}, {b, maximum ? 2.0 : -2.0}}, + maximum ? ObjectiveSense::Maximize : ObjectiveSense::Minimize, maximum ? -11 : 11); + return m; +} +void pure_models() { + for (bool negative : {false, true}) for (bool maximum : {false, true}) + for (bool redundant : {false, true}) for (int capacity : {0, 1, 3, 4, 7}) { + auto m = capacity_model(negative, maximum, capacity, redundant); exercise(m); + } + for (int pattern = 0; pattern < 12; ++pattern) { + Model m; std::vector row, cost; + for (int i = 0; i < 5; ++i) { + const auto x = m.add_binary(); row.push_back({x, double(i + 1)}); + cost.push_back({x, double((pattern + 2 * i) % 7 - 3)}); + } + m.add_row(row, -inf, pattern % 9); + m.set_objective(cost, pattern % 2 ? ObjectiveSense::Maximize : ObjectiveSense::Minimize, pattern - 7); + exercise(m); + } + Model ties; auto a = ties.add_binary(), b = ties.add_binary(); + ties.add_row({{a, 2}, {b, 2}}, -inf, 2); ties.minimize({{a, -1}, {b, -1}}); exercise(ties); + ties.minimize({}); exercise(ties); // No-benefit objective must keep the old path. +} +void exact_bound_and_work() { + for (bool negative : {false, true}) for (bool maximum : {false, true}) { + auto m = capacity_model(negative, maximum); const auto source = m.snapshot(); + const auto best = oracle(source); + for (auto order : {NativeSearchOrder::DepthFirst, NativeSearchOrder::BestBound}) { + NativeSearchOptions search; search.solve = exact(); search.order = order; + search.solve.node_limit = 1; + const auto root = solve_native_search(source, search); + check(source, root.result, best); + assert(!root.result.has_solution() && root.result.best_bound == *best); + search.solve.node_limit = 3; + const auto full = solve_native_search(source, search); + check(source, full.result, best, true); + assert(full.frontier.admitted_nodes == 3 && full.frontier.feasible_leaves == 1); + } + auto options = exact(); options.node_limit = 8; + check(source, solve_native(source, options), best, true); + // Both an already optimal and a poor feasible start remain ordinary starts. + for (int selected : {0, 1}) { + options = exact(); options.primal_start = { + {source.variables[1].variable, double(selected)}, {source.variables[2].variable, 0}}; + const auto answer = solve_native(source, options); + check(source, answer, best, true); assert(answer.start_submitted); + } + } +} +void zero_cost_ties() { + // A stable FIFO tie-break alone explores many equal-bound regions before + // reaching a leaf. The unique region retaining the DP witness must remain + // reachable within a linear admission quota despite these zero-cost bits. + constexpr int count = 12; + for (bool maximum : {false, true}) { + Model m; std::vector weights; + for (int i = 0; i < count; ++i) weights.push_back({m.add_binary(), 1}); + m.add_row(weights, -inf, 8); + m.set_objective({{weights.front().variable, maximum ? 1.0 : -1.0}}, + maximum ? ObjectiveSense::Maximize : ObjectiveSense::Minimize, maximum ? -3 : 3); + const auto source = m.snapshot(); const auto best = oracle(source); + auto options = exact(); options.node_limit = 2 * count + 3; + check(source, solve_native(source, options), best, true); + for (auto order : {NativeSearchOrder::DepthFirst, NativeSearchOrder::BestBound}) { + NativeSearchOptions search; search.solve = options; search.order = order; + const auto result = solve_native_search(source, search); + check(source, result.result, best, true); + assert(result.frontier.admitted_nodes <= *options.node_limit); + } + } +} +void packed_reconstruction() { + // Enumerated optima cover decision bits spanning word/row boundaries, signed + // costs, both row orientations and objective senses. Reconstruction must use + // decisions from the original row, never a subsequently overwritten value. + const int weights[] = {3,4,5,7,9,12,13,17}; + const int costs[] = {-7,-8,-11,3,-12,-16,0,-22}; + for (bool negative : {false,true}) for (bool maximum : {false,true}) + for (int capacity : {17,31,63,64,65}) { + Model m; std::vector row, objective_terms; + for (int i=0;i<8;++i) { + const auto x=m.add_binary(); + row.push_back({x,double(negative ? -weights[i] : weights[i])}); + objective_terms.push_back({x,double(maximum ? -costs[i] : costs[i])}); + } + m.add_row(row,negative ? -capacity : -inf,negative ? inf : capacity); + m.set_objective(objective_terms,maximum ? ObjectiveSense::Maximize : ObjectiveSense::Minimize, + maximum ? -9 : 9); + const auto source=m.snapshot(); const auto best=oracle(source); + check(source,solve_native(source,exact()),best,true); + for (auto order : {NativeSearchOrder::DepthFirst,NativeSearchOrder::BestBound}) { + NativeSearchOptions options; options.solve=exact(); options.order=order; + check(source,solve_native_search(source,options).result,best,true); + } + } + // Equal-profit choices across multiple packed words retain the first item: + // every later equal-cost recurrence skips its item, including zero-cost bits. + Model ties; std::vector row, costs_with_ties; + std::vector variables; + for (int i=0;i<70;++i) { + variables.push_back(ties.add_binary()); row.push_back({variables.back(),1}); + if (i<69) costs_with_ties.push_back({variables.back(),-1}); + } + ties.add_row(row,-inf,1); ties.minimize(costs_with_ties); + const auto source=ties.snapshot(); + for (int mode=0;mode<3;++mode) { + NativeSearchOptions options; options.solve=exact(); + options.order=mode==1 ? NativeSearchOrder::DepthFirst : NativeSearchOrder::BestBound; + const auto result=mode==0 ? solve_native(source,options.solve) + : solve_native_search(source,options).result; + check(source,result,-1,true); + for (std::size_t i=0;i::infinity(); + assert(solve_native(malformed, exact()).termination == Termination::InvalidModel); +} +void cap_boundaries() { + // Independent optimum: at most one profitable item fits; all other items cost + // zero and can be excluded. These checks need no exponential large-box oracle. + for (int capacity : {65536, 65537}) { + Model m; auto a = m.add_binary(), b = m.add_binary(); + m.add_row({{a, 40000}, {b, 40000}}, -inf, capacity); m.minimize({{a, -3}, {b, -2}}); + NativeSearchOptions o; o.solve = exact(); o.solve.node_limit = 1; + const auto r = solve_native_search(m, o); ++runs; + assert(r.result.termination == Termination::NodeLimit && !r.result.has_solution()); + assert(r.result.best_bound == (capacity == 65536 ? -3 : -5)); + } + // This shape exceeded the old million-int64-cell table, but now passes the + // shared automatic admission. The route assertion does not depend on whether + // optional DP finishes before its local time cap on a slow/contended host. + for (bool negative : {false,true}) for (bool maximum : {false,true}) { + Model m; std::vector weights, costs; + for (int i=0;i<17;++i) { + const auto x=m.add_binary(); weights.push_back({x,negative ? -40000.0 : 40000.0}); + if (i<2) costs.push_back({x,(maximum ? 1.0 : -1.0)*(i ? 2 : 3)}); + } + m.add_row(weights,negative ? -65536 : -inf,negative ? inf : 65536); + m.set_objective(costs,maximum ? ObjectiveSense::Maximize : ObjectiveSense::Minimize, + maximum ? -11 : 11); + auto options=exact(); options.node_limit=1; + const auto selected=solve_native_auto(m,options); ++runs; + if (native_lp_capabilities().available) + assert(selected.message.find("eligible exact knapsack DP")!=std::string::npos); + else assert(selected.backend=="Gecode native"); // Native-only route still runs DP. + assert(selected.termination==Termination::NodeLimit && !selected.has_solution()); + const auto source=m.snapshot(); + check(source,solve_native(source,exact()),maximum ? -8 : 8,true); + // Pre-cancelled large admission cannot publish any partial witness/bound. + // The existing native-search fault gate also cancels after one completed + // recurrence row, so retaining that hook tests interruption after allocation. + options=exact(); options.cancellation=std::make_shared(); + options.cancellation->cancel(); + const auto cancelled=solve_native(source,options); ++runs; + assert(cancelled.termination==Termination::Cancelled); + assert(!cancelled.has_solution() && !cancelled.best_bound); + } + // The new work cap is independent of packed storage: 489*65536 transitions + // exceed 32 million although the compact arrays would fit the byte cap. + Model oversized; std::vector weights, costs; + for (int i=0;i<489;++i) { + const auto x=oversized.add_binary(); weights.push_back({x,40000}); + if (i<2) costs.push_back({x,i ? -2.0 : -3.0}); + } + oversized.add_row(weights,-inf,65535); oversized.minimize(costs); + NativeSearchOptions options; options.solve=exact(); options.solve.node_limit=1; + const auto fallback=solve_native_search(oversized,options); ++runs; + assert(fallback.result.termination==Termination::NodeLimit && !fallback.result.has_solution()); + assert(fallback.result.best_bound==-5); +} +void explicit_lp_and_stops() { + auto m = capacity_model(); const auto source = m.snapshot(); + if (native_lp_capabilities().available) { + NativeLpOptions lp; lp.solve = exact(); + check(source, solve_native_lp(source, lp).result, oracle(source), true); + NativeSearchOptions search; search.solve = exact(); search.solve.node_limit = 1; + search.relaxation = NativeLpSettings{}; + const auto r = solve_native_search(source, search); ++runs; + assert(r.result.termination == Termination::NodeLimit && !r.result.has_solution()); + assert(r.result.best_bound == 7); // Exact LP ceil(-4)+offset11; DP would give8. + } + for (int mode = 0; mode < 3; ++mode) { + auto options = exact(); + if (mode == 0) options.node_limit = 0; + if (mode == 1) options.time_limit_seconds = 0; + if (mode == 2) { options.cancellation = std::make_shared(); options.cancellation->cancel(); } + const auto r = solve_native(source, options); ++runs; + assert(r.termination == (mode == 0 ? Termination::NodeLimit : mode == 1 ? Termination::TimeLimit : Termination::Cancelled)); + assert(!r.has_solution() && !r.best_bound); + } + std::vector> pending; + for (bool maximum : {false, true}) { + pending.push_back(std::async(std::launch::async, [maximum] { + auto local = capacity_model(true, maximum); + return solve_native(local.snapshot(), exact()); // Compiler/root die before result use. + })); + } + assert(pending[0].get().objective == 8); assert(pending[1].get().objective == -8); runs += 2; +} +} // namespace +int main() { + if (!native_capabilities().available) { + auto m = capacity_model(); assert(solve_native(m, exact()).termination == Termination::Unsupported); + NativeSearchOptions o; o.solve = exact(); + assert(solve_native_search(m, o).result.termination == Termination::Unsupported); + std::cout << "Native knapsack unavailable: explicit boundaries pass\n"; return 0; + } + pure_models(); exact_bound_and_work(); zero_cost_ties(); packed_reconstruction(); fallback_models(); + cap_boundaries(); explicit_lp_and_stops(); + std::cout << runs << " native knapsack original-oracle and budget configurations pass\n"; +} diff --git a/test/optimize/native_lp.cpp b/test/optimize/native_lp.cpp new file mode 100644 index 0000000000..8b58bf0bdb --- /dev/null +++ b/test/optimize/native_lp.cpp @@ -0,0 +1,345 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace Gecode::Optimize; +namespace { +constexpr double inf = std::numeric_limits::infinity(); + +// Deliberately separate integer oracle: no solver/compiler/validator calls. +bool feasible(const ModelSnapshot& model, const std::vector& values) { + const auto row = [&](const std::vector& terms, double lo, double hi) { + std::int64_t sum = 0; + for (const auto& term : terms) + sum += static_cast(term.coefficient) * static_cast(values[term.variable.id]); + return sum >= lo && sum <= hi; + }; + for (const auto& original : model.rows) + if (original.active && !original.indicator_origin && !row(original.terms, original.lower, original.upper)) return false; + for (const auto& original : model.indicators) if (original.active) { + const bool enabled = values[original.activator.id] == (original.active_value ? 1 : 0); + if (enabled && !row(original.terms, original.lower, original.upper)) return false; + if (original.inactive_gate && values[original.inactive_gate->id] != (enabled ? 0 : 1)) return false; + } + // The enumerated global fixture uses all-different only; other native global + // implementations already have their own full-product conformance suite. + for (const auto& original : model.globals) if (original.active) { + const auto& vars = std::get(original.payload).variables; + for (std::size_t i = 0; i < vars.size(); ++i) + for (std::size_t j = i + 1; j < vars.size(); ++j) + if (values[vars[i].id] == values[vars[j].id]) return false; + } + return true; +} + +std::optional oracle(const ModelSnapshot& model) { + std::vector values(model.variables.size()); + std::optional best; + const auto visit = [&](const auto& self, std::size_t slot) -> void { + if (slot == model.variables.size()) { + if (!feasible(model, values)) return; + auto objective = static_cast(model.objective.offset); + for (const auto& term : model.objective.terms) + objective += static_cast(term.coefficient) * static_cast(values[term.variable.id]); + if (!best || (model.objective.sense == ObjectiveSense::Minimize ? objective < *best : objective > *best)) best = objective; + return; + } + const auto& variable = model.variables[slot]; + if (!variable.active) { self(self, slot + 1); return; } + if (variable.type == VariableType::SemiInteger) { values[slot] = 0; self(self, slot + 1); } + for (int value = static_cast(variable.lower); value <= variable.upper; ++value) { + values[slot] = value; self(self, slot + 1); + } + }; + visit(visit, 0); return best; +} + +std::uint64_t calls = 0, bounds = 0, tightened = 0; +std::uint64_t configurations = 0; +void check(const Model& model) { + const auto snapshot = model.snapshot(); + const auto expected = oracle(snapshot); + for (auto frequency : {NativeLpFrequency::Root, NativeLpFrequency::AfterBoundChanges}) + for (bool filtering : {false, true}) + for (unsigned interval : {1U, 3U}) for (bool covers : {false,true}) { + ++configurations; + NativeLpOptions options; options.solve.guarantee = Guarantee::Exact; + options.frequency = frequency; options.bound_tightening = filtering; + options.bound_change_interval = interval; + if(covers) options.root_cover_cuts=NativeRootCoverSettings{}; + const auto solved = solve_native_lp(snapshot, options); + const auto& result = solved.result; + if (result.termination != (expected ? Termination::Optimal : Termination::Infeasible)) { + std::cerr << to_string(result.termination) << ": " << result.message << '\n'; assert(false); + } + assert(result.model_id == model.id() && result.revision == model.revision()); + assert(result.guarantee == Guarantee::Exact && result.elapsed_seconds >= 0); + assert(result.backend == "Gecode native + checked LP"); + assert(result.backend_version.find("HiGHS") != std::string::npos); + assert(result.has_solution() == expected.has_value()); + if (expected) { + assert(result.objective == static_cast(*expected)); + assert(result.best_bound == result.objective && result.absolute_gap == 0); + assert(feasible(snapshot, result.values)); + for (const auto& variable : snapshot.variables) { + if (!variable.active) { assert(!result.active_variables[variable.variable.id]); continue; } + const auto value = result.value(variable.variable); + assert(value == std::trunc(value)); + assert((variable.type == VariableType::SemiInteger && value == 0) || + (value >= variable.lower && value <= variable.upper)); + } + } else assert(!result.objective && !result.best_bound && !result.solution_validated); + calls += solved.relaxation.lp_calls; bounds += solved.relaxation.valid_bounds; + tightened += solved.relaxation.variable_bound_tightenings; + const auto& root=solved.relaxation.root_cover; + assert(root.requested==covers && root.rounds<=4 && root.cuts<=64); + assert(solved.relaxation.lp_calls>=root.lp_calls && solved.relaxation.valid_bounds>=root.valid_bounds); + assert(solved.relaxation.rejected_bounds>=root.rejected_bounds && solved.relaxation.lp_seconds>=root.lp_seconds); + assert(root.completion!=(covers?NativeRootCoverCompletion::NotStarted:NativeRootCoverCompletion::NoNewCuts)); + if(!covers) assert(root.completion==NativeRootCoverCompletion::NotRequested && !root.lp_calls); + } +} + +void enumerated_models() { + for (int i = 0; i < 32; ++i) { + Model model; + const auto gone = model.add_integer(-1, 1); model.remove(gone); + const auto x = model.add_integer(-3, 3), y = model.add_integer(-2, 2), b = model.add_binary(); + model.add_row({{x, i % 5 - 2.0}, {y, i % 3 - 1.0}, {b, 2}}, -2, 4); + model.add_row({{x, -1}, {y, 1}}, -inf, i % 4 - 1.0); + const auto dead = model.add_row({{x, 1}}, -inf, 0); model.remove(dead); + model.set_objective({{x, i % 7 - 3.0}, {y, -2}, {b, 3}}, + i % 2 ? ObjectiveSense::Maximize : ObjectiveSense::Minimize, i - 20); + check(model); + } + for (bool activation : {false, true}) for (bool maximum : {false, true}) { + Model model; + const auto b = model.add_binary(), x = model.add_integer(-2, 3); + const auto semi = model.add_variable(VariableType::SemiInteger, 2, 4); + const auto indicator = add_indicator(model, b, activation, {{x, 2}, {semi, -1}}, -2, 1); + assert(indicator.inactive_gate); + model.add_row({{*indicator.inactive_gate, 1}, {semi, 1}}, -inf, 4); + add_all_different(model, {x, semi}); + model.set_objective({{x, -2}, {semi, 3}, {*indicator.inactive_gate, 1}}, + maximum ? ObjectiveSense::Maximize : ObjectiveSense::Minimize, -7); + check(model); + } + Model empty; empty.minimize({}, -9); check(empty); + empty.add_row({}, 1, inf); check(empty); + Model no_cost; no_cost.add_integer(-2, 2); check(no_cost); + Model coupled; + const auto x = coupled.add_integer(0, 10), y = coupled.add_integer(0, 10); + coupled.add_row({{x, 2}, {y, 3}}, 7, inf); coupled.minimize({{x, 1}, {y, 1}}, 8); + check(coupled); + for(bool activation:{false,true}) for(bool maximum:{false,true}) { + Model hybrid;const auto x=hybrid.add_binary(),y=hybrid.add_binary(),b=hybrid.add_binary(); + const auto s=hybrid.add_variable(VariableType::SemiInteger,2,3); + hybrid.add_row({{x,3},{y,3}},-inf,5); + add_all_different(hybrid,{x,y}); + add_indicator(hybrid,b,activation,{{s,1},{x,1}},2,inf); + hybrid.set_objective({{x,maximum?2.0:-2.0},{y,maximum?2.0:-2.0},{s,maximum?-1.0:1.0}}, + maximum?ObjectiveSense::Maximize:ObjectiveSense::Minimize,maximum?-17:17); + check(hybrid); + } + assert(calls > 0 && bounds > 0 && tightened > 0); +} + +void limits_and_rejections() { + Model model; const auto x = model.add_integer(0, 100), y = model.add_integer(0, 100); + model.add_row({{x, 2}, {y, 3}}, 7, inf); model.minimize({{x, 1}, {y, 1}}); + NativeLpOptions options; + options.solve.time_limit_seconds = 0; + auto result = solve_native_lp(model, options); + assert(result.result.termination == Termination::TimeLimit && result.relaxation.lp_calls == 0); + options = {}; options.solve.cancellation = std::make_shared(); options.solve.cancellation->cancel(); + result = solve_native_lp(model.snapshot(), options); + assert(result.result.termination == Termination::Cancelled && result.relaxation.lp_calls == 0); + options = {}; options.solve.node_limit = 0; + result = solve_native_lp(model, options); + assert(result.result.termination == Termination::NodeLimit && !result.result.best_bound); + options = {}; options.solve.node_limit = 1; + result = solve_native_lp(model, options); + assert(result.result.termination == Termination::NodeLimit && !result.result.best_bound); + options = {}; options.solve.guarantee = Guarantee::Certified; + assert(solve_native_lp(model, options).result.termination == Termination::Unsupported); + options = {}; options.solve.backend = Backend::Highs; + assert(solve_native_lp(model, options).result.termination == Termination::Unsupported); + options = {}; options.solve.threads = 2; + assert(solve_native_lp(model, options).result.termination == Termination::Unsupported); + options = {}; options.solve.primal_start = {{x, 3}}; + assert(solve_native_lp(model, options).result.termination == Termination::Unsupported); + options = {}; options.bound_change_interval = 0; + assert(solve_native_lp(model, options).result.termination == Termination::InvalidModel); + options = {}; options.frequency = static_cast(-1); + assert(solve_native_lp(model, options).result.termination == Termination::InvalidModel); + auto malformed = model.snapshot(); malformed.rows[0].terms[0].variable.model_id++; + assert(solve_native_lp(malformed).result.termination == Termination::InvalidModel); + Model continuous; continuous.add_continuous(0, 1); + assert(solve_native_lp(continuous).result.termination == Termination::Unsupported); + Model fractional; auto variable = fractional.add_integer(0, 1); fractional.minimize({{variable, .5}}); + assert(solve_native_lp(fractional).result.termination == Termination::Unsupported); + Model scale; variable = scale.add_integer(0, 0); scale.add_row({{variable, 1000000001}}, -inf, 0); + assert(solve_native(scale).termination == Termination::Optimal); + assert(solve_native_lp(scale).result.termination == Termination::Unsupported); + auto old = solve_native_lp(model); const auto revision = old.result.revision; + model.set_bounds(x, 5, 10); + auto changed = solve_native_lp(model); + assert(old.result.revision == revision && changed.result.revision != revision); + assert(changed.result.objective == 5 && old.result.objective == 3); + assert(solve_native(model).backend == "Gecode native"); // unchanged native route +} + +Model cover_model() { + Model model;const auto x=model.add_binary(),y=model.add_binary(); + model.add_row({{x,3},{y,3}},-inf,5);model.minimize({{x,-2},{y,-2}},17);return model; +} +void root_cover_limits() { + auto model=cover_model();const auto snapshot=model.snapshot(); + NativeLpOptions options;options.solve.guarantee=Guarantee::Exact; + options.root_cover_cuts=NativeRootCoverSettings{}; + const auto full=solve_native_lp(model,options); + assert(full.result.termination==Termination::Optimal && full.result.objective==15); + const auto& root=full.relaxation.root_cover; + assert(root.requested && root.cuts==1 && root.nonzeros==2 && root.augmentations==1); + assert(root.lp_calls==2 && root.rounds==2 && root.completion==NativeRootCoverCompletion::NoNewCuts); + assert(full.relaxation.lp_calls>=root.lp_calls && full.relaxation.lp_calls<=root.lp_calls+1); + assert(full.relaxation.valid_bounds>=root.valid_bounds); + for(unsigned kind=0;kind<11;++kind) { + options.root_cover_cuts=NativeRootCoverSettings{};auto& limits=*options.root_cover_cuts; + auto expected=NativeRootCoverCompletion::StorageLimit; + if(kind==0){limits.max_rounds=0;expected=NativeRootCoverCompletion::RoundLimit;} + if(kind==1){limits.max_rounds=1;expected=NativeRootCoverCompletion::RoundLimit;} + if(kind==2){limits.max_work=0;expected=NativeRootCoverCompletion::WorkLimit;} + if(kind==3)limits.max_cuts=0; + if(kind==4)limits.max_cut_nonzeros=1; + if(kind==5)limits.max_model_columns=1; + if(kind==6)limits.max_model_rows=1; + if(kind==7)limits.max_model_nonzeros=3; + if(kind==8){limits.max_separation_rows=0;expected=NativeRootCoverCompletion::SeparationLimit;} + if(kind==9){limits.max_terms_per_row=1;expected=NativeRootCoverCompletion::NoNewCuts;} + if(kind==10){limits.max_work=1;expected=NativeRootCoverCompletion::WorkLimit;} + const auto partial=solve_native_lp(model,options);++configurations; + assert(partial.result.termination==Termination::Optimal && partial.result.objective==15); + assert(partial.relaxation.root_cover.completion==expected); + assert(partial.relaxation.root_cover.work<=limits.max_work); + assert(partial.relaxation.lp_calls>=partial.relaxation.root_cover.lp_calls); + if(kind==1)assert(partial.relaxation.root_cover.cuts==1 && partial.relaxation.root_cover.lp_calls==1); + else assert(partial.relaxation.root_cover.cuts==0); + assert(partial.result.model_id==snapshot.model_id && partial.result.revision==snapshot.revision); + assert(feasible(snapshot,partial.result.values)); + } + for(std::size_t work=10;work<400;work+=13) { + options.root_cover_cuts=NativeRootCoverSettings{};options.root_cover_cuts->max_work=work; + auto limited=solve_native_lp(model,options);++configurations; + assert(limited.result.termination==Termination::Optimal && limited.result.objective==15); + assert(limited.relaxation.root_cover.work<=work); + } + for(unsigned reason=0;reason<3;++reason) { + options={};options.solve.guarantee=Guarantee::Exact;options.root_cover_cuts=NativeRootCoverSettings{}; + if(reason==0)options.solve.time_limit_seconds=0; + if(reason==1)options.solve.node_limit=0; + if(reason==2){options.solve.cancellation=std::make_shared();options.solve.cancellation->cancel();} + auto stopped=solve_native_lp(model,options); + assert(stopped.result.guarantee==Guarantee::Exact && !stopped.result.has_solution() && !stopped.result.best_bound); + assert(stopped.result.termination==(reason==0?Termination::TimeLimit:reason==1?Termination::NodeLimit:Termination::Cancelled)); + assert(stopped.relaxation.root_cover.requested && stopped.relaxation.root_cover.completion==NativeRootCoverCompletion::NotStarted); + assert(!stopped.relaxation.lp_calls); + } + Model empty;empty.minimize({},-9);options={};options.root_cover_cuts=NativeRootCoverSettings{}; + auto no_hint=solve_native_lp(empty,options); + assert(no_hint.result.termination==Termination::Optimal && no_hint.result.objective==-9); + assert(no_hint.relaxation.root_cover.completion==NativeRootCoverCompletion::NoPrimalSuggestion); + empty.add_row({},1,inf);no_hint=solve_native_lp(empty,options); + assert(no_hint.result.termination==Termination::Infeasible && !no_hint.result.has_solution()); + assert(no_hint.relaxation.root_cover.completion==NativeRootCoverCompletion::NoPrimalSuggestion); + assert(model.revision()==snapshot.revision); +} + +#ifdef GECODE_NATIVE_ROOT_CUT_TEST_HOOKS +std::string root_event; +std::optional injected_completion; +std::shared_ptr root_cancellation; +int root_exception=0; +void root_failures() { + auto model=cover_model();NativeLpOptions options;options.solve.guarantee=Guarantee::Exact; + options.root_cover_cuts=NativeRootCoverSettings{}; + for(auto completion:{NativeRootCoverCompletion::NoPrimalSuggestion,NativeRootCoverCompletion::InvalidSuggestion, + NativeRootCoverCompletion::CallbackError,NativeRootCoverCompletion::BackendError,NativeRootCoverCompletion::AllocationFailure}) { + root_event="after_loop";injected_completion=completion; + auto result=solve_native_lp(model,options);++configurations; + const bool hint=completion==NativeRootCoverCompletion::NoPrimalSuggestion || completion==NativeRootCoverCompletion::InvalidSuggestion; + assert(result.result.termination==(hint?Termination::Optimal: + completion==NativeRootCoverCompletion::AllocationFailure?Termination::MemoryLimit:Termination::BackendError)); + assert(result.result.has_solution()==hint && result.result.guarantee==Guarantee::Exact); + if(hint)assert(result.result.objective==15); + else assert(!result.result.best_bound && !result.result.objective); + assert(result.relaxation.root_cover.completion==completion && result.relaxation.root_cover.cuts==1); + assert(result.relaxation.lp_calls>=result.relaxation.root_cover.lp_calls); + } + injected_completion.reset(); + for(const auto* event:{"before_loop","after_loop","prepared"})for(int mode=0;mode<3;++mode) { + root_event=event;root_exception=mode;root_cancellation=std::make_shared(); + options.solve.cancellation=root_cancellation; + const auto result=solve_native_lp(model,options);++configurations; + assert(result.result.termination==(mode==0?Termination::Cancelled:mode==1?Termination::MemoryLimit:Termination::BackendError)); + assert(!result.result.has_solution() && !result.result.best_bound && result.result.guarantee==Guarantee::Exact); + assert(result.relaxation.root_cover.requested); + } + root_event.clear();root_cancellation.reset();root_exception=0; +} +#endif +} + +#ifdef GECODE_NATIVE_ROOT_CUT_TEST_HOOKS +namespace Gecode { namespace Optimize { +void native_root_cut_test_event(const char* event,NativeRootCoverCompletion& completion) { + if(root_event!=event)return; + if(injected_completion){completion=*injected_completion;return;} + if(root_exception==1)throw std::bad_alloc(); + if(root_exception==2)throw std::runtime_error("injected root preparation failure"); + root_cancellation->cancel(); +} +}} +#endif + +int main() { + if (!native_lp_capabilities().available) { + Model model; model.add_integer(0, 1); + assert(solve_native_lp(model).result.termination == Termination::Unsupported); + assert(solve_native_lp(model.snapshot()).result.termination == Termination::Unsupported); + NativeLpOptions invalid; invalid.bound_change_interval = 0; + assert(solve_native_lp(model, invalid).result.termination == Termination::InvalidModel); + invalid={};invalid.root_cover_cuts=NativeRootCoverSettings{};invalid.root_cover_cuts->denominator=3; + assert(solve_native_lp(model,invalid).result.termination==Termination::InvalidModel); + invalid.root_cover_cuts->denominator=1048576; + const auto missing=solve_native_lp(model,invalid); + assert(missing.result.termination==Termination::Unsupported && missing.relaxation.root_cover.requested); + std::cout << "Native checked LP unavailable: explicit boundary verified\n"; return 0; + } + enumerated_models(); limits_and_rejections();root_cover_limits(); +#ifdef GECODE_NATIVE_ROOT_CUT_TEST_HOOKS + root_failures(); +#endif + // Independent solves own LP workspaces; historical results outlive them. + const auto solve_one = [] { + Model model; auto x = model.add_integer(-2, 5), y = model.add_integer(0, 5); + model.add_row({{x, 2}, {y, 3}}, 7, inf); model.minimize({{x, 1}, {y, 1}}); + NativeLpOptions options;options.root_cover_cuts=NativeRootCoverSettings{}; + return solve_native_lp(model,options); + }; + auto a = std::async(std::launch::async, solve_one), b = std::async(std::launch::async, solve_one); + const auto first = a.get(), second = b.get(); + assert(first.result.termination == Termination::Optimal && first.result.objective == 2); + assert(second.result.objective == first.result.objective && second.result.model_id != first.result.model_id); + std::cout << configurations << " native LP oracle/root-cover configurations pass; " << calls << " LP calls, " + << bounds << " checked bounds, " << tightened << " interval tightenings\n"; +} diff --git a/test/optimize/native_neighborhoods.cpp b/test/optimize/native_neighborhoods.cpp new file mode 100644 index 0000000000..80c3e8ebae --- /dev/null +++ b/test/optimize/native_neighborhoods.cpp @@ -0,0 +1,341 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace Gecode::Optimize; +namespace { +constexpr double inf=std::numeric_limits::infinity(); +std::uint64_t runs=0; +thread_local std::function*)> hook; +// Independent finite product oracle: no native compiler, propagator or common +// validator is used to compute feasible points or objective bounds. +bool feasible(const ModelSnapshot& model,const std::vector& point) { + const auto row=[&](const std::vector& terms,double lower,double upper) { + std::int64_t sum=0; + for(const auto& term:terms) sum+=static_cast(term.coefficient)*static_cast(point[term.variable.id]); + return sum>=lower && sum<=upper; + }; + for(const auto& variable:model.variables) if(variable.active) { + const auto value=point.at(variable.variable.id); + if(value!=std::trunc(value) || !std::isfinite(value)) return false; + if(!(variable.type==VariableType::SemiInteger && value==0) && (valuevariable.upper)) return false; + } + for(const auto& original:model.rows) + if(original.active && !original.indicator_origin && !row(original.terms,original.lower,original.upper)) return false; + for(const auto& indicator:model.indicators) if(indicator.active) { + const bool enabled=point[indicator.activator.id]==(indicator.active_value?1:0); + if(enabled && !row(indicator.terms,indicator.lower,indicator.upper)) return false; + if(indicator.inactive_gate && point[indicator.inactive_gate->id]!=(enabled?0:1)) return false; + } + for(const auto& global:model.globals) if(global.active) { + const bool valid=std::visit([&](const auto& data) { + using T=std::decay_t; + const auto value=[&](Variable v) {return static_cast(point[v.id]);}; + if constexpr(std::is_same_v) { + std::set seen; + for(auto v:data.variables) if(!seen.insert(value(v)).second) return false; + return true; + } else if constexpr(std::is_same_v) { + const auto index=value(data.index)-data.index_base; + return index>=0 && static_cast(index)) { + std::vector tuple;for(auto v:data.variables) tuple.push_back(value(v)); + return std::find(data.tuples.begin(),data.tuples.end(),tuple)!=data.tuples.end(); + } else if constexpr(std::is_same_v) { + std::set starts; + for(std::size_t i=0;idata.capacity) return false; + } + return true; + } else if constexpr(std::is_same_v) { + auto state=data.initial_state; + for(auto variable:data.variables){ + const auto edge=std::find_if(data.transitions.begin(),data.transitions.end(),[&](const RegularTransition& t){return t.from==state&&t.symbol==value(variable);}); + if(edge==data.transitions.end())return false; + state=edge->to; + } + return std::find(data.final_states.begin(),data.final_states.end(),state)!=data.final_states.end(); + } else { + if(data.successors.empty()) return false; + std::set seen;std::int64_t next=0; + for(std::size_t i=0;i(next)>=data.successors.size() || !seen.insert(next).second) return false; + next=value(data.successors[next])-data.index_base; + } + return next==0; + } + },global.payload); + if(!valid) return false; + } + return true; +} +std::int64_t objective(const ModelSnapshot& model,const std::vector& point) { + auto value=static_cast(model.objective.offset); + for(const auto& term:model.objective.terms) value+=static_cast(term.coefficient)*static_cast(point[term.variable.id]); + return value; +}std::vector> points(const ModelSnapshot& model) { + std::vector point(model.variables.size(),std::numeric_limits::quiet_NaN()); + std::vector> all; + const auto visit=[&](const auto& self,std::size_t slot)->void { + if(slot==model.variables.size()) {all.push_back(point);return;} + const auto& v=model.variables[slot];if(!v.active){self(self,slot+1);return;} + assert(std::isfinite(v.lower) && std::isfinite(v.upper) && v.upper-v.lower<=6); + if(v.type==VariableType::SemiInteger && v.lower>0){point[slot]=0;self(self,slot+1);} + for(std::int64_t value=static_cast(v.lower);value<=v.upper;++value){point[slot]=static_cast(value);self(self,slot+1);} + };visit(visit,0);return all; +} +std::optional oracle(const ModelSnapshot& source) { + std::optional best; + for(const auto& p:points(source))if(feasible(source,p)){ + const auto value=objective(source,p); + if(!best || (source.objective.sense==ObjectiveSense::Minimize?value<*best:value>*best))best=value; + }return best; +} +std::vector entries(const ModelSnapshot& source,const std::vector& p,bool omit_gates=false) { + std::set live; + if(omit_gates)for(const auto& i:source.indicators)if(i.active && i.inactive_gate)live.insert(i.inactive_gate->id); + std::vector out; + for(const auto& v:source.variables)if(v.active && !live.count(v.variable.id))out.push_back({v.variable,p[v.variable.id]}); + return out; +} + +NativeNeighborhoodOptions options() { + NativeNeighborhoodOptions out; + out.search.solve.backend=Backend::Native;out.search.solve.guarantee=Guarantee::Exact; + out.neighborhood.time_limit_seconds=30; + return out; +} +void verify(const ModelSnapshot& source,const NativeNeighborhoodOptions& options, + const NativeNeighborhoodResult& answer) { + ++runs;const auto best=oracle(source);const auto& r=answer.search.result; + const bool minimize=source.objective.sense==ObjectiveSense::Minimize; + assert(r.model_id==source.model_id && r.revision==source.revision); + assert(r.guarantee==options.search.solve.guarantee); + if(r.has_solution()) { + assert(best && feasible(source,r.values));assert(r.objective==static_cast(objective(source,r.values))); + assert(minimize?*r.objective>=*best:*r.objective<=*best); + } + if(r.best_bound && best) assert(minimize?*r.best_bound<=*best:*r.best_bound>=*best); + if(r.termination==Termination::Optimal) { + assert(best && r.has_solution() && r.objective==static_cast(*best) && r.best_bound==r.objective); + } + if(r.termination==Termination::Infeasible) assert(!best && !r.has_solution()); + const auto& n=answer.neighborhood; + assert(n.requested && n.attempts<=1 && n.accepted_improvements<=1); + assert(n.completed_status_calls<=n.status_attempts && n.status_attempts<=options.neighborhood.max_status_calls); + assert(n.source_entries<=options.neighborhood.max_source_entries); + assert(n.coordinator_work<=options.neighborhood.max_coordinator_work); + assert(n.peak_local_spaces<=options.neighborhood.max_local_spaces); + assert(n.peak_total_spaces<=options.search.max_open_nodes); + assert(n.budget_nodes==answer.search.frontier.admitted_nodes+answer.search.branching.probe_status_calls+n.status_attempts); + assert(n.budget_nodes==answer.search.branching.budget_nodes); + if(options.search.solve.node_limit) assert(n.budget_nodes<=*options.search.solve.node_limit); + if(n.accepted_improvements) assert(r.has_solution()); +} +NativeNeighborhoodResult run(const ModelSnapshot& model,const NativeNeighborhoodOptions& o) { + auto result=solve_native_neighborhoods(model,o);verify(model,o,result);return result; +} +std::vector worst(const ModelSnapshot& model) { + std::vector out; + for(auto p:points(model)) if(feasible(model,p) && (out.empty() || + (model.objective.sense==ObjectiveSense::Minimize?objective(model,p)>objective(model,out):objective(model,p)(radius); + if(start && !bad.empty()) o.search.solve.primal_start=entries(source,bad,true); + auto result=run(source,o);assert(result.search.result.termination==Termination::Optimal || result.search.result.termination==Termination::Infeasible); + for(std::uint64_t quota=0;quota<14;++quota) { + o.search.solve.node_limit=quota;run(source,o); + } + } +} +Model outside(bool table=false,bool maximize=false) { + Model model;auto x=model.add_binary(),y=model.add_binary(),z=model.add_binary(); + if(table) add_table(model,{y,z},{{0,0},{1,0},{1,1}}); + else model.add_row({{z,1},{y,-1}},-inf,0); + if(maximize) model.maximize({{x,-100},{y,20},{z,-1}},17); + else model.minimize({{x,100},{y,-20},{z,1}},-9); + return model; +} +void models() { + auto m=outside();exhaustive(m); + auto t=outside(true,true);exhaustive(t); + {Model m;auto x=m.add_binary(),y=m.add_binary(),a=m.add_integer(-2,2); + auto s=m.add_variable(VariableType::SemiInteger,2,3);m.add_row({{x,1},{y,1},{a,1},{s,1}},0,3); + m.minimize({{a,2},{s,-1},{x,3},{y,-2}},11);exhaustive(m);} + {Model m;auto x=m.add_binary(),y=m.add_binary(),z=m.add_binary();m.add_row({{x,1},{y,-1}},0,0); + m.maximize({{x,2},{y,1},{z,-1}});exhaustive(m);} + for(bool activation:{false,true}) { + Model m;auto x=m.add_binary(),y=m.add_binary(),z=m.add_integer(-1,2); + add_indicator(m,x,activation,{{z,1}},0,1);m.maximize({{x,2},{y,3},{z,-1}},-4);exhaustive(m); + } + {Model m;auto x=m.add_binary(),y=m.add_binary(),z=m.add_binary(); + auto indicator=add_indicator(m,x,true,{{z,1}},-inf,0);auto snap=m.snapshot(); + const auto gate=snap.indicators[indicator.indicator.id].inactive_gate;assert(gate);remove_indicator(m,indicator.indicator); + m.minimize({{x,2},{y,-1},{z,-1},{*gate,-1}});exhaustive(m);} + {Model m;auto x=m.add_binary(),y=m.add_binary(),z=m.add_integer(0,2); + add_all_different(m,{x,z});m.minimize({{x,3},{y,1},{z,-1}});exhaustive(m);} + {Model m;auto x=m.add_binary(),y=m.add_binary(),z=m.add_binary(),a=m.add_integer(0,1); + add_element(m,a,{x,y},z);m.minimize({{x,2},{y,-2},{z,3}});exhaustive(m);} + {Model m;auto x=m.add_binary(),y=m.add_binary(),a=m.add_integer(3,5),b=m.add_integer(3,5),c=m.add_integer(3,5); + add_circuit(m,{a,b,c},3);m.minimize({{x,5},{y,-4},{a,1},{b,-1}});exhaustive(m);} + {Model m;auto x=m.add_binary(),y=m.add_binary(),a=m.add_integer(-1,1),b=m.add_integer(-1,1); + add_cumulative(m,{a,b,a},{1,1,0},{1,1,8},1);m.minimize({{x,3},{y,-2},{a,2},{b,-1}});exhaustive(m);} + {Model m;auto x=m.add_binary(),y=m.add_binary(),z=m.add_binary(); + add_regular(m,{x,y,x},2,0,{{0,0,0},{0,1,1},{1,0,1},{1,1,0}},{1}); + add_regular(m,{},100000000000ULL,7,{}, {7});m.maximize({{x,3},{y,4},{z,-1}});exhaustive(m);} + {Model m;auto x=m.add_binary(),y=m.add_binary(),z=m.add_binary(); + add_regular(m,{x,y},2,0,{{0,1,1}},{1});m.maximize({{z,1}});exhaustive(m);} + {Model m;auto x=m.add_binary(),dead=m.add_binary(),y=m.add_binary();m.remove(dead); + m.minimize({{x,2},{y,-3}},-9007199254740980.0);exhaustive(m);} +} +void limits_and_skips() { + auto m=outside();const auto source=m.snapshot();auto o=options();o.search.order=NativeSearchOrder::DepthFirst; + const auto full=run(source,o);assert(full.neighborhood.accepted_improvements==1); + for(std::size_t limit:{0U,1U,2U,3U,5U,10U,40U}) { + auto a=o;a.neighborhood.max_coordinator_work=limit;run(source,a); + a=o;a.neighborhood.max_source_entries=limit;run(source,a); + a=o;a.neighborhood.max_local_spaces=limit;run(source,a); + a=o;a.neighborhood.max_status_calls=limit;run(source,a); + a=o;a.search.max_open_nodes=limit;run(source,a); + } + {auto a=o;a.neighborhood.time_limit_seconds=0;auto r=run(source,a);assert(!r.neighborhood.attempts && r.neighborhood.completion==NativeNeighborhoodCompletion::LocalTimeLimit);} + {auto a=o;a.neighborhood.max_distance_variables=2;auto r=run(source,a);assert(!r.neighborhood.attempts && r.neighborhood.completion==NativeNeighborhoodCompletion::FormulationLimit);} + {auto a=o;a.neighborhood.radius=std::numeric_limits::max();auto r=run(source,a);assert(!r.neighborhood.attempts && r.neighborhood.completion==NativeNeighborhoodCompletion::NonrestrictingRadius);} + {Model pure;auto x=pure.add_integer(-2,2),y=pure.add_integer(-2,2);pure.add_row({{x,1},{y,1}},0,3);pure.minimize({{x,2},{y,-1}}); + auto r=run(pure.snapshot(),o);assert(!r.neighborhood.attempts);} + {Model fixed;auto x=fixed.add_binary(),y=fixed.add_binary();fixed.set_bounds(x,0,0);fixed.minimize({{y,1}});run(fixed.snapshot(),o);} + {Model constant;constant.add_binary();constant.add_binary();constant.minimize({},17);auto r=run(constant.snapshot(),o);assert(!r.neighborhood.attempts);} + {auto a=o;a.search.solve.node_limit=0;auto r=run(source,a);assert(r.search.result.termination==Termination::NodeLimit && !r.neighborhood.attempts);} + {auto a=o;a.search.solve.time_limit_seconds=0;auto r=run(source,a);assert(r.search.result.termination==Termination::TimeLimit && !r.neighborhood.attempts);} + for(int lp=0;lp<2;++lp) { + if(lp && !native_lp_capabilities().available) continue; + for(std::uint64_t quota=0;quota<50;++quota) { + auto a=o;a.search.solve.node_limit=quota;a.search.branching=NativeBranchingSettings{}; + if(lp) {a.search.relaxation=NativeLpSettings{};a.search.relaxation->root_cover_cuts=NativeRootCoverSettings{};} + run(source,a); + } + } +} +void bad_inputs() { + Model m;auto x=m.add_binary(),y=m.add_binary();m.minimize({{x,1},{y,-1}}); + auto o=options();o.neighborhood.policy=static_cast(73); + assert(solve_native_neighborhoods(m,o).search.result.termination==Termination::InvalidModel); + o=options();o.neighborhood.time_limit_seconds=inf; + assert(solve_native_neighborhoods(m,o).search.result.termination==Termination::InvalidModel); + auto source=m.snapshot();source.variables[0].variable.model_id++; + assert(solve_native_neighborhoods(source,options()).search.result.termination==Termination::InvalidModel); + source=m.snapshot();source.objective.terms[0].coefficient=0.5; + assert(solve_native_neighborhoods(source,options()).search.result.termination==Termination::Unsupported); + source=m.snapshot();source.variables[0].upper=inf; + assert(solve_native_neighborhoods(source,options()).search.result.termination!=Termination::Infeasible); + Model moved=std::move(m);assert(solve_native_neighborhoods(m,options()).search.result.termination==Termination::InvalidModel); + o=options();o.search.solve.guarantee=Guarantee::Certified; + assert(solve_native_neighborhoods(moved,o).search.result.termination==Termination::Unsupported); + o=options();o.search.solve.backend=Backend::Highs; + assert(solve_native_neighborhoods(moved,o).search.result.termination==Termination::Unsupported); +} +#ifdef GECODE_NATIVE_NEIGHBORHOOD_TEST_HOOKS +void faults() { + auto m=outside(true);auto source=m.snapshot();auto o=options();o.search.order=NativeSearchOrder::DepthFirst; + std::vector events; + hook=[&](const char* event,std::size_t,int,int,std::vector*) {events.emplace_back(event);}; + auto full=run(source,o);hook={};assert(full.neighborhood.accepted_improvements==1); + bool outside_parent=false,publication=false;int parent_x=-1; + hook=[&](const char* event,std::size_t slot,int lower,int,std::vector* values) { + if(std::string(event)=="neighborhood_parent_variable" && slot==0) parent_x=lower; + if(std::string(event)=="before_neighborhood_validation") outside_parent=parent_x==1 && (*values)[0]==0; + if(std::string(event)=="after_neighborhood_publication") publication=true; + }; + run(source,o);hook={};assert(outside_parent && publication); + // Every actual hook position is a cancellation boundary. Counts need not + // represent executed propagation; the independent full optimum checks proof. + for(std::size_t stop=0;stop();std::size_t count=0; + hook=[&](const char*,std::size_t,int,int,std::vector*) {if(count++==stop)a.search.solve.cancellation->cancel();}; + auto r=run(source,a);hook={};assert(r.search.result.termination==Termination::Cancelled); + } + for(const std::string event:{"neighborhood_root_alloc","neighborhood_constructed","neighborhood_choice", + "neighborhood_child_clone","before_neighborhood_validation","after_neighborhood_release", + "before_neighborhood_publication","after_neighborhood_publication"}) { + bool fired=false; + hook=[&](const char* at,std::size_t,int,int,std::vector*) {if(!fired && event==at){fired=true;throw std::bad_alloc();}}; + auto r=run(source,o);hook={};assert(fired && r.search.result.termination==Termination::MemoryLimit); + if(event=="after_neighborhood_publication") assert(r.neighborhood.accepted_improvements==1); + if(event=="before_neighborhood_publication") assert(!r.neighborhood.accepted_improvements); + } + {bool fired=false;hook=[&](const char* event,std::size_t,int,int,std::vector* values) { + if(!fired && std::string(event)=="before_neighborhood_validation") {fired=true;(*values)[0]=1-1e-12;} + };auto r=run(source,o);hook={};assert(fired && r.search.result.termination==Termination::BackendError && !r.neighborhood.accepted_improvements);} + for(const std::string event:{"neighborhood_construction","before_neighborhood_status","after_neighborhood_validation", + "after_neighborhood_release","before_neighborhood_publication"}) { + auto a=o;a.neighborhood.time_limit_seconds=0.02;bool fired=false; + hook=[&](const char* at,std::size_t,int,int,std::vector*) { + if(!fired && event==at){fired=true;std::this_thread::sleep_for(std::chrono::milliseconds(30));} + }; + auto r=run(source,a);hook={};assert(fired && r.neighborhood.completion==NativeNeighborhoodCompletion::LocalTimeLimit && !r.neighborhood.accepted_improvements); + assert(r.search.result.termination==Termination::Optimal); + } + for(const std::string event:{"before_neighborhood_publication","after_neighborhood_publication"}) { + auto a=o;a.search.solve.time_limit_seconds=0.02;bool fired=false; + hook=[&](const char* at,std::size_t,int,int,std::vector*) { + if(!fired && event==at){fired=true;std::this_thread::sleep_for(std::chrono::milliseconds(30));} + }; + auto r=run(source,a);hook={};assert(fired && r.search.result.termination==Termination::TimeLimit); + assert(r.neighborhood.accepted_improvements==(event=="after_neighborhood_publication"?1U:0U)); + assert(r.neighborhood.elapsed_seconds>=0.03); + } + // Radius zero fixes just Binary slots. Its first local status fixes the + // improving general integer through the strict objective cutoff. + {Model z;auto x=z.add_binary(),y=z.add_binary(),a=z.add_integer(0,1);z.maximize({{a,1}}); + auto p=options();p.search.solve.primal_start={{x,0},{y,0},{a,0}};p.neighborhood.radius=0;p.neighborhood.max_status_calls=1; + auto r=run(z.snapshot(),p);assert(r.neighborhood.status_attempts==1 && r.neighborhood.accepted_improvements==1);} +} +#endif +} // namespace +namespace Gecode { namespace Optimize { +#ifdef GECODE_NATIVE_NEIGHBORHOOD_TEST_HOOKS +void native_neighborhood_test_event(const char* event,std::size_t slot,int lower,int upper,std::vector* values) { + if(hook) hook(event,slot,lower,upper,values); +} +#endif +}} +int main() { + if(!native_capabilities().available) { + Model m;m.add_binary();auto r=solve_native_neighborhoods(m,options()); + assert(r.search.result.termination==Termination::Unsupported && r.neighborhood.requested); + std::cout<<"Native neighborhoods disabled-backend contract passed\n";return 0; + } + models();limits_and_skips();bad_inputs(); +#ifdef GECODE_NATIVE_NEIGHBORHOOD_TEST_HOOKS + faults(); +#endif + const auto model=outside().snapshot(); + auto a=std::async(std::launch::async,[&]{return solve_native_neighborhoods(model,options());}); + auto b=std::async(std::launch::async,[&]{return solve_native_neighborhoods(model,options());}); + verify(model,options(),a.get());verify(model,options(),b.get()); + std::cout<<"Native neighborhoods passed "< +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace Gecode::Optimize; +namespace { +constexpr double inf=std::numeric_limits::infinity(); + +// Independent finite-product oracle: no Optimize solve/compiler/validator is +// used to decide feasibility, objective values or the proof status below. +SolveResult oracle(const ModelSnapshot& model) { + SolveResult result; + result.model_id=model.model_id; result.revision=model.revision; + result.guarantee=Guarantee::Exact; result.backend="Gecode native test oracle"; + result.message="Independent finite product exhausted"; + std::vector point(model.variables.size(),std::numeric_limits::quiet_NaN()); + const auto visit=[&](const auto& self,std::size_t slot)->void { + if (slot(variable.lower); value<=variable.upper; ++value) { + point[slot]=value; self(self,slot+1); + } + return; + } + for (const auto& row:model.rows) if (row.active) { + std::int64_t sum=0; + for (const auto& term:row.terms) + sum+=static_cast(term.coefficient)*static_cast(point[term.variable.id]); + if (sumrow.upper) return; + } + auto value=static_cast(model.objective.offset); + for (const auto& term:model.objective.terms) + value+=static_cast(term.coefficient)*static_cast(point[term.variable.id]); + if (!result.objective || (model.objective.sense==ObjectiveSense::Minimize ? value<*result.objective : value>*result.objective)) { + result.objective=static_cast(value); result.values=point; + } + }; + visit(visit,0); + if (result.objective) { + for (const auto& variable:model.variables) result.active_variables.push_back(variable.active); + result.solution_validated=true; result.termination=Termination::Optimal; + result.best_bound=result.objective; result.update_gaps(model.objective.sense); + } else result.termination=Termination::Infeasible; + return result; +} + +Model reduced_fixture(bool maximum=false) { + Model model; + const auto dead=model.add_integer(-1,1); model.remove(dead); + const auto fixed=model.add_integer(2,2), x=model.add_integer(-2,2); + model.add_row({{fixed,3},{x,-2}},4,8); + model.add_row({{x,1}},-2,2); // A redundant original row. + model.set_objective({{fixed,-3},{x,2}},maximum?ObjectiveSense::Maximize:ObjectiveSense::Minimize,11); + return model; +} + +Model auxiliary_fixture(bool maximize,bool bounded,int sign=1) { + Model model;std::vector capacity,equality; + for(int i=0;i<6;++i){const auto x=model.add_binary();capacity.push_back({x,double(i+1)}); + equality.push_back({x,double((maximize?-1:1)*sign*(i*3+1))});} + model.add_row(capacity,-inf,10); + const auto auxiliary=model.add_integer(maximize?3:(bounded?-22:-48),maximize?(bounded?28:54):3); + equality.push_back({auxiliary,double(sign)});model.add_row(equality,3*sign,3*sign); + model.set_objective({{auxiliary,1}},maximize?ObjectiveSense::Maximize:ObjectiveSense::Minimize,-11); + return model; +} + +void objective_auxiliaries() { + SolveOptions options;options.backend=Backend::Native;options.guarantee=Guarantee::Exact; + options.relative_gap=options.absolute_gap=0;options.time_limit_seconds=5; + for(bool maximize:{false,true})for(bool bounded:{false,true})for(int sign:{-1,1}) { + const auto model=auxiliary_fixture(maximize,bounded,sign);const auto source=model.snapshot(); + const auto expected=oracle(source); + for(bool interrupted:{false,true}) { + auto limited=options;limited.node_limit=2;SolveBudget budget(limited);bool called=false; + const auto actual=Detail::native_objective_auxiliary(source,limited,budget, + [&](const ModelSnapshot& reduced,const SolveOptions& exact,SolveBudget& shared) { + called=true;assert(&shared==&budget && exact.guarantee==Guarantee::Exact); + assert(!reduced.variables.back().active && reduced.objective.terms.size()==6); + assert(reduced.rows[1].active==bounded); // Preserve nonredundant auxiliary domain bounds. + auto result=oracle(reduced);shared.add_nodes(2); + if(interrupted){result.termination=Termination::NodeLimit; + result.best_bound=*result.objective+(maximize?3:-3);} + return result; + }); + assert(called && actual && actual->model_id==source.model_id && actual->revision==source.revision); + assert(actual->has_solution() && actual->objective==expected.objective && budget.nodes()==2); + assert(actual->active_variables.back() && actual->values.back()==*actual->objective+11); + assert(validate(source,actual->values,0,0).valid); + assert(actual->termination==(interrupted?Termination::NodeLimit:Termination::Optimal)); + assert(actual->best_bound==*expected.objective+(interrupted?(maximize?3:-3):0)); + } + if(native_capabilities().available)for(bool enabled:{false,true}) { + NativeAutoOptions configured;configured.solve=options;configured.settings={enabled,false,false,true}; + auto actual=solve_native_auto_configured(source,configured); + if(actual.termination!=Termination::Optimal)std::cerr< SolveResult {assert(false);return {};}; + // A second use of the auxiliary or a non-unit equality coefficient must keep + // the original model. No unsupported affine substitution is approximated. + model.add_row({{original.variables.back().variable,1}},3,54); + SolveBudget repeated(options);assert(!Detail::native_objective_auxiliary(model.snapshot(),options,repeated,never)); + auto nonunit=original;for(auto& term:nonunit.rows[1].terms)term.coefficient*=2; + nonunit.rows[1].lower*=2;nonunit.rows[1].upper*=2; + SolveBudget nonunit_budget(options);assert(!Detail::native_objective_auxiliary(nonunit,options,nonunit_budget,never)); + auto started=options;const auto witness=oracle(original); + for(const auto& v:original.variables)started.primal_start.push_back({v.variable,witness.values[v.variable.id]}); + SolveBudget start_budget(started);assert(!Detail::native_objective_auxiliary(original,started,start_budget,never)); + auto cancelled=options;cancelled.cancellation=std::make_shared(); + SolveBudget cancel_budget(cancelled); + const auto stopped=Detail::native_objective_auxiliary(original,cancelled,cancel_budget, + [&](const ModelSnapshot& reduced,const SolveOptions&,SolveBudget&){ + auto result=oracle(reduced);cancelled.cancellation->cancel();return result; + }); + assert(stopped && stopped->termination==Termination::Cancelled && !stopped->has_solution() && !stopped->best_bound); + SolveBudget foreign_budget(options); + const auto foreign=Detail::native_objective_auxiliary(original,options,foreign_budget, + [&](const ModelSnapshot& reduced,const SolveOptions&,SolveBudget&){auto result=oracle(reduced);++result.model_id;return result;}); + assert(foreign && foreign->termination==Termination::BackendError && !foreign->has_solution()); + SolveBudget mask_budget(options); + const auto mask=Detail::native_objective_auxiliary(original,options,mask_budget, + [&](const ModelSnapshot& reduced,const SolveOptions&,SolveBudget&){ + auto result=oracle(reduced);result.active_variables[0]=false;return result; + }); + assert(mask && mask->termination==Termination::BackendError && !mask->has_solution()); + SolveBudget unsupported_budget(options); + const auto unsupported=Detail::native_objective_auxiliary(original,options,unsupported_budget, + [&](const ModelSnapshot& reduced,const SolveOptions&,SolveBudget&){ + SolveResult result;result.model_id=reduced.model_id;result.revision=reduced.revision; + result.guarantee=Guarantee::Exact;result.termination=Termination::Unsupported;return result; + }); + assert(!unsupported); // The caller retains the originally admitted native route. +} + +void fixed_offsets_and_bounds() { + for (bool maximum:{false,true}) for (auto guarantee:{Guarantee::Exact,Guarantee::Numerical}) { + const auto model=reduced_fixture(maximum); const auto source=model.snapshot(); + const auto expected=oracle(source); + for (bool interrupted:{false,true}) { + SolveOptions options; options.backend=Backend::Native; options.guarantee=guarantee; + options.node_limit=2; + SolveBudget budget(options); std::size_t calls=0; + auto answer=Detail::native_presolve(source,options,budget, + [&](const ModelSnapshot& reduced,const SolveOptions& exact,SolveBudget& shared) { + ++calls; assert(&shared==&budget && exact.guarantee==Guarantee::Exact); + assert(reduced.model_id!=source.model_id && reduced.variables.size()guarantee==guarantee); + assert(answer->model_id==source.model_id && answer->revision==source.revision); + assert(answer->has_solution() && answer->objective==expected.objective); + assert(answer->values.size()==source.variables.size()); + assert(!answer->active_variables[0] && std::isnan(answer->values[0])); + assert(answer->values[1]==2 && answer->values[2]==(maximum?1:-1)); + assert(!answer->start_submitted && budget.nodes()==2); + assert(answer->termination==(interrupted?Termination::NodeLimit:Termination::Optimal)); + assert(answer->best_bound==*expected.objective+(interrupted?(maximum?3:-3):0)); + assert(answer->absolute_gap==(interrupted?3:0)); + } + if (native_capabilities().available) { + SolveOptions options; options.backend=Backend::Native; options.guarantee=guarantee; + const auto actual=solve_native_auto(source,options); + assert(actual.termination==Termination::Optimal && actual.objective==expected.objective); + assert(actual.model_id==source.model_id && actual.revision==source.revision); + assert(actual.guarantee==guarantee && actual.has_solution() && actual.best_bound==actual.objective); + } + } +} + +void infeasibility_and_fixpoint() { + SolveOptions options; options.backend=Backend::Native; + for (bool interval:{false,true}) { + Model model; const auto fixed=model.add_integer(2,2); + const auto x=model.add_binary(),y=model.add_binary(),z=model.add_binary(); + if (interval) model.add_row({{x,1}},2,inf); + else model.add_row({{x,2},{y,2},{z,2}},3,3); + model.minimize({{fixed,3},{x,-2}},-11); + const auto source=model.snapshot(); assert(!oracle(source).has_solution()); + SolveBudget budget(options); std::size_t calls=0; + const auto result=Detail::native_presolve(source,options,budget, + [&](const ModelSnapshot& reduced,const SolveOptions&,SolveBudget&) { ++calls; return oracle(reduced); }); + assert(result && result->termination==Termination::Infeasible && !result->has_solution()); + assert(!result->best_bound && !result->objective && result->model_id==source.model_id); + assert(calls==(interval?0U:1U)); + } + Model unchanged; const auto x=unchanged.add_binary(),y=unchanged.add_binary(); + unchanged.add_row({{x,1},{y,1}},1,inf); + SolveBudget budget(options); bool called=false; + const auto result=Detail::native_presolve(unchanged.snapshot(),options,budget, + [&](const ModelSnapshot& reduced,const SolveOptions&,SolveBudget&) { called=true; return oracle(reduced); }); + assert(!result && !called); // A presolve fixpoint is not a solved optimization. +} + +void incomplete_artifact() { + Model model; std::vector x; + for (int i=0;i<8;++i) x.push_back(model.add_binary()); + for (int i=7;i>0;--i) model.add_row({{x[i],1},{x[i-1],-1}},0,0); + model.add_row({{x[0],1}},1,1); model.minimize({{x.back(),-7}},13); + SolveOptions options; SolveBudget budget(options); + const auto result=Detail::native_presolve(model.snapshot(),options,budget, + [&](const ModelSnapshot& reduced,const SolveOptions&,SolveBudget&) { return oracle(reduced); }); + assert(result && result->termination==Termination::Optimal && result->objective==6); + assert(result->message.find("partial reduction")!=std::string::npos); + for (double value:result->values) assert(value==1); +} + +void starts_and_stops() { + const auto model=reduced_fixture(); const auto source=model.snapshot(); + SolveOptions options; options.primal_start={{source.variables[1].variable,2},{source.variables[2].variable,-1}}; + SolveBudget start_budget(options); bool called=false; + const auto skipped=Detail::native_presolve(source,options,start_budget, + [&](const ModelSnapshot& reduced,const SolveOptions&,SolveBudget&) { called=true; return oracle(reduced); }); + assert(!skipped && !called); + options={}; options.time_limit_seconds=0; SolveBudget zero_budget(options); + auto stopped=Detail::native_presolve(source,options,zero_budget, + [&](const ModelSnapshot& reduced,const SolveOptions&,SolveBudget&) { called=true; return oracle(reduced); }); + assert(stopped && stopped->termination==Termination::TimeLimit && !called && !stopped->has_solution()); + options={}; options.cancellation=std::make_shared(); SolveBudget cancelled(options); + stopped=Detail::native_presolve(source,options,cancelled, + [&](const ModelSnapshot& reduced,const SolveOptions&,SolveBudget& shared) { + called=true; auto solved=oracle(reduced); shared.cancellation()->cancel(); return solved; + }); + assert(called && stopped && stopped->termination==Termination::Cancelled); + assert(!stopped->has_solution() && !stopped->best_bound && !stopped->objective); + assert(stopped->model_id==source.model_id && stopped->revision==source.revision); + options={}; SolveBudget foreign_budget(options); + const auto foreign=Detail::native_presolve(source,options,foreign_budget, + [&](const ModelSnapshot& reduced,const SolveOptions&,SolveBudget&) { + auto solved=oracle(reduced); ++solved.revision; return solved; + }); + assert(foreign && foreign->termination==Termination::BackendError && !foreign->has_solution()); +} +} + +int main() { + objective_auxiliaries(); fixed_offsets_and_bounds(); infeasibility_and_fixpoint(); incomplete_artifact(); starts_and_stops(); + std::cout<<"Exact native presolve composition: offsets, proof transfer, partial reductions, starts and stops pass\n"; +} diff --git a/test/optimize/native_race.cpp b/test/optimize/native_race.cpp new file mode 100644 index 0000000000..a19a46cf01 --- /dev/null +++ b/test/optimize/native_race.cpp @@ -0,0 +1,115 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +using namespace Gecode::Optimize; +namespace { +const double inf=std::numeric_limits::infinity(); +Model fixture(bool maximize,int seed) { + Model m;std::vector x;std::vector cost; + for(int i=0;i<12;++i){x.push_back(m.add_binary());cost.push_back({x.back(),double((i*7+seed)%17+1)});} + for(int j=0;j<5;++j){std::vector terms; + for(int i=0;i<12;++i)terms.push_back({x[i],double((i*13+j*7+seed)%11+1)}); + m.add_row(terms,20,42+j); + } + if(maximize)m.maximize(cost,-7);else m.minimize(cost,11); + return m; +} +double oracle(const ModelSnapshot& m) { + const bool minimize=m.objective.sense==ObjectiveSense::Minimize; + double best=minimize?inf:-inf; + for(unsigned mask=0;mask<(1U<>t.variable.id)&1U); + valid=valid && a>=row.lower && a<=row.upper;} + if(!valid)continue; + double value=m.objective.offset;for(auto t:m.objective.terms)value+=t.coefficient*((mask>>t.variable.id)&1U); + best=minimize?std::min(best,value):std::max(best,value); + } + return best; +} +void check(const ModelSnapshot& m,const SolveResult& r,double expected) { + assert(r.model_id==m.model_id && r.revision==m.revision); + if(r.has_solution()) { + double value=m.objective.offset; + for(auto t:m.objective.terms)value+=t.coefficient*r.values[t.variable.id]; + assert(r.objective==value); + for(double x:r.values)assert(x==0 || x==1); + for(const auto& row:m.rows){double a=0;for(auto t:row.terms)a+=t.coefficient*r.values[t.variable.id]; + assert(a>=row.lower && a<=row.upper);} + } + if(r.best_bound)assert(m.objective.sense==ObjectiveSense::Minimize ? *r.best_bound<=expected:*r.best_bound>=expected); + if(r.termination==Termination::Optimal)assert(r.has_solution() && r.objective==expected && r.best_bound==expected); +} +} +int main(){ + NativeRaceOptions o;o.solve.backend=Backend::Native;o.solve.guarantee=Guarantee::Exact; + o.solve.relative_gap=o.solve.absolute_gap=0;o.solve.time_limit_seconds=5; + o.exploration_seconds=1;o.probe_node_limit=1; + if(!native_capabilities().available){auto m=fixture(false,0); + o.automatic={false,false,false,false}; + assert(solve_native_race(m,o).termination==Termination::Unsupported); + o.probe_node_limit=0; + assert(solve_native_race(m,o).termination==Termination::InvalidModel);return 0;} + bool saw_two=false; + for(bool maximize:{false,true})for(int seed=0;seed<4;++seed){ + auto m=fixture(maximize,seed);auto s=m.snapshot();auto optimum=oracle(s); + auto result=solve_native_race(m,o);check(s,result,optimum); + if(result.termination!=Termination::Optimal)std::cerr<();stopped.solve.cancellation->cancel(); + assert(solve_native_race(s,stopped).termination==Termination::Cancelled); + } + assert(saw_two); + // Disabled exploration still forwards settings, rather than falling back to + // an unconfigured automatic solve. Isolate symmetry to observe its effect. + Model symmetric;std::vector terms; + for(int i=0;i<6;++i)terms.push_back({symmetric.add_binary(),1}); + symmetric.add_row(terms,2,4);symmetric.minimize(terms,-3); + for(bool enabled:{false,true}){ + auto configured=o;configured.exploration_seconds=0; + configured.automatic={false,false,enabled,false}; + auto result=solve_native_race(symmetric,configured);check(symmetric.snapshot(),result,-1); + assert(result.termination==Termination::Optimal && result.message.find("Native race skipped")==0); + assert((result.message.find("duplicate-column symmetry")!=std::string::npos)==enabled); + } + auto m=fixture(false,0);auto invalid=o;invalid.exploration_seconds=-1; + assert(solve_native_race(m,invalid).termination==Termination::InvalidModel); + invalid=o;invalid.probe_node_limit=0; + assert(solve_native_race(m,invalid).termination==Termination::InvalidModel); + auto s=m.snapshot();s.rows[0].terms[0].variable.id+=100; + assert(solve_native_race(s,o).termination==Termination::InvalidModel); + auto saved=std::move(m);assert(solve_native_race(m,o).termination==Termination::InvalidModel); + Model impossible;auto x=impossible.add_binary();impossible.add_row({{x,2}},1,1); + assert(solve_native_race(impossible,o).termination==Termination::Infeasible); + std::cout<<"Native sequential race: exhaustive min/max optima, shared nodes, starts and interruption passed\n"; +} diff --git a/test/optimize/native_search.cpp b/test/optimize/native_search.cpp new file mode 100644 index 0000000000..6458409e81 --- /dev/null +++ b/test/optimize/native_search.cpp @@ -0,0 +1,465 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Gecode::Optimize; +namespace { +constexpr double inf=std::numeric_limits::infinity(); +std::uint64_t runs=0; + +// Independent finite product oracle: no native compiler, propagator or common +// validator is used to compute feasible points or objective bounds. +bool feasible(const ModelSnapshot& model,const std::vector& point) { + const auto row=[&](const std::vector& terms,double lower,double upper) { + std::int64_t sum=0; + for(const auto& term:terms) sum+=static_cast(term.coefficient)*static_cast(point[term.variable.id]); + return sum>=lower && sum<=upper; + }; + for(const auto& variable:model.variables) if(variable.active) { + const auto value=point.at(variable.variable.id); + if(value!=std::trunc(value) || !std::isfinite(value)) return false; + if(!(variable.type==VariableType::SemiInteger && value==0) && (valuevariable.upper)) return false; + } + for(const auto& original:model.rows) + if(original.active && !original.indicator_origin && !row(original.terms,original.lower,original.upper)) return false; + for(const auto& indicator:model.indicators) if(indicator.active) { + const bool enabled=point[indicator.activator.id]==(indicator.active_value?1:0); + if(enabled && !row(indicator.terms,indicator.lower,indicator.upper)) return false; + if(indicator.inactive_gate && point[indicator.inactive_gate->id]!=(enabled?0:1)) return false; + } + for(const auto& global:model.globals) if(global.active) { + const bool valid=std::visit([&](const auto& data) { + using T=std::decay_t; + const auto value=[&](Variable v) {return static_cast(point[v.id]);}; + if constexpr(std::is_same_v) { + std::set seen; + for(auto v:data.variables) if(!seen.insert(value(v)).second) return false; + return true; + } else if constexpr(std::is_same_v) { + const auto index=value(data.index)-data.index_base; + return index>=0 && static_cast(index)) { + std::vector tuple;for(auto v:data.variables) tuple.push_back(value(v)); + return std::find(data.tuples.begin(),data.tuples.end(),tuple)!=data.tuples.end(); + } else if constexpr(std::is_same_v) { + std::set starts; + for(std::size_t i=0;idata.capacity) return false; + } + return true; + } else if constexpr(std::is_same_v) { + auto state=data.initial_state; + for(auto variable:data.variables){ + const auto edge=std::find_if(data.transitions.begin(),data.transitions.end(),[&](const RegularTransition& t){return t.from==state&&t.symbol==value(variable);}); + if(edge==data.transitions.end())return false; + state=edge->to; + } + return std::find(data.final_states.begin(),data.final_states.end(),state)!=data.final_states.end(); + } else { + if(data.successors.empty()) return false; + std::set seen;std::int64_t next=0; + for(std::size_t i=0;i(next)>=data.successors.size() || !seen.insert(next).second) return false; + next=value(data.successors[next])-data.index_base; + } + return next==0; + } + },global.payload); + if(!valid) return false; + } + return true; +} +std::int64_t objective(const ModelSnapshot& model,const std::vector& point) { + auto value=static_cast(model.objective.offset); + for(const auto& term:model.objective.terms) value+=static_cast(term.coefficient)*static_cast(point[term.variable.id]); + return value; +} +std::optional oracle(const ModelSnapshot& model) { + std::vector point(model.variables.size());std::optional best; + const auto visit=[&](const auto& self,std::size_t slot)->void { + if(slot==model.variables.size()) { + if(!feasible(model,point)) return; + const auto value=objective(model,point); + if(!best || (model.objective.sense==ObjectiveSense::Minimize ? value<*best:value>*best)) best=value; + return; + } + const auto& variable=model.variables[slot]; + if(!variable.active) {self(self,slot+1);return;} + if(variable.type==VariableType::SemiInteger) {point[slot]=0;self(self,slot+1);} + for(int value=static_cast(variable.lower);value<=variable.upper;++value) {point[slot]=value;self(self,slot+1);} + }; + visit(visit,0);return best; +} +void evidence(const ModelSnapshot& source,const NativeSearchOptions& options,const NativeSearchResult& solved) { + ++runs;const auto expected=oracle(source);const auto& result=solved.result; + const bool minimize=source.objective.sense==ObjectiveSense::Minimize; + assert(result.model_id==source.model_id && result.revision==source.revision); + assert(result.guarantee==options.solve.guarantee && result.elapsed_seconds>=0); + assert(solved.frontier.peak_open_nodes<=options.max_open_nodes); + const auto& root=solved.relaxation.root_cover; + assert(root.requested==bool(options.relaxation && options.relaxation->root_cover_cuts)); + assert(solved.relaxation.lp_calls>=root.lp_calls && solved.relaxation.valid_bounds>=root.valid_bounds); + assert(solved.relaxation.rejected_bounds>=root.rejected_bounds && solved.relaxation.lp_seconds>=root.lp_seconds); + if(options.solve.node_limit) assert(solved.frontier.admitted_nodes<=*options.solve.node_limit); + if(result.has_solution()) { + assert(expected && feasible(source,result.values)); + assert(result.objective==static_cast(objective(source,result.values))); + assert(minimize ? *result.objective>=*expected:*result.objective<=*expected); + for(const auto& variable:source.variables) { + assert(result.active_variables[variable.variable.id]==variable.active); + if(!variable.active) assert(std::isnan(result.values[variable.variable.id])); + } + } + if(result.best_bound) { + assert(std::isfinite(*result.best_bound)); + if(expected) assert(minimize ? *result.best_bound<=*expected:*result.best_bound>=*expected); + if(result.has_solution()) { + assert(minimize ? *result.best_bound<=*result.objective:*result.best_bound>=*result.objective); + assert(result.absolute_gap==std::fabs(*result.objective-*result.best_bound)); + } + } + if(result.termination==Termination::Optimal) { + assert(expected && result.has_solution() && result.objective==static_cast(*expected)); + assert(result.best_bound==result.objective && result.absolute_gap==0 && solved.frontier.unresolved_regions==0); + } else if(result.termination==Termination::Infeasible) { + assert(!expected && !result.has_solution() && !result.best_bound && solved.frontier.unresolved_regions==0); + } else { + assert(result.termination==Termination::NodeLimit || result.termination==Termination::MemoryLimit || + result.termination==Termination::Cancelled || result.termination==Termination::TimeLimit); + } +} +void all_limits(const Model& model) { + const auto source=model.snapshot(); + for(auto order:{NativeSearchOrder::DepthFirst,NativeSearchOrder::BestBound}) { + NativeSearchOptions options;options.order=order;options.solve.guarantee=Guarantee::Exact; + const auto full=solve_native_search(source,options);evidence(source,options,full); + assert(full.result.termination==Termination::Optimal || full.result.termination==Termination::Infeasible); + assert(full.relaxation.lp_calls==0); + for(std::uint64_t nodes=0;nodes<=full.frontier.admitted_nodes+1;++nodes) { + options.solve.node_limit=nodes; + const auto limited=solve_native_search(source,options);evidence(source,options,limited); + if(nodes(5,full.frontier.peak_open_nodes+1);++cap) { + options.max_open_nodes=cap;evidence(source,options,solve_native_search(source,options)); + } + } + assert(model.revision()==source.revision); +} +void ordinary_and_globals() { + for(int i=0;i<48;++i) { + Model model;const auto gone=model.add_binary();model.remove(gone); + const auto x=model.add_integer(-2,2),y=model.add_integer(-1,2),b=model.add_binary(); + model.add_row({{x,double(i%5-2)},{y,double(i%3-1)},{b,2}},-2,3); + model.add_row({{x,1},{y,-1}},-inf,double(i%4-2)); + model.set_objective({{x,double(i%7-3)},{y,2},{b,-3}},i%2?ObjectiveSense::Minimize:ObjectiveSense::Maximize,i-21); + all_limits(model); + } + Model empty;empty.minimize({},-9);all_limits(empty);empty.add_row({},1,inf);all_limits(empty); + Model ties;ties.add_integer(-2,2);ties.add_binary();all_limits(ties); + Model fixed;const auto f=fixed.add_integer(-2,-2);fixed.maximize({{f,-3}},9007199254740900.0);all_limits(fixed); + for(bool activation:{false,true}) { + Model model;const auto b=model.add_binary(),x=model.add_integer(-1,2),s=model.add_variable(VariableType::SemiInteger,2,3); + const auto indicator=add_indicator(model,b,activation,{{x,1},{s,1}},-inf,2); + add_all_different(model,{x,s});model.minimize({{x,2},{s,-1},{*indicator.inactive_gate,1}},-8);all_limits(model); + } + Model element;auto i=element.add_integer(3,4),x=element.add_integer(-1,1),y=element.add_integer(0,2),r=element.add_integer(-1,2); + add_element(element,i,{x,y},r,3);element.maximize({{r,2},{x,-1}},-2);all_limits(element); + Model table;x=table.add_integer(-1,1);y=table.add_integer(0,2); + add_table(table,{x,y},{{-1,2},{0,0},{1,1}});table.minimize({{x,2},{y,1}},5);all_limits(table); + Model cumulative;x=cumulative.add_integer(-1,1);y=cumulative.add_integer(0,2); + add_cumulative(cumulative,{x,y},{2,1},{1,1},1);cumulative.minimize({{x,1},{y,1}},-1);all_limits(cumulative); + Model circuit;std::vector successors; + for(int j=0;j<3;++j) successors.push_back(circuit.add_integer(2,4)); + add_circuit(circuit,successors,2);circuit.minimize({{successors[0],1},{successors[1],-1}});all_limits(circuit); +} +void sibling_bound() { + for(bool maximum:{false,true}) { + Model model;const auto x=model.add_binary(),y=model.add_binary(); + model.set_objective({{x,maximum?-2.0:2.0},{y,maximum?-1.0:1.0}}, + maximum?ObjectiveSense::Maximize:ObjectiveSense::Minimize,maximum?17:-17); + NativeSearchOptions options;options.order=NativeSearchOrder::DepthFirst;options.solve.node_limit=4; + options.solve.guarantee=Guarantee::Exact; + const auto solved=solve_native_search(model,options);evidence(model.snapshot(),options,solved); + assert(solved.result.termination==Termination::NodeLimit && solved.result.has_solution()); + assert(solved.result.objective==(maximum?15:-15)); + assert(solved.result.best_bound==(maximum?17:-17)); + assert(solved.frontier.unresolved_regions>=2); + auto historical=solved.result;model.set_bounds(x,1,1); + assert(historical.revision!=model.revision() && historical.value(x)==1); + } +} +void options_and_boundaries() { + Model model;const auto x=model.add_binary(),y=model.add_binary();model.minimize({{x,2},{y,1}}); + NativeSearchOptions options;options.order=static_cast(-1); + assert(solve_native_search(model,options).result.termination==Termination::InvalidModel); + options={};options.relaxation=NativeLpSettings{};options.relaxation->bound_change_interval=0; + assert(solve_native_search(model,options).result.termination==Termination::InvalidModel); + options={};options.relaxation=NativeLpSettings{};options.relaxation->root_cover_cuts=NativeRootCoverSettings{}; + options.relaxation->root_cover_cuts->denominator=3; + assert(solve_native_search(model,options).result.termination==Termination::InvalidModel); + options={};options.solve.time_limit_seconds=-1; + assert(solve_native_search(model,options).result.termination==Termination::InvalidModel); + auto bad=model.snapshot();bad.variables[0].variable.model_id++; + assert(solve_native_search(bad).result.termination==Termination::InvalidModel); + if(!native_capabilities().available) { + options={};options.solve.guarantee=Guarantee::Exact; + const auto missing=solve_native_search(model,options); + assert(missing.result.termination==Termination::Unsupported && !missing.result.has_solution() && !missing.result.best_bound); + assert(missing.result.guarantee==Guarantee::Exact && missing.frontier.admitted_nodes==0); + assert(solve_native_search(model.snapshot(),options).result.termination==Termination::Unsupported);return; + } + options={};options.solve.time_limit_seconds=0; + auto solved=solve_native_search(model,options);evidence(model.snapshot(),options,solved); + assert(solved.result.termination==Termination::TimeLimit && !solved.result.has_solution()); + options={};options.solve.cancellation=std::make_shared();options.solve.cancellation->cancel(); + solved=solve_native_search(model,options);evidence(model.snapshot(),options,solved); + assert(solved.result.termination==Termination::Cancelled && !solved.result.has_solution()); + options={};options.solve.guarantee=Guarantee::Certified; + assert(solve_native_search(model,options).result.termination==Termination::Unsupported); + options={};options.solve.backend=Backend::Highs; + assert(solve_native_search(model,options).result.termination==Termination::Unsupported); + options={};options.solve.threads=2; + assert(solve_native_search(model,options).result.termination==Termination::Unsupported); + options={};options.solve.primal_start={{x,1}}; + assert(solve_native_search(model,options).result.termination==Termination::Unsupported); + Model continuous;continuous.add_continuous(0,1); + assert(solve_native_search(continuous).result.termination==Termination::Unsupported); + Model fractional;const auto z=fractional.add_integer(0,1);fractional.minimize({{z,.5}}); + assert(solve_native_search(fractional).result.termination==Termination::Unsupported); + options={};options.relaxation=NativeLpSettings{}; + if(!native_lp_capabilities().available) + assert(solve_native_search(model,options).result.termination==Termination::Unsupported); + // Existing explicit solver behavior and native BAB limit contract are unchanged. + assert(solve_native(model).backend=="Gecode native"); + SolveOptions old;old.node_limit=1; + assert(!solve_native(model,old).best_bound); + NativeLpOptions old_lp;old_lp.frequency=NativeLpFrequency::AfterBoundChanges; + old_lp.bound_change_interval=3;old_lp.bound_tightening=false;old_lp.validate(); +} +void with_lp() { + if(!native_lp_capabilities().available) return; + Model model;const auto x=model.add_integer(0,5),y=model.add_integer(-1,5); + model.add_row({{x,2},{y,3}},7,inf);model.minimize({{x,1},{y,1}},-6); + std::uint64_t calls=0; + for(auto frequency:{NativeLpFrequency::Root,NativeLpFrequency::AfterBoundChanges}) + for(auto order:{NativeSearchOrder::DepthFirst,NativeSearchOrder::BestBound}) for(bool covers:{false,true}) { + NativeSearchOptions options;options.solve.guarantee=Guarantee::Exact;options.order=order; + options.relaxation=NativeLpSettings{};options.relaxation->frequency=frequency; + if(covers)options.relaxation->root_cover_cuts=NativeRootCoverSettings{}; + auto result=solve_native_search(model,options);evidence(model.snapshot(),options,result); + calls+=result.relaxation.lp_calls; + for(std::uint64_t limit=0;limit<=result.frontier.admitted_nodes+1;++limit) { + options.solve.node_limit=limit;evidence(model.snapshot(),options,solve_native_search(model,options)); + } + } + assert(calls>0); +} + +void root_covers() { + if(!native_lp_capabilities().available)return; + for(bool maximum:{false,true})for(bool activation:{false,true}) { + Model model;const auto gone=model.add_binary();model.remove(gone); + const auto x=model.add_binary(),y=model.add_binary(),b=model.add_binary(); + const auto s=model.add_variable(VariableType::SemiInteger,2,3); + model.add_row({{x,3},{y,3}},-inf,5);add_all_different(model,{x,y}); + add_indicator(model,b,activation,{{s,1},{x,1}},2,inf); + model.set_objective({{x,maximum?2.0:-2.0},{y,maximum?2.0:-2.0},{s,maximum?-1.0:1.0}}, + maximum?ObjectiveSense::Maximize:ObjectiveSense::Minimize,maximum?-17:17); + const auto source=model.snapshot(); + for(auto order:{NativeSearchOrder::DepthFirst,NativeSearchOrder::BestBound})for(unsigned rounds:{0U,1U,4U}) { + NativeSearchOptions options;options.solve.guarantee=Guarantee::Exact;options.order=order; + options.relaxation=NativeLpSettings{};options.relaxation->root_cover_cuts=NativeRootCoverSettings{}; + options.relaxation->root_cover_cuts->max_rounds=rounds; + const auto full=solve_native_search(model,options);evidence(source,options,full); + assert(full.result.termination==Termination::Optimal); + assert(full.relaxation.root_cover.cuts==(rounds?1U:0U)); + for(std::uint64_t nodes=0;nodes<=full.frontier.admitted_nodes+1;++nodes) { + options.solve.node_limit=nodes;const auto limited=solve_native_search(model,options);evidence(source,options,limited); + if(!nodes)assert(!limited.relaxation.root_cover.lp_calls); + } + options.solve.node_limit.reset(); + for(std::size_t cap=0;cap<=2;++cap) { + options.max_open_nodes=cap;evidence(source,options,solve_native_search(model,options)); + } + } + } + Model simple;auto x=simple.add_binary(),y=simple.add_binary(); + simple.add_row({{x,3},{y,3}},-inf,5);simple.minimize({{x,-2},{y,-2}},17); + for(unsigned kind=0;kind<6;++kind) { + NativeSearchOptions options;options.solve.guarantee=Guarantee::Exact; + options.relaxation=NativeLpSettings{};options.relaxation->root_cover_cuts=NativeRootCoverSettings{}; + auto& limits=*options.relaxation->root_cover_cuts; + if(kind==0)limits.max_work=0; + if(kind==1)limits.max_cuts=0; + if(kind==2)limits.max_cut_nonzeros=1; + if(kind==3)limits.max_model_rows=1; + if(kind==4)limits.max_separation_rows=0; + if(kind==5)limits.max_terms_per_row=1; + const auto solved=solve_native_search(simple,options);evidence(simple.snapshot(),options,solved); + assert(solved.result.termination==Termination::Optimal && solved.result.objective==15); + assert(!solved.relaxation.root_cover.cuts); + } +} + +#ifdef GECODE_NATIVE_ROOT_CUT_TEST_HOOKS +std::string cut_event; +std::optional cut_completion; +std::shared_ptr cut_cancellation; +int cut_exception=0; +void root_cut_failures() { + if(!native_lp_capabilities().available)return; + Model model;const auto x=model.add_binary(),y=model.add_binary(); + model.add_row({{x,3},{y,3}},-inf,5);model.minimize({{x,-2},{y,-2}},17); + NativeSearchOptions options;options.solve.guarantee=Guarantee::Exact; + options.relaxation=NativeLpSettings{};options.relaxation->root_cover_cuts=NativeRootCoverSettings{}; + for(auto completion:{NativeRootCoverCompletion::NoPrimalSuggestion,NativeRootCoverCompletion::InvalidSuggestion, + NativeRootCoverCompletion::CallbackError,NativeRootCoverCompletion::BackendError,NativeRootCoverCompletion::AllocationFailure}) { + cut_event="after_loop";cut_completion=completion; + const auto solved=solve_native_search(model,options);++runs; + const bool hint=completion==NativeRootCoverCompletion::NoPrimalSuggestion || completion==NativeRootCoverCompletion::InvalidSuggestion; + assert(solved.result.termination==(hint?Termination::Optimal: + completion==NativeRootCoverCompletion::AllocationFailure?Termination::MemoryLimit:Termination::BackendError)); + assert(solved.result.guarantee==Guarantee::Exact && solved.result.has_solution()==hint); + assert(solved.result.best_bound && *solved.result.best_bound<=15); + if(hint)assert(solved.result.objective==15 && feasible(model.snapshot(),solved.result.values)); + else assert(!solved.result.objective && solved.frontier.admitted_nodes==0 && solved.result.best_bound==13); + assert(solved.relaxation.root_cover.completion==completion); + assert(solved.relaxation.lp_calls>=solved.relaxation.root_cover.lp_calls); + } + cut_completion.reset(); + for(const auto* event:{"before_loop","after_loop","prepared"})for(int mode=0;mode<3;++mode) { + cut_event=event;cut_exception=mode;cut_cancellation=std::make_shared(); + options.solve.cancellation=cut_cancellation; + const auto solved=solve_native_search(model,options);++runs; + assert(solved.result.termination==(mode==0?Termination::Cancelled:mode==1?Termination::MemoryLimit:Termination::BackendError)); + assert(!solved.result.has_solution() && solved.result.best_bound==13); + assert(solved.frontier.admitted_nodes==0 && solved.result.guarantee==Guarantee::Exact); + assert(solved.relaxation.root_cover.requested); + } + cut_event.clear();cut_cancellation.reset();cut_exception=0; +} +#endif +#ifdef GECODE_NATIVE_SEARCH_TEST_HOOKS +std::string selected_event; +unsigned selected_occurrence=0,event_occurrences=0; +bool allocation_failure=false; +std::shared_ptr cancellation; +std::map event_counts; +void knapsack_interruptions() { + Model model; + const auto x = model.add_binary(), y = model.add_binary(); + model.add_row({{x,2},{y,2}},-inf,3); + model.minimize({{x,-3},{y,-2}},11); + NativeSearchOptions options; + options.solve.guarantee = Guarantee::Exact; + assert(oracle(model.snapshot()) == 8); + for (bool fail : {false,true}) { + selected_event = "knapsack_row_completed"; + selected_occurrence = 1; + event_occurrences = 0; + allocation_failure = fail; + cancellation = std::make_shared(); + options.solve.cancellation = cancellation; + const auto solved = solve_native_search(model,options); + assert(event_occurrences == 1); + assert(solved.result.termination == + (fail ? Termination::MemoryLimit : Termination::Cancelled)); + assert(!solved.result.has_solution()); + // The first table row cannot publish a partial DP bound. The original + // objective box still represents the whole unconstructed root region. + assert(solved.result.best_bound == 6); + assert(solved.frontier.admitted_nodes == 0); + assert(solved.frontier.unresolved_regions == 1); + evidence(model.snapshot(),options,solved); + } + selected_event.clear(); + cancellation.reset(); +} +void fault_injections() { + Model model;const auto x=model.add_binary(),y=model.add_binary();model.minimize({{x,2},{y,1}},-17); + NativeSearchOptions options;options.order=NativeSearchOrder::DepthFirst;options.solve.guarantee=Guarantee::Exact; + event_counts.clear(); + solve_native_search(model,options); + const auto counts=event_counts; + for(const auto& event:counts) for(unsigned occurrence=1;occurrence<=std::min(2U,event.second);++occurrence) + for(bool fail:{false,true}) { + selected_event=event.first;selected_occurrence=occurrence;event_occurrences=0;allocation_failure=fail; + cancellation=std::make_shared();options.solve.cancellation=cancellation; + const auto result=solve_native_search(model,options); + assert(event_occurrences==occurrence); + assert(result.result.termination==(fail?Termination::MemoryLimit:Termination::Cancelled)); + evidence(model.snapshot(),options,result); + if(event.first=="after_validation" && occurrence==1) assert(!result.result.has_solution()); + if(event.first=="after_validation" && occurrence==2) assert(result.result.objective==-15); + } + selected_event.clear();cancellation.reset(); +} +#endif +} +#ifdef GECODE_NATIVE_ROOT_CUT_TEST_HOOKS +namespace Gecode { namespace Optimize { +void native_root_cut_test_event(const char* event,NativeRootCoverCompletion& completion) { + if(cut_event!=event)return; + if(cut_completion){completion=*cut_completion;return;} + if(cut_exception==1)throw std::bad_alloc(); + if(cut_exception==2)throw std::runtime_error("injected frontier root preparation failure"); + cut_cancellation->cancel(); +} +}} +#endif +#ifdef GECODE_NATIVE_SEARCH_TEST_HOOKS +namespace Gecode { namespace Optimize { +void native_search_test_event(const char* event) { + ++event_counts[event]; + if(selected_event!=event || ++event_occurrences!=selected_occurrence) return; + if(allocation_failure) throw std::bad_alloc(); + cancellation->cancel(); +} +}} +#endif +int main() { + options_and_boundaries(); + if(!native_capabilities().available) {std::cout<<"Native frontier unavailable: explicit boundaries pass\n";return 0;} + ordinary_and_globals();sibling_bound();with_lp();root_covers(); +#ifdef GECODE_NATIVE_ROOT_CUT_TEST_HOOKS + root_cut_failures(); +#endif +#ifdef GECODE_NATIVE_SEARCH_TEST_HOOKS + knapsack_interruptions();fault_injections(); +#else + // Production builds have no shared coordinator hook or search state. + const auto isolated=[] { + Model model;const auto x=model.add_integer(-1,3),y=model.add_integer(0,3); + model.add_row({{x,2},{y,3}},5,inf);model.minimize({{x,1},{y,1}},-3); + NativeSearchOptions options;options.solve.guarantee=Guarantee::Exact; + if(native_lp_capabilities().available) options.relaxation=NativeLpSettings{}; + return solve_native_search(model,options); + }; + auto left=std::async(std::launch::async,isolated),right=std::async(std::launch::async,isolated); + const auto a=left.get(),b=right.get(); + assert(a.result.termination==Termination::Optimal && a.result.objective==-1); + assert(b.result.objective==a.result.objective && a.result.model_id!=b.result.model_id); +#endif + std::cout< +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace Gecode::Optimize; +namespace { +constexpr double inf=std::numeric_limits::infinity(); +std::uint64_t runs=0; +thread_local std::function hook; +// Independent finite product oracle: no native compiler, propagator or common +// validator is used to compute feasible points or objective bounds. +bool feasible(const ModelSnapshot& model,const std::vector& point) { + const auto row=[&](const std::vector& terms,double lower,double upper) { + std::int64_t sum=0; + for(const auto& term:terms) sum+=static_cast(term.coefficient)*static_cast(point[term.variable.id]); + return sum>=lower && sum<=upper; + }; + for(const auto& variable:model.variables) if(variable.active) { + const auto value=point.at(variable.variable.id); + if(value!=std::trunc(value) || !std::isfinite(value)) return false; + if(!(variable.type==VariableType::SemiInteger && value==0) && (valuevariable.upper)) return false; + } + for(const auto& original:model.rows) + if(original.active && !original.indicator_origin && !row(original.terms,original.lower,original.upper)) return false; + for(const auto& indicator:model.indicators) if(indicator.active) { + const bool enabled=point[indicator.activator.id]==(indicator.active_value?1:0); + if(enabled && !row(indicator.terms,indicator.lower,indicator.upper)) return false; + if(indicator.inactive_gate && point[indicator.inactive_gate->id]!=(enabled?0:1)) return false; + } + for(const auto& global:model.globals) if(global.active) { + const bool valid=std::visit([&](const auto& data) { + using T=std::decay_t; + const auto value=[&](Variable v) {return static_cast(point[v.id]);}; + if constexpr(std::is_same_v) { + std::set seen; + for(auto v:data.variables) if(!seen.insert(value(v)).second) return false; + return true; + } else if constexpr(std::is_same_v) { + const auto index=value(data.index)-data.index_base; + return index>=0 && static_cast(index)) { + std::vector tuple;for(auto v:data.variables) tuple.push_back(value(v)); + return std::find(data.tuples.begin(),data.tuples.end(),tuple)!=data.tuples.end(); + } else if constexpr(std::is_same_v) { + std::set starts; + for(std::size_t i=0;idata.capacity) return false; + } + return true; + } else if constexpr(std::is_same_v) { + auto state=data.initial_state; + for(auto variable:data.variables){ + const auto edge=std::find_if(data.transitions.begin(),data.transitions.end(),[&](const RegularTransition& t){return t.from==state&&t.symbol==value(variable);}); + if(edge==data.transitions.end())return false; + state=edge->to; + } + return std::find(data.final_states.begin(),data.final_states.end(),state)!=data.final_states.end(); + } else { + if(data.successors.empty()) return false; + std::set seen;std::int64_t next=0; + for(std::size_t i=0;i(next)>=data.successors.size() || !seen.insert(next).second) return false; + next=value(data.successors[next])-data.index_base; + } + return next==0; + } + },global.payload); + if(!valid) return false; + } + return true; +} +std::int64_t objective(const ModelSnapshot& model,const std::vector& point) { + auto value=static_cast(model.objective.offset); + for(const auto& term:model.objective.terms) value+=static_cast(term.coefficient)*static_cast(point[term.variable.id]); + return value; +}std::vector> points(const ModelSnapshot& model) { + std::vector point(model.variables.size(),std::numeric_limits::quiet_NaN()); + std::vector> all; + const auto visit=[&](const auto& self,std::size_t slot)->void { + if(slot==model.variables.size()) {all.push_back(point);return;} + const auto& v=model.variables[slot];if(!v.active){self(self,slot+1);return;} + assert(std::isfinite(v.lower) && std::isfinite(v.upper) && v.upper-v.lower<=6); + if(v.type==VariableType::SemiInteger && v.lower>0){point[slot]=0;self(self,slot+1);} + for(std::int64_t value=static_cast(v.lower);value<=v.upper;++value){point[slot]=static_cast(value);self(self,slot+1);} + };visit(visit,0);return all; +} +std::optional oracle(const ModelSnapshot& source) { + std::optional best; + for(const auto& p:points(source))if(feasible(source,p)){ + const auto value=objective(source,p); + if(!best || (source.objective.sense==ObjectiveSense::Minimize?value<*best:value>*best))best=value; + }return best; +} +std::vector entries(const ModelSnapshot& source,const std::vector& p,bool omit_gates=false) { + std::set live; + if(omit_gates)for(const auto& i:source.indicators)if(i.active && i.inactive_gate)live.insert(i.inactive_gate->id); + std::vector out; + for(const auto& v:source.variables)if(v.active && !live.count(v.variable.id))out.push_back({v.variable,p[v.variable.id]}); + return out; +} +struct Answer {SolveResult result;NativeFrontierStatistics frontier;NativeBranchingStatistics branching;NativeLpStatistics lp;}; +bool is_frontier(int route){return route>=2 && route<=6;} +std::vector routes(){return native_lp_capabilities().available?std::vector{0,1,2,3,4,5,6,7}:std::vector{0,2,3,5};} +Answer solve(const ModelSnapshot& source,const SolveOptions& options,int route,std::size_t resident=100000) { + Answer out; + if(route==0)out.result=solve_native(source,options); + else if(route==1 || route==7){NativeLpOptions lp;lp.solve=options;lp.frequency=NativeLpFrequency::AfterBoundChanges;if(route==7)lp.root_cover_cuts=NativeRootCoverSettings{}; + auto r=solve_native_lp(source,lp);out.result=std::move(r.result);out.lp=r.relaxation; + }else{NativeSearchOptions search;search.solve=options;search.max_open_nodes=resident;search.order=route==2?NativeSearchOrder::DepthFirst:NativeSearchOrder::BestBound; + if(route==4 || route==6){search.relaxation=NativeLpSettings{};search.relaxation->frequency=NativeLpFrequency::AfterBoundChanges;if(route==6)search.relaxation->root_cover_cuts=NativeRootCoverSettings{};} + if(route==5 || route==6)search.branching=NativeBranchingSettings{}; + auto r=solve_native_search(source,search);out.result=std::move(r.result);out.frontier=r.frontier;out.branching=r.branching;out.lp=r.relaxation; + }return out; +} +SolveOptions options(){SolveOptions out;out.backend=Backend::Native;out.guarantee=Guarantee::Exact;return out;} +void verify(const ModelSnapshot& source,const Answer& answer,int route,const SolveOptions& o) { + ++runs;const auto best=oracle(source);const auto& r=answer.result;const bool min=source.objective.sense==ObjectiveSense::Minimize; + assert(r.model_id==source.model_id && r.revision==source.revision && r.guarantee==o.guarantee); + if(r.has_solution()){ + assert(best && feasible(source,r.values) && r.objective==static_cast(objective(source,r.values))); + assert(min?*r.objective>=*best:*r.objective<=*best); + for(const auto& v:source.variables){assert(r.active_variables[v.variable.id]==v.active);if(!v.active)assert(std::isnan(r.values[v.variable.id]));} + } + if(r.start_submitted)assert(r.has_solution()); + if(r.best_bound){assert(std::isfinite(*r.best_bound));if(best)assert(min?*r.best_bound<=*best:*r.best_bound>=*best);if(r.has_solution())assert(min?*r.best_bound<=*r.objective:*r.best_bound>=*r.objective);} + if(r.termination==Termination::Optimal)assert(best && r.has_solution() && r.objective==static_cast(*best) && r.best_bound==r.objective); + if(r.termination==Termination::Infeasible)assert(!best && !r.has_solution() && !r.start_submitted && !r.best_bound); + if(!is_frontier(route) && r.termination!=Termination::Optimal)assert(!r.best_bound && !r.absolute_gap && !r.relative_gap); + if(is_frontier(route)){assert(answer.branching.budget_nodes==answer.frontier.admitted_nodes+answer.branching.probe_status_calls);if(o.node_limit)assert(answer.branching.budget_nodes<=*o.node_limit);} +} +void exhaustive(const Model& model) { + const auto source=model.snapshot();const auto all=points(source);const auto best=oracle(source); + for(auto route:routes()){ + auto o=options();const auto cold=solve(source,o,route);verify(source,cold,route,o);assert(!cold.result.start_submitted); + assert(cold.result.termination==(best?Termination::Optimal:Termination::Infeasible)); + for(const auto& point:all){o.primal_start=entries(source,point);if(o.primal_start.empty())continue; + const auto r=solve(source,o,route);verify(source,r,route,o); + if(feasible(source,point)){ + assert(r.result.start_submitted && r.result.termination==Termination::Optimal); + if(!source.indicators.empty()){o.primal_start=entries(source,point,true);const auto derived=solve(source,o,route);verify(source,derived,route,o);assert(derived.result.start_submitted && derived.result.termination==Termination::Optimal);} + }else assert(r.result.termination==Termination::InvalidModel && !r.result.has_solution() && !r.result.start_submitted && !r.lp.lp_calls); + } + } + assert(source.revision==model.revision()); +} +void fixtures(){ + for(int n=0;n<8;++n){Model m;const auto dead=m.add_binary();m.remove(dead);const auto x=m.add_integer(-1,1),y=m.add_binary();m.add_row({{x,2},{y,1}},-1,1);m.set_objective({{x,double(n%3-1)},{y,double(n%2?2:-3)}},n%2?ObjectiveSense::Maximize:ObjectiveSense::Minimize,n-4);exhaustive(m);} + {Model m;m.minimize({},-3);exhaustive(m);m.add_row({},1,inf);exhaustive(m);} + for(bool active:{false,true}){Model m;auto a=m.add_binary(),x=m.add_integer(-1,2);add_indicator(m,a,active,{{x,1}},0,1);m.maximize({{x,2},{a,-1}},-17);exhaustive(m);} + {Model m;auto x=m.add_variable(VariableType::SemiInteger,2,3),y=m.add_integer(-1,1);m.add_row({{x,1},{y,1}},0,3);m.minimize({{x,1},{y,-2}},9);exhaustive(m);} + {Model m;auto a=m.add_binary(),b=m.add_binary();add_all_different(m,{a,b});m.minimize({{a,1},{b,-3}});exhaustive(m);} + {Model m;auto a=m.add_binary();add_all_different(m,{a,a});exhaustive(m);} + {Model m;auto a=m.add_binary(),b=m.add_binary();add_table(m,{a,b},{{0,1},{1,0}});m.maximize({{a,2}});exhaustive(m);} + {Model m;auto a=m.add_binary(),b=m.add_binary(),i=m.add_integer(-1,0),r=m.add_binary();add_element(m,i,{a,b},r,-1);m.minimize({{r,2},{a,-1}});exhaustive(m);} + {Model m;auto a=m.add_binary(),b=m.add_binary();add_cumulative(m,{a,b,a},{1,1,0},{1,1,9},1);m.maximize({{a,1},{b,2}});exhaustive(m);} + {Model m;std::vector s;for(int i=0;i<3;++i)s.push_back(m.add_integer(0,2));add_circuit(m,s);m.minimize({{s[0],2},{s[1],-1}},5);exhaustive(m);} + for(bool minimize:{false,true}){Model m;const auto x=m.add_binary(),y=m.add_binary(),z=m.add_binary();m.add_row({{x,3},{y,3},{z,1}},-inf,5);m.set_objective({{x,-2},{y,-3},{z,1}},minimize?ObjectiveSense::Minimize:ObjectiveSense::Maximize,-5);exhaustive(m);} + for(int value:{-INT_MAX+1,INT_MAX-1,-(INT_MAX-1)/2,(INT_MAX-1)/2}){Model m;auto x=m.add_integer(value,value); + if(std::abs(value)>(INT_MAX-1)/2)m.minimize({},1);else m.minimize({{x,1}},1); + auto o=options();o.primal_start={{x,double(value)}}; + for(int route:{0,2,3,5}){auto r=solve(m.snapshot(),o,route);verify(m.snapshot(),r,route,o);assert(r.result.termination==Termination::Optimal && r.result.start_submitted);} + } +} +void limits(){ + Model m;std::vector vars;std::vector terms;for(int i=0;i<4;++i){vars.push_back(m.add_binary());terms.push_back({vars.back(),double(i+1)});}m.maximize(terms,-13); + const auto source=m.snapshot();std::vector zero(vars.size(),0); + for(auto route:routes()){ + auto o=options();o.primal_start=entries(source,zero);const auto full=solve(source,o,route);verify(source,full,route,o);assert(full.result.start_submitted); + const auto cap=is_frontier(route)?full.branching.budget_nodes+1:24; + for(std::uint64_t n=0;n<=cap;++n){o.node_limit=n;const auto r=solve(source,o,route);verify(source,r,route,o);assert(r.result.termination==Termination::Optimal || r.result.termination==Termination::NodeLimit);assert(r.result.start_submitted==(n>0));} + o.node_limit.reset(); + for(std::size_t n=0;n<4;++n)if(is_frontier(route)){const auto r=solve(source,o,route,n);verify(source,r,route,o);assert(r.result.start_submitted && r.result.has_solution() && r.frontier.peak_open_nodes<=n);} + for(int kind=0;kind<3;++kind){auto stop=o;if(kind==0)stop.time_limit_seconds=0;else if(kind==1)stop.node_limit=0;else{stop.cancellation=std::make_shared();stop.cancellation->cancel();}auto r=solve(source,stop,route);verify(source,r,route,stop);assert(!r.result.start_submitted && !r.result.has_solution() && !r.lp.lp_calls);} + } +} +void input_boundaries(){ + Model m;auto dead=m.add_binary();m.remove(dead);auto x=m.add_integer(-1,1),y=m.add_variable(VariableType::SemiInteger,2,3);m.minimize({{x,1},{y,1}},7);const auto source=m.snapshot();Model foreign;auto other=foreign.add_binary(); + for(auto route:routes()){ + auto o=options();o.primal_start={{x,0},{y,0}}; + const auto good=solve(source,o,route);verify(source,good,route,o);assert(good.result.start_submitted); + for(int kind=0;kind<8;++kind){auto bad=o; + if(kind==0)bad.primal_start.push_back({x,0});else if(kind==1)bad.primal_start.push_back({other,0});else if(kind==2)bad.primal_start.push_back({dead,0});else if(kind==3)bad.primal_start[0].variable.id=source.variables.size();else if(kind==4)bad.primal_start[0].value=1-1e-12;else if(kind==5)bad.primal_start[1].value=1;else if(kind==6)bad.primal_start[0].value=inf;else bad.primal_start[0].value=std::numeric_limits::quiet_NaN(); + const auto r=solve(source,bad,route);assert(r.result.termination==Termination::InvalidModel && !r.result.start_submitted && !r.result.has_solution());++runs; + } + o.primal_start={{x,0}};const auto partial=solve(source,o,route);assert(partial.result.termination==Termination::Unsupported && !partial.result.start_submitted);++runs; + auto malformed=source;malformed.variables[x.id].variable.model_id++;o.primal_start={{x,0},{y,0}};assert(solve(malformed,o,route).result.termination==Termination::InvalidModel);++runs; + o.guarantee=Guarantee::Numerical;o.primal_start[0].value=1-1e-12;assert(solve(source,o,route).result.termination==Termination::InvalidModel);++runs; + o.primal_start[0].value=0;const auto numerical=solve(source,o,route);verify(source,numerical,route,o);assert(numerical.result.start_submitted && numerical.result.termination==Termination::Optimal); + o=options();o.guarantee=Guarantee::Certified;o.primal_start={{x,0},{y,0}};assert(solve(source,o,route).result.termination==Termination::Unsupported);++runs; + Model fractional;auto v=fractional.add_binary();fractional.minimize({{v,.5}});o=options();o.primal_start={{v,0}};assert(solve(fractional.snapshot(),o,route).result.termination==Termination::Unsupported);++runs; + Model overflow;v=overflow.add_integer(0,INT_MAX-1);overflow.minimize({{v,2}});o.primal_start={{v,0}};assert(solve(overflow.snapshot(),o,route).result.termination==Termination::Unsupported);++runs; + } + auto o=options();o.primal_start={{x,0},{y,0}};const auto old=solve_native(m,o);assert(old.start_submitted); + m.maximize({{x,-2},{y,3}},-4);const auto revised=solve_native(m,o);verify(m.snapshot(),Answer{revised,{},{},{}},0,o);assert(revised.start_submitted && old.revision!=revised.revision && old.value(y)==0); + m.set_bounds(x,1,1);assert(solve_native(m,o).termination==Termination::InvalidModel); + Model moved(std::move(m));assert(solve_native(m,o).termination==Termination::InvalidModel);assert(solve_native_search(m).result.termination==Termination::InvalidModel); + NativeLpOptions lp;lp.solve=o;assert(solve_native_lp(m,lp).result.termination==Termination::InvalidModel); + o.primal_start={{x,1},{y,0}};std::vector> tasks;auto snapshot=moved.snapshot();for(int n=0;n<4;++n)tasks.push_back(std::async(std::launch::async,[&]{return solve_native(snapshot,o);}));for(auto& task:tasks){const auto r=task.get();assert(r.start_submitted);verify(snapshot,Answer{r,{},{},{}},0,o);} +} +// Swap logical slots while retaining every structural identity and owned row/gate. +void reverse_indicators(ModelSnapshot& source){ + std::reverse(source.indicators.begin(),source.indicators.end()); + for(std::size_t i=0;iid].indicator_origin=d.indicator; + for(auto row:d.generated_rows)source.rows[row.id].indicator_origin=d.indicator; + } +} +void gates(){ + Model m;auto a=m.add_binary(),x=m.add_integer(0,3);auto first=add_indicator(m,a,true,{{x,1}},2,inf);assert(first.inactive_gate);auto second=add_indicator(m,*first.inactive_gate,true,{{x,1}},-inf,1);assert(second.inactive_gate);m.minimize({{x,-1}},9); + auto source=m.snapshot();reverse_indicators(source); + for(auto route:routes())for(bool active:{false,true}){ + auto o=options();o.primal_start={{a,active?1.:0.},{x,active?2.:1.}};const auto r=solve(source,o,route);verify(source,r,route,o);assert(r.result.start_submitted); + o.primal_start.push_back({*first.inactive_gate,active?1.:0.});assert(solve(source,o,route).result.termination==Termination::InvalidModel);++runs; + } + // A structurally valid cyclic equation set has no guessed orientation. + source=m.snapshot();auto& d=source.indicators[first.indicator.id];d.activator=*second.inactive_gate; + for(auto row:d.generated_rows){auto& terms=source.rows[row.id].terms;for(auto& t:terms)if(t.variable==a)t.variable=*second.inactive_gate;std::sort(terms.begin(),terms.end(),[](const Term& l,const Term& r){return l.variable.id stages={"before_start","map_entry","gate_dependency","known_slot","derive_gate","start_completeness","before_exact_start","exact_variable","exact_term","exact_row","exact_indicator","exact_global","after_exact_global","exact_objective","after_exact_start","before_numerical_start","after_numerical_start","before_start_publication","after_start_publication","before_start_cutoff","after_start_cutoff","after_start_release"}; + for(auto route:routes())for(const auto& stage:stages)for(int mode=0;mode<3;++mode){ + auto o=base;o.cancellation=std::make_shared();bool fired=false,published=false; + hook=[&](const char* raw){const std::string event=raw;if(event=="after_start_publication")published=true;if(!fired && event==stage){fired=true;if(mode==0)o.cancellation->cancel();else if(mode==1)throw std::bad_alloc();else throw std::runtime_error("injected native start failure");}}; + const auto r=solve(source,o,route);hook={};assert(fired);verify(source,r,route,o); + assert(r.result.termination==(mode==0?Termination::Cancelled:mode==1?Termination::MemoryLimit:Termination::BackendError)); + assert(r.result.start_submitted==published && r.result.has_solution()==published); + if(published)assert(*r.result.objective<=10); + if(is_frontier(route) && stage!="after_start_release")assert(r.result.best_bound==6 && r.frontier.unresolved_regions==1 && !r.frontier.admitted_nodes); + if(stage=="after_start_publication")assert(r.result.objective==10 && !r.lp.lp_calls); + } + // Every injected failure after the incumbent is published retains it. + for(int route:{0,1,7})if(route==0 || native_lp_capabilities().available){ + for(const std::string stage:{"start_root_alloc","start_search_ready","before_start_search_next","before_start_candidate_validation","before_start_candidate_publication"}){ + auto o=base;o.cancellation=std::make_shared();bool fired=false; + hook=[&](const char* event){if(!fired && stage==event){fired=true;o.cancellation->cancel();}}; + const auto r=solve(source,o,route);hook={};assert(fired);verify(source,r,route,o);assert(r.result.start_submitted && r.result.objective==10 && r.result.termination==Termination::Cancelled); + } + } +#ifdef GECODE_NATIVE_SEARCH_TEST_HOOKS + for(int route:{2,3,5})for(const std::string stage:{"frontier:root_alloc","frontier:before_propagation","frontier:after_propagation","frontier:child_clone","frontier:child_committed","frontier:child_enqueued","frontier:before_validation","frontier:after_validation"})for(int mode=0;mode<3;++mode){ + auto o=base;o.cancellation=std::make_shared();bool fired=false; + hook=[&](const char* event){if(!fired && stage==event){fired=true;if(mode==0)o.cancellation->cancel();else if(mode==1)throw std::bad_alloc();else throw std::runtime_error("injected frontier transfer failure");}}; + const auto r=solve(source,o,route);hook={};assert(fired);verify(source,r,route,o);assert(r.result.start_submitted && r.result.has_solution());assert(r.result.termination==(mode==0?Termination::Cancelled:mode==1?Termination::MemoryLimit:Termination::BackendError)); + } +#endif +#ifdef GECODE_NATIVE_ROOT_CUT_TEST_HOOKS + if(native_lp_capabilities().available)for(int route:{6,7})for(const std::string stage:{"root_cut:before_loop","root_cut:after_loop","root_cut:prepared"})for(int mode=0;mode<3;++mode){ + auto o=base;o.cancellation=std::make_shared();bool fired=false; + hook=[&](const char* event){if(!fired && stage==event){fired=true;if(mode==0)o.cancellation->cancel();else if(mode==1)throw std::bad_alloc();else throw std::runtime_error("injected start LP setup failure");}}; + const auto r=solve(source,o,route);hook={};assert(fired);verify(source,r,route,o);assert(r.result.start_submitted && r.result.objective==10 && r.result.termination==(mode==0?Termination::Cancelled:mode==1?Termination::MemoryLimit:Termination::BackendError)); + } +#endif + for(int route:{0,3})for(const std::string stage:{"before_start_publication","after_start_publication"}){ + auto o=base;o.time_limit_seconds=.5;bool fired=false;hook=[&](const char* event){if(!fired && stage==event){fired=true;std::this_thread::sleep_for(std::chrono::milliseconds(550));}}; + const auto r=solve(source,o,route);hook={};assert(fired);verify(source,r,route,o);assert(r.result.termination==Termination::TimeLimit && r.result.start_submitted==(stage=="after_start_publication")); + } + // No start invokes no start-specific work/hooks, including explicit empty input. + for(auto route:routes()){ + auto o=options();std::uint64_t events=0;hook=[&](const char* event){const std::string e=event;if(e.find("frontier:")!=0 && e.find("root_cut:")!=0)++events;}; + const auto first=solve(source,o,route);o.primal_start={};const auto second=solve(source,o,route);hook={};verify(source,first,route,o);verify(source,second,route,o);assert(!events && first.result.objective==second.result.objective && first.frontier.admitted_nodes==second.frontier.admitted_nodes && first.lp.lp_calls==second.lp.lp_calls); + } +} +#endif +} +namespace Gecode {namespace Optimize { +#ifdef GECODE_NATIVE_START_TEST_HOOKS +void native_start_test_event(const char* event){if(hook)hook(event);} +#endif +#ifdef GECODE_NATIVE_SEARCH_TEST_HOOKS +void native_search_test_event(const char* event){if(hook)hook((std::string("frontier:")+event).c_str());} +#endif +#ifdef GECODE_NATIVE_ROOT_CUT_TEST_HOOKS +void native_root_cut_test_event(const char* event,NativeRootCoverCompletion&){if(hook)hook((std::string("root_cut:")+event).c_str());} +#endif +}} +int main(){ + if(!native_capabilities().available){Model m;auto x=m.add_binary();auto o=options();o.primal_start={{x,0}};assert(solve_native(m,o).termination==Termination::Unsupported);NativeSearchOptions search;search.solve=o;assert(solve_native_search(m,search).result.termination==Termination::Unsupported);NativeLpOptions lp;lp.solve=o;assert(solve_native_lp(m,lp).result.termination==Termination::Unsupported);o.primal_start[0].value=inf;assert(solve_native(m,o).termination==Termination::InvalidModel);std::cout<<"Native starts unavailable: explicit boundaries pass\n";return 0;} + fixtures();limits();input_boundaries();gates(); +#ifdef GECODE_NATIVE_START_TEST_HOOKS + faults(); +#endif + std::cout< +#include +#include +#include +#include +#include +#include +using namespace Gecode::Optimize; +namespace { +constexpr double inf=std::numeric_limits::infinity(); +std::optional oracle(const ModelSnapshot& m){ + std::optional best;std::vector v(m.variables.size()); + const auto visit=[&](const auto& self,std::size_t i)->void{ + if(irow.upper)return;} + double a=m.objective.offset;for(auto t:m.objective.terms)a+=t.coefficient*v[t.variable.id]; + if(!best || (m.objective.sense==ObjectiveSense::Minimize ? a<*best:a>*best))best=a; + };visit(visit,0);return best; +} +Model fixture(bool binary,bool maximize,bool identical){ + Model m;std::vector sum,twice,cost; + for(int i=0;i<5;++i){auto x=binary?m.add_binary():m.add_integer(-1,2); + sum.push_back({x,1});twice.push_back({x,2});cost.push_back({x,identical?1.0:double(i+1)});} + m.add_row(sum,2,inf);m.add_row(twice,-inf,8); + m.set_objective(cost,maximize?ObjectiveSense::Maximize:ObjectiveSense::Minimize,-13); + return m; +} +} +int main(){ + if(!native_capabilities().available){std::cout<<"Symmetry native integration unavailable\n";return 0;} + SolveOptions o;o.backend=Backend::Native;o.guarantee=Guarantee::Exact;o.relative_gap=o.absolute_gap=0;o.time_limit_seconds=5; + for(bool binary:{false,true})for(bool maximize:{false,true})for(bool identical:{false,true}){ + auto m=fixture(binary,maximize,identical);auto s=m.snapshot();auto expected=oracle(s); + auto result=solve(m,o); + if(result.termination!=Termination::Optimal)std::cerr<();stopped.cancellation->cancel(); + auto cancelled=solve(m,stopped);assert(cancelled.termination==Termination::Cancelled && !cancelled.has_solution()); + // Two coupled rows prohibit swapping the differently occurring variables. + Model asymmetric;auto x=asymmetric.add_binary(),y=asymmetric.add_binary(); + asymmetric.add_row({{x,1},{y,2}},1,1);asymmetric.minimize({{x,1},{y,1}}); + auto a=solve(asymmetric,o);assert(a.termination==Termination::Optimal && a.value(x)==1 && a.value(y)==0); + // Public automatic path assembles two separate components and the offset once. + Model disconnected;auto p=disconnected.add_integer(0,3),q=disconnected.add_integer(0,3); + disconnected.add_row({{p,1}},1,3);disconnected.add_row({{q,1}},1,3); + disconnected.minimize({{p,2},{q,-3}},7);auto d=solve(disconnected,o); + assert(d.termination==Termination::Optimal && d.objective==0 && validate(disconnected.snapshot(),d.values,0,0).valid); + assert(d.message.find("independent components")!=std::string::npos); + std::cout<<"Symmetry and public preprocessing integration checks passed\n"; +} diff --git a/test/optimize/package_origin/consumer/CMakeLists.txt b/test/optimize/package_origin/consumer/CMakeLists.txt new file mode 100644 index 0000000000..6b79c6008a --- /dev/null +++ b/test/optimize/package_origin/consumer/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.21) +project(OptimizationPackageOriginConsumer LANGUAGES CXX) + +if(IMPORT_MODE STREQUAL "combined-first") + find_package(Gecode CONFIG REQUIRED COMPONENTS int search + PATHS "${COMBINED_PREFIX}/lib/cmake/Gecode" NO_DEFAULT_PATH) +elseif(IMPORT_MODE STREQUAL "alias-first") + add_library(Gecode::optimize INTERFACE IMPORTED) +elseif(IMPORT_MODE STREQUAL "c-alias-first") + add_library(Gecode::optimize_c INTERFACE IMPORTED) +elseif(IMPORT_MODE STREQUAL "c-target-first") + add_library(Gecode::gecodeoptimize_c INTERFACE IMPORTED) +endif() + +set(GecodeOptimize_DIR "${STANDALONE_PREFIX}/lib/cmake/GecodeOptimize") +find_package(GecodeOptimize CONFIG REQUIRED) +if(IMPORT_MODE STREQUAL "repeat") + find_package(GecodeOptimize CONFIG REQUIRED) + get_target_property(origin Gecode::gecodeoptimize GECODE_OPTIMIZE_PACKAGE_ORIGIN) + get_filename_component(expected "${GecodeOptimize_DIR}" REALPATH) + if(NOT origin STREQUAL expected) + message(FATAL_ERROR "Repeated package find did not retain its original target") + endif() +elseif(IMPORT_MODE STREQUAL "different-prefix") + set(GecodeOptimize_DIR "${SECOND_STANDALONE_PREFIX}/lib/cmake/GecodeOptimize") + find_package(GecodeOptimize CONFIG REQUIRED) +endif() diff --git a/test/optimize/package_origin/producer/CMakeLists.txt b/test/optimize/package_origin/producer/CMakeLists.txt new file mode 100644 index 0000000000..f1e738b761 --- /dev/null +++ b/test/optimize/package_origin/producer/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.21) +project(OptimizationPackageOriginFixture VERSION 0.1.0 LANGUAGES CXX) +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) + +# Interface-only fixtures exercise CMake's real generated export behavior. +# Installing them requires no solver compilation or linked binaries. +add_library(gecodeoptimize INTERFACE) +if(PACKAGE_KIND STREQUAL "standalone") + set(GECODE_OPTIMIZE_WITH_HIGHS OFF) + configure_package_config_file("${OPTIMIZE_CONFIG_TEMPLATE}" + "${CMAKE_CURRENT_BINARY_DIR}/GecodeOptimizeConfig.cmake" + INSTALL_DESTINATION lib/cmake/GecodeOptimize) + install(TARGETS gecodeoptimize EXPORT GecodeOptimizeTargets) + install(EXPORT GecodeOptimizeTargets NAMESPACE Gecode:: DESTINATION lib/cmake/GecodeOptimize) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/GecodeOptimizeConfig.cmake" + DESTINATION lib/cmake/GecodeOptimize) +elseif(PACKAGE_KIND STREQUAL "combined") + add_library(gecodeint INTERFACE) + add_library(gecodesearch INTERFACE) + install(TARGETS gecodeint gecodesearch gecodeoptimize EXPORT GecodeTargets) + install(EXPORT GecodeTargets NAMESPACE Gecode:: DESTINATION lib/cmake/Gecode) + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/GecodeConfig.cmake" [=[ +include("${CMAKE_CURRENT_LIST_DIR}/GecodeTargets.cmake") +add_library(Gecode::optimize INTERFACE IMPORTED) +set_target_properties(Gecode::optimize PROPERTIES INTERFACE_LINK_LIBRARIES Gecode::gecodeoptimize) +set(Gecode_int_FOUND TRUE) +set(Gecode_search_FOUND TRUE) +]=]) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/GecodeConfig.cmake" DESTINATION lib/cmake/Gecode) +else() + message(FATAL_ERROR "Specify PACKAGE_KIND=standalone or combined") +endif() diff --git a/test/optimize/package_origin/run.py b/test/optimize/package_origin/run.py new file mode 100644 index 0000000000..db11d78856 --- /dev/null +++ b/test/optimize/package_origin/run.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Configure-only package ownership regressions; no solver builds or benchmarks. + +Creates and installs interface-only export fixtures using the actual standalone +package config template. Repeated discovery must succeed; foreign combined, +standalone-prefix, or alias targets must produce the explicit ownership error. +--check-native-boundary also checks the real standalone Native=ON rejection. +""" +import argparse +from pathlib import Path +import subprocess +import tempfile + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[2] + + +def run(arguments, expected_error=None): + result = subprocess.run(arguments, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, timeout=120) + if expected_error is None: + if result.returncode: + raise RuntimeError("Command failed: " + repr(arguments) + "\n" + result.stdout) + elif result.returncode == 0 or expected_error not in " ".join(result.stdout.split()): + raise RuntimeError("Expected explicit rejection: " + expected_error + "\n" + result.stdout) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cmake", default="cmake") + parser.add_argument("--generator") + parser.add_argument("--work-root", type=Path) + parser.add_argument("--source-root", type=Path, default=ROOT) + parser.add_argument("--check-native-boundary", action="store_true") + args = parser.parse_args() + args.source_root = args.source_root.resolve(strict=True) + if args.work_root: + args.work_root.mkdir(parents=True, exist_ok=True) + generator = ["-G", args.generator] if args.generator else [] + with tempfile.TemporaryDirectory(prefix="gecode-optimize-origin-", dir=args.work_root) as temporary: + work = Path(temporary) + prefixes = {} + for name, kind in (("standalone", "standalone"), ("second", "standalone"), ("combined", "combined")): + prefixes[name] = work / (name + "-install") + build = work / (name + "-build") + run([args.cmake, "-S", str(HERE / "producer"), "-B", str(build), *generator, + "-DPACKAGE_KIND=" + kind, + "-DOPTIMIZE_CONFIG_TEMPLATE=" + str(args.source_root / "gecode/optimize/GecodeOptimizeConfig.cmake.in"), + "-DCMAKE_INSTALL_PREFIX=" + str(prefixes[name]), "-DCMAKE_INSTALL_LIBDIR=lib"]) + run([args.cmake, "--install", str(build), "--config", "Release"]) + rejection = "already belongs to a different optimization package" + for mode in ("repeat", "combined-first", "different-prefix", "alias-first", "c-alias-first", "c-target-first"): + run([args.cmake, "-S", str(HERE / "consumer"), "-B", str(work / mode), *generator, + "-DIMPORT_MODE=" + mode, "-DSTANDALONE_PREFIX=" + str(prefixes["standalone"]), + "-DSECOND_STANDALONE_PREFIX=" + str(prefixes["second"]), + "-DCOMBINED_PREFIX=" + str(prefixes["combined"])], + None if mode == "repeat" else rejection) + print("PASS package origin: " + mode, flush=True) + if args.check_native_boundary: + run([args.cmake, "-S", str(args.source_root / "gecode/optimize"), + "-B", str(work / "unsupported-native"), *generator, + "-DGECODE_OPTIMIZE_WITH_NATIVE=ON", "-DGECODE_OPTIMIZE_WITH_HIGHS=OFF", + "-DGECODE_OPTIMIZE_BUILD_TESTS=OFF"], + "Native optimization bridge requires a top-level Gecode build") + print("PASS standalone native support boundary", flush=True) + + +if __name__ == "__main__": + main() diff --git a/test/optimize/pool.cpp b/test/optimize/pool.cpp new file mode 100644 index 0000000000..49c35d2e73 --- /dev/null +++ b/test/optimize/pool.cpp @@ -0,0 +1,333 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace Gecode::Optimize; +namespace { +constexpr double inf = std::numeric_limits::infinity(); +[[maybe_unused]] bool near(double a, double b) { return std::fabs(a-b) <= 1e-6; } +void historical(const ModelSnapshot& model, const PoolResult& result) { + assert(result.model_id == model.model_id && result.revision == model.revision); + std::set> keys; + for (const auto& entry : result.entries) { + assert(keys.insert(entry.projection_values).second); + assert(entry.solution.has_solution()); + assert(entry.solution.model_id == model.model_id && entry.solution.revision == model.revision); + assert(entry.solution.values.size() == model.variables.size()); + assert(entry.solution.termination == Termination::Unknown); + assert(!entry.solution.best_bound && !entry.solution.absolute_gap && !entry.solution.relative_gap); + auto checked = validate(model, entry.solution.values); + assert(checked.valid && checked.objective == entry.solution.objective); + for (std::size_t i=0; i first; +// Independent small exact arithmetic oracle, including every private binary +// witness. Does not call the implementation's validator or no-good generator. +std::vector enumerate(const ModelSnapshot& model, bool reverse) { + std::vector values(model.variables.size()), best; + double optimum = model.objective.sense == ObjectiveSense::Minimize ? inf : -inf; + std::function visit = [&](std::size_t slot) { + if (slot != model.variables.size()) { + const auto& variable = model.variables[slot]; + if (!variable.active) { values[slot] = 0; visit(slot+1); return; } + assert(variable.type == VariableType::Integer || variable.type == VariableType::Binary); + assert(std::isfinite(variable.lower) && std::isfinite(variable.upper)); + for (int x=static_cast(std::ceil(variable.lower)); x<=std::floor(variable.upper); ++x) { + values[slot]=x; visit(slot+1); + } + return; + } + for (const auto& row : model.rows) if (row.active) { + double activity=0; for (const auto& term : row.terms) activity += term.coefficient*values[term.variable.id]; + if (activity < row.lower || activity > row.upper) return; + } + double objective=model.objective.offset; + for (const auto& term : model.objective.terms) objective += term.coefficient*values[term.variable.id]; + const bool minimize = (model.objective.sense == ObjectiveSense::Minimize) != reverse; + if (best.empty() || (minimize ? objectiveoptimum)) { + best=values; optimum=objective; + } + }; + visit(0); return best; +} +} +namespace Gecode { namespace Optimize { +SolveResult solve(const ModelSnapshot& model, const SolveOptions& options) { + assert(options.relative_gap == 0 && options.absolute_gap == 0); + assert(options.time_limit_seconds <= remaining); remaining=options.time_limit_seconds; + if (calls != 0) assert(options.primal_start.empty()); + SolveResult result; + result.model_id=model.model_id; result.revision=model.revision; + result.guarantee=options.guarantee; + result.values=enumerate(model, calls==0 && (scenario==Scenario::ReversedRank || scenario==Scenario::ReversedLimitedRank || scenario==Scenario::ReversedInfeasible)); + for (const auto& variable : model.variables) result.active_variables.push_back(variable.active); + result.termination = result.values.empty() ? Termination::Infeasible : Termination::Optimal; + if (!result.values.empty()) { + result.solution_validated=true; result.objective=model.objective.offset; + for (const auto& term : model.objective.terms) *result.objective += term.coefficient*result.values[term.variable.id]; + if (scenario==Scenario::OffsetHonest) result.objective=1; + if (scenario==Scenario::OffsetForged) result.objective=0; + result.best_bound=result.objective; result.update_gaps(model.objective.sense); + if (calls==0) first=result.values; + } + if (scenario==Scenario::BadOwner || scenario==Scenario::BadInfeasibleOwner) ++result.model_id; + if (scenario==Scenario::BadRevision) ++result.revision; + if (scenario==Scenario::BadMask) result.active_variables[0]=false; + if (scenario==Scenario::BadPrimal) result.values[0]=99; + if (scenario==Scenario::MissingBound) { result.best_bound.reset(); result.absolute_gap.reset(); } + if (scenario==Scenario::BadBound) result.best_bound=*result.objective+1; + if (scenario==Scenario::OpenBound) result.best_bound=*result.objective-1; + if (scenario==Scenario::BadObjective) { result.objective=0; result.best_bound=0; } + if (scenario==Scenario::WrongGuarantee) result.guarantee=Guarantee::Certified; + if (scenario==Scenario::Duplicate && calls==1) { + result.values.assign(model.variables.size(), 0); + std::copy(first.begin(), first.end(), result.values.begin()); + } + if ((scenario==Scenario::FirstLimit && calls==0) || (scenario==Scenario::SecondLimit && calls==1) || + (scenario==Scenario::ReversedLimitedRank && calls==1)) result.termination=Termination::TimeLimit; + if (scenario==Scenario::FirstMemory) result.termination=Termination::MemoryLimit; + if ((scenario==Scenario::FirstCancel && calls==0) || (scenario==Scenario::SecondCancel && calls==1)) options.cancellation->cancel(); + if (scenario==Scenario::Ambiguous || scenario==Scenario::BadInfeasibleOwner) { + result.solution_validated=false; result.values.clear(); result.objective.reset(); result.best_bound.reset(); + result.termination=scenario==Scenario::Ambiguous ? Termination::InfeasibleOrUnbounded : Termination::Infeasible; + } + if (scenario==Scenario::UnvalidatedFeasible || (scenario==Scenario::ReversedInfeasible && calls==1)) { + result.solution_validated=false; result.termination=Termination::Infeasible; + } + if (scenario==Scenario::NanInfeasible) { + result.solution_validated=false; result.values.clear(); result.termination=Termination::Infeasible; + result.objective=std::numeric_limits::quiet_NaN(); + } + ++calls; return result; +} +}} +int main() { + Model model; auto x=model.add_integer(0,2); auto y=model.add_binary(); + model.minimize({{x,1},{y,3}},7); + PoolOptions options; options.projection=std::vector{x}; options.solve.time_limit_seconds=60; + options.solve.primal_start={{x,2},{y,1}}; + for (auto selected : {Scenario::Good,Scenario::FirstLimit,Scenario::SecondLimit,Scenario::FirstCancel, + Scenario::SecondCancel,Scenario::BadOwner,Scenario::BadRevision,Scenario::BadMask,Scenario::BadPrimal, + Scenario::MissingBound,Scenario::BadBound,Scenario::OpenBound,Scenario::BadObjective,Scenario::Duplicate, + Scenario::Ambiguous,Scenario::BadInfeasibleOwner,Scenario::WrongGuarantee,Scenario::ReversedRank, + Scenario::ReversedLimitedRank,Scenario::UnvalidatedFeasible,Scenario::NanInfeasible,Scenario::FirstMemory,Scenario::ReversedInfeasible}) { + scenario=selected; calls=0; remaining=inf; + auto result=solve_pool(model,options); + historical(model.snapshot(),result); + if (selected==Scenario::Good) { + assert(result.exhausted() && result.entries.size()==3 && result.ranked_prefix==3 && calls==4); + for (unsigned i=0;i<3;++i) { + assert(result.entries[i].projection_values==std::vector{i}); + assert(result.entries[i].solution.objective==7+i); + } + } else { + assert(!result.exhausted() && result.completion==PoolCompletion::Incomplete); + if (selected==Scenario::FirstLimit || selected==Scenario::SecondLimit || selected==Scenario::FirstMemory) { + assert(result.termination==(selected==Scenario::FirstMemory ? Termination::MemoryLimit:Termination::TimeLimit)); + assert(result.entries.size()==(selected==Scenario::SecondLimit ? 2u:1u)); + assert(result.ranked_prefix+1==result.entries.size() && !result.entries.back().rank_established); + } else if (selected==Scenario::FirstCancel || selected==Scenario::SecondCancel) { + assert(result.termination==Termination::Cancelled); + assert(result.entries.size()==(selected==Scenario::FirstCancel ? 0u : 1u)); + assert(result.ranked_prefix==result.entries.size()); + } else if (selected==Scenario::Ambiguous) { + assert(result.termination==Termination::InfeasibleOrUnbounded && result.entries.empty()); + } else { + assert(result.termination==Termination::NumericalFailure); + if (selected==Scenario::ReversedRank || selected==Scenario::ReversedLimitedRank || selected==Scenario::ReversedInfeasible) { + assert(result.entries.size()==1 && result.ranked_prefix==0 && !result.entries[0].rank_established); + } + } + } + } + for (auto selected : {Scenario::OffsetHonest,Scenario::OffsetForged}) { + scenario=selected; calls=0; remaining=inf; + Model cancellation; auto a=cancellation.add_integer(1,1); auto b=cancellation.add_integer(1e6,1e6); + auto choice=cancellation.add_binary(); cancellation.minimize({{a,1},{b,1e10}},-1e16); + PoolOptions selected_options; selected_options.projection=std::vector{choice}; + auto result=solve_pool(cancellation,selected_options); + if (selected==Scenario::OffsetHonest) { + assert(result.exhausted() && result.ranked_prefix==2); + for (const auto& entry:result.entries) assert(entry.solution.objective==1); + } else { + assert(result.termination==Termination::NumericalFailure && result.entries.empty() && !result.exhausted()); + } + } + scenario=Scenario::Good; calls=0; remaining=inf; options.max_solutions=2; + auto result=solve_pool(model,options); + assert(result.completion==PoolCompletion::RequestedLimit && result.termination==Termination::SolutionLimit); + assert(result.ranked_prefix==2 && calls==2 && !result.exhausted()); + calls=0; options.solve.time_limit_seconds=0; + assert(solve_pool(model,options).termination==Termination::TimeLimit && calls==0); + options.solve.time_limit_seconds=60; options.solve.node_limit=1; + assert(solve_pool(model,options).termination==Termination::Unsupported && calls==0); +} +#else +namespace { +void discrete_oracles(Backend backend, Guarantee guarantee) { + for (auto sense : {ObjectiveSense::Minimize,ObjectiveSense::Maximize}) { + Model model; auto x=model.add_integer(-2,2); auto y=model.add_integer(0,2); + model.add_row({{x,1},{y,1}},0,inf); model.set_objective({{x,2},{y,-1}},sense,-9); + const auto before=model.snapshot(); + std::map,double> expected; + for (int a=-2;a<=2;++a) for(int b=0;b<=2;++b) if(a+b>=0) expected[{a,b}]=2*a-b-9; + PoolOptions options; options.max_solutions=20; options.solve.backend=backend; options.solve.guarantee=guarantee; + auto result=solve_pool(model,options); historical(before,result); + assert(result.exhausted() && result.termination==Termination::Optimal && result.guarantee==guarantee); + assert(result.entries.size()==expected.size() && result.ranked_prefix==expected.size()); + double previous=sense==ObjectiveSense::Minimize ? -inf:inf; + for (const auto& entry:result.entries) { + assert(entry.rank_established && entry.solution.objective==expected.at(entry.projection_values)); + assert(sense==ObjectiveSense::Minimize ? *entry.solution.objective>=previous:*entry.solution.objective<=previous); + previous=*entry.solution.objective; + } + // Excluding x removes all y completions, so there are exactly five classes. + options.projection=std::vector{x}; + result=solve_pool(model,options); historical(before,result); + assert(result.exhausted() && result.entries.size()==5 && result.ranked_prefix==5); + for (const auto& entry:result.entries) { + auto a=entry.projection_values[0]; double best=sense==ObjectiveSense::Minimize ? inf:-inf; + for(int b=0;b<=2;++b) if(a+b>=0) best=sense==ObjectiveSense::Minimize ? std::min(best,double(2*a-b-9)):std::max(best,double(2*a-b-9)); + assert(entry.solution.objective==best); + } + assert(model.revision()==before.revision && model.snapshot().variables.size()==before.variables.size()); + assert(model.snapshot().rows.size()==before.rows.size() && model.snapshot().objective.offset==-9); + auto historical_value=result.entries[0].solution.value(x); + model.set_bounds(x,-1,1); + assert(result.entries[0].solution.value(x)==historical_value && result.revision==before.revision); + } +} +void recourse() { + for (auto sense : {ObjectiveSense::Minimize,ObjectiveSense::Maximize}) { + Model model; auto x=model.add_integer(0,3); auto y=model.add_continuous(-inf,inf); + model.add_row({{x,-1},{y,1}},-inf,2.25); // y <= x+2.25 + model.add_row({{x,1},{y,1}},3.5,inf); // y >= 3.5-x: x=0 infeasible + model.set_objective({{x,2},{y,1}},sense,-5); + PoolOptions options; options.max_solutions=10; options.projection=std::vector{x}; + auto result=solve_pool(model,options); historical(model.snapshot(),result); + assert(result.exhausted() && result.entries.size()==3); + for(const auto& entry:result.entries) { + const double a=entry.projection_values[0]; + const double completion=sense==ObjectiveSense::Minimize ? 3.5-a:a+2.25; + assert(near(entry.solution.value(y),completion)); + assert(near(*entry.solution.objective,2*a+completion-5)); + } + } + { + Model model; auto x=model.add_binary(); auto y=model.add_integer(-inf,inf); + model.add_row({{x,1},{y,1}},1,inf); model.add_row({{x,-1},{y,1}},-inf,2); + model.minimize({{x,3},{y,1}}); + PoolOptions options; options.projection=std::vector{x}; + auto result=solve_pool(model,options); historical(model.snapshot(),result); + assert(result.exhausted() && result.ranked_prefix==2); + assert(result.entries[0].solution.objective==1 && result.entries[1].solution.objective==3); + } + Model unbounded; auto choice=unbounded.add_binary(); auto free=unbounded.add_continuous(-inf,inf); + unbounded.maximize({{free,1}}); + PoolOptions options; options.projection=std::vector{choice}; + auto result=solve_pool(unbounded,options); + assert((result.termination==Termination::Unbounded || result.termination==Termination::InfeasibleOrUnbounded) && + result.entries.empty() && !result.exhausted()); +} +void boundaries() { + Model binary; auto x=binary.add_binary(); auto y=binary.add_binary(); + binary.add_row({{x,1},{y,1}},1,inf); // three tied representatives + PoolOptions options; options.max_solutions=3; options.solve.primal_start={{x,1},{y,0}}; + auto result=solve_pool(binary,options); historical(binary.snapshot(),result); + assert(result.completion==PoolCompletion::RequestedLimit && result.ranked_prefix==3 && !result.exhausted()); + assert(result.entries[0].solution.start_submitted && !result.entries[1].solution.start_submitted); + options.max_solutions=4; result=solve_pool(binary,options); + assert(result.exhausted() && result.entries.size()==3); + options.projection=std::vector{y,x}; result=solve_pool(binary,options); + assert(result.exhausted() && result.projection[0]==y && result.projection[1]==x); + options.projection=std::vector{x,x}; + assert(solve_pool(binary,options).termination==Termination::InvalidModel); + options.projection=std::vector{Variable{binary.id()+1,x.id}}; + assert(solve_pool(binary,options).termination==Termination::InvalidModel); + options.projection=std::vector{}; + assert(solve_pool(binary,options).termination==Termination::Unsupported); + options={}; options.max_solutions=0; + assert(solve_pool(binary,options).termination==Termination::InvalidModel); + options.max_solutions=2; options.solve.node_limit=0; + assert(solve_pool(binary,options).termination==Termination::NodeLimit); + options.solve.node_limit=10; + assert(solve_pool(binary,options).termination==Termination::Unsupported); + options.solve.node_limit.reset(); options.solve.time_limit_seconds=0; + assert(solve_pool(binary,options).termination==Termination::TimeLimit); + options.solve.time_limit_seconds=inf; options.solve.cancellation=std::make_shared(); options.solve.cancellation->cancel(); + assert(solve_pool(binary,options).termination==Termination::Cancelled); + options={}; options.solve.guarantee=Guarantee::Certified; + assert(solve_pool(binary,options).termination==Termination::Unsupported); + options.solve.guarantee=Guarantee::Exact; + assert(solve_pool(binary,options).termination==Termination::Unsupported); + options={}; + Model empty; empty.minimize({},7); + result=solve_pool(empty,options); historical(empty.snapshot(),result); + assert(result.exhausted() && result.entries.size()==1 && result.entries[0].solution.objective==7); + empty.add_row({},1,inf); result=solve_pool(empty,options); + assert(result.exhausted() && result.entries.empty()); + Model fractional; fractional.add_integer(0.2,0.8); + result=solve_pool(fractional,options); assert(result.exhausted() && result.entries.empty()); + Model tombstones; auto deleted=tombstones.add_binary(); tombstones.remove(deleted); tombstones.add_binary(); + result=solve_pool(tombstones,options); historical(tombstones.snapshot(),result); + assert(result.exhausted() && result.entries.size()==2 && result.entries[0].solution.values.size()==2); + options.projection=std::vector{deleted}; + assert(solve_pool(tombstones,options).termination==Termination::InvalidModel); + options={}; + Model continuous; auto c=continuous.add_continuous(0,1); + assert(solve_pool(continuous,options).termination==Termination::Unsupported); + options.projection=std::vector{c}; + assert(solve_pool(continuous,options).termination==Termination::Unsupported); + options={}; + Model wide; wide.add_integer(-inf,inf); + assert(solve_pool(wide,options).termination==Termination::Unsupported); + Model huge; huge.add_integer(-9007199254740992.0,9007199254740992.0); + assert(solve_pool(huge,options).termination==Termination::Unsupported); + Model semi; semi.add_variable(VariableType::SemiInteger,2,4); + assert(solve_pool(semi,options).termination==Termination::Unsupported); + options.max_solutions=20; + Model logical; auto b=logical.add_binary(); auto v=logical.add_integer(0,2); + auto indicator=add_indicator(logical,b,true,{{v,1}},1,inf); + assert(solve_pool(logical,options).termination==Termination::Unsupported); + remove_indicator(logical,indicator.indicator); + assert(solve_pool(logical,options).completion==PoolCompletion::Exhausted); + auto global=add_all_different(logical,{b,v}); + assert(solve_pool(logical,options).termination==Termination::Unsupported); + logical.remove(global); + assert(solve_pool(logical,options).completion==PoolCompletion::Exhausted); +} +} +int main() { + if(capabilities(Backend::Native).available) discrete_oracles(Backend::Native,Guarantee::Exact); + if(!capabilities(Backend::Highs).available) { + Model model; model.add_binary(); + auto result=solve_pool(model); + assert(result.termination==Termination::Unsupported && result.entries.empty() && !result.exhausted()); + return 0; + } + discrete_oracles(Backend::Highs,Guarantee::Numerical); + recourse(); boundaries(); +} +#endif diff --git a/test/optimize/presolve.cpp b/test/optimize/presolve.cpp new file mode 100644 index 0000000000..f551b8a3be --- /dev/null +++ b/test/optimize/presolve.cpp @@ -0,0 +1,273 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace Gecode::Optimize; +namespace { +using Point = std::vector; +constexpr double inf = std::numeric_limits::infinity(); +bool feasible(const ModelSnapshot& model, const Point& values) { + for (const auto& row : model.rows) if (row.active) { + std::int64_t sum=0; + for (const auto& term : row.terms) sum += static_cast(term.coefficient)*values[term.variable.id]; + if (sum < row.lower || sum > row.upper) return false; + } + return true; +} +std::int64_t objective(const ModelSnapshot& model, const Point& values) { + auto sum=static_cast(model.objective.offset); + for (const auto& term : model.objective.terms) sum += static_cast(term.coefficient)*values[term.variable.id]; + return sum; +} +std::set enumerate(const ModelSnapshot& model) { + std::set points; Point values(model.variables.size()); + std::function visit=[&](std::size_t slot) { + if(slot==values.size()) { if(feasible(model,values)) points.insert(values); return; } + const auto& variable=model.variables[slot]; + if(!variable.active) { visit(slot+1); return; } + for(auto x=static_cast(variable.lower);x<=variable.upper;++x) { values[slot]=x; visit(slot+1); } + }; + visit(0); return points; +} +SolveResult candidate(const ModelSnapshot& model, const Point& point) { + SolveResult result; result.model_id=model.model_id; result.revision=model.revision; + for(auto value:point) result.values.push_back(static_cast(value)); + for(const auto& variable:model.variables) result.active_variables.push_back(variable.active); + // Deliberately untrusted objective/proof flags: postsolve must recompute. + result.objective=-999; result.best_bound=-999; result.termination=Termination::Optimal; + result.absolute_gap=0; result.relative_gap=0; result.solution_validated=false; + return result; +} +void equivalent(const ModelSnapshot& source, const PresolveOptions& options={}) { + const auto before=enumerate(source); + auto result=presolve_integer(source,options); + assert(result.model_id==source.model_id && result.revision==source.revision && result.guarantee==Guarantee::Exact); + if(result.status==PresolveStatus::Infeasible) { + assert(before.empty() && result.termination==Termination::Infeasible && !result.model && result.infeasible_row); + return; + } + assert(result.model && (result.status==PresolveStatus::Fixpoint || result.status==PresolveStatus::Incomplete)); + const auto& artifact=*result.model; + assert(artifact.original().model_id==source.model_id && artifact.original().revision==source.revision); + assert(artifact.reduced().model_id!=source.model_id); + assert(artifact.reduced().objective.sense==source.objective.sense); + assert(artifact.variables().size()==source.variables.size() && artifact.rows().size()==source.rows.size()); + std::set reconstructed; + for(const auto& point:enumerate(artifact.reduced())) { + auto post=artifact.postsolve(candidate(artifact.reduced(),point)); + assert(post.exact_witness_validated && post.solution.has_solution()); + assert(post.solution.model_id==source.model_id && post.solution.revision==source.revision); + assert(post.solution.termination==Termination::Unknown && !post.solution.best_bound); + assert(!post.solution.absolute_gap && !post.solution.relative_gap && !post.solution.native_backend_gap); + Point original(source.variables.size()); + for(std::size_t i=0;i(post.solution.values[i]); + else assert(std::isnan(post.solution.values[i])); + } + assert(feasible(source,original) && before.count(original)); + assert(reconstructed.insert(original).second); + assert(post.solution.objective==objective(source,original)); + assert(objective(source,original)==objective(artifact.reduced(),point)); + } + assert(reconstructed==before); // full feasible-set preservation, both directions + for(const auto& point:before) { + Point projected(artifact.reduced().variables.size()); + for(const auto& mapping:artifact.variables()) if(mapping.active) { + if(mapping.fixed_value) assert(point[mapping.original.id]==*mapping.fixed_value); + else projected[mapping.reduced->id]=point[mapping.original.id]; + } + assert(feasible(artifact.reduced(),projected)); + } + for(const auto& change:result.changes) { + assert(change.variable.model_id==source.model_id && change.row.model_id==source.model_id); + assert(source.rows[change.row.id].active); + assert(change.side==PresolveBoundSide::Lower ? change.after>change.before : change.after variables; + for(unsigned i=0;i<3;++i) { + if(i==0 && trial%3==0) variables.push_back(model.add_binary()); + else { const int lo=int(random()%3)-2; variables.push_back(model.add_integer(lo,lo+int(random()%3))); } + } + for(unsigned i=0,n=1+random()%4;i terms; + for(auto variable:variables) terms.push_back({variable,double(int(random()%7)-3)}); + const int lower=int(random()%13)-6, upper=lower+int(random()%7); + const unsigned form=random()%3; + model.add_row(terms,form==0 ? -inf:double(lower),form==1 ? inf:double(upper)); + } + model.set_objective({{variables[0],-2},{variables[1],1},{variables[2],3}}, + trial%2 ? ObjectiveSense::Minimize:ObjectiveSense::Maximize,-5); + equivalent(model.snapshot()); + PresolveOptions partial; partial.max_passes=trial%2; + equivalent(model.snapshot(),partial); + partial.max_passes=100; partial.max_row_visits=trial%3; + equivalent(model.snapshot(),partial); + } +} +void substitution_and_history() { + Model model; auto deleted=model.add_binary(); auto old=model.add_row({{deleted,1}},0,1); + model.remove(old); model.remove(deleted); + auto x=model.add_integer(2,2,"fixed"); auto y=model.add_integer(-2,2,"kept"); + auto row=model.add_row({{x,3},{y,-2}},4,8,"range"); + model.maximize({{x,-3},{y,2}},11); + auto before=model.snapshot(); auto result=presolve_integer(model); + equivalent(before); + assert(model.revision()==before.revision && model.row(row).lower==4 && model.variable(y).lower==-2); + assert(result.model && result.model->variables()[x.id].fixed_value==2); + assert(!result.model->variables()[deleted.id].active && !result.model->rows()[old.id].active); + assert(result.model->reduced().objective.offset==5 && result.model->reduced().objective.sense==ObjectiveSense::Maximize); + assert(result.model->reduced().variables.size()==1 && result.model->reduced().variables[0].name=="kept"); + assert(result.model->variables()[y.id].lower==-1 && result.model->variables()[y.id].upper==1); + auto post=result.model->postsolve(candidate(result.model->reduced(),{0})); + assert(post.exact_witness_validated && post.solution.value(x)==2 && post.solution.value(y)==0 && post.solution.objective==5); + model.set_bounds(y,1,1); + assert(result.model->original().variables[y.id].lower==-2 && post.solution.value(y)==0); + + // Fixed substitution must shift a row that remains a genuine restriction. + Model retained; auto f=retained.add_integer(-2,-2); + auto u=retained.add_integer(0,2); auto v=retained.add_integer(0,2); + auto restricted=retained.add_row({{f,-3},{u,1},{v,1}},7,8,"retained range"); + retained.minimize({{f,-4},{u,2},{v,-1}},-3); + auto mapped=presolve_integer(retained); + assert(mapped.model && mapped.model->rows()[restricted.id].reduced); + assert(mapped.model->rows()[restricted.id].substituted_constant==6); + const auto& private_row=mapped.model->reduced().rows[mapped.model->rows()[restricted.id].reduced->id]; + assert(private_row.lower==1 && private_row.upper==2 && private_row.name=="retained range"); + assert(mapped.model->reduced().objective.offset==5); + equivalent(retained.snapshot()); + PresolveOptions no_passes; no_passes.max_passes=0; + mapped=presolve_integer(retained,no_passes); + assert(mapped.status==PresolveStatus::Incomplete && mapped.model && mapped.passes==0); + assert(mapped.model->rows()[restricted.id].substituted_constant==6); + equivalent(retained.snapshot(),no_passes); + + Model fixed; auto a=fixed.add_integer(1,1); auto b=fixed.add_integer(9007199254740992.0,9007199254740992.0); + fixed.minimize({{a,1},{b,1}},-9007199254740992.0); + auto collapsed=presolve_integer(fixed); + assert(collapsed.model && collapsed.model->reduced().variables.empty()); + assert(collapsed.model->reduced().objective.offset==1); + post=collapsed.model->postsolve(candidate(collapsed.model->reduced(),{})); + assert(post.exact_witness_validated && post.solution.objective==1); + + Model equality; auto e=equality.add_integer(-2,2); equality.add_row({{e,-3}},-3,-3); + equivalent(equality.snapshot()); + auto unique=presolve_integer(equality); + assert(unique.model && unique.fixed_variables==1 && unique.model->variables()[e.id].fixed_value==1); + Model empty; empty.minimize({},-9); equivalent(empty.snapshot()); + empty.add_row({},1,inf); equivalent(empty.snapshot()); + + Model parity; auto p=parity.add_binary(); auto q=parity.add_binary(); auto r=parity.add_binary(); + parity.add_row({{p,2},{q,2},{r,2}},3,3); + auto incomplete_knowledge=presolve_integer(parity); + assert(incomplete_knowledge.status==PresolveStatus::Fixpoint && incomplete_knowledge.model); + assert(enumerate(parity.snapshot()).empty()); // fixpoint is not a feasibility/optimality decision + equivalent(parity.snapshot()); +} +void limits() { + Model model; auto x=model.add_integer(0,10); auto y=model.add_integer(0,10); auto z=model.add_integer(0,10); + model.add_row({{x,1},{y,-1}},-inf,0); model.add_row({{y,1},{z,-1}},-inf,0); model.add_row({{z,1}},-inf,2); + PresolveOptions options; options.max_passes=1; + auto result=presolve_integer(model,options); + assert(result.status==PresolveStatus::Incomplete && result.termination==Termination::IterationLimit && result.model); + assert(result.passes==1 && result.model->variables()[z.id].upper==2 && result.model->variables()[x.id].upper==10); + equivalent(model.snapshot(),options); + options.max_passes=100; options.max_row_visits=1; + result=presolve_integer(model,options); + assert(result.model && result.row_visits==1 && result.status==PresolveStatus::Incomplete); + equivalent(model.snapshot(),options); + options.max_row_visits.reset(); result=presolve_integer(model,options); + assert(result.status==PresolveStatus::Fixpoint && result.passes==4 && result.model->variables()[x.id].upper==2); + options.time_limit_seconds=0; result=presolve_integer(model,options); + assert(result.status==PresolveStatus::Incomplete && result.termination==Termination::TimeLimit && !result.model && !result.infeasible_row); + options.time_limit_seconds=-1; + assert(presolve_integer(model,options).termination==Termination::InvalidModel); + options.time_limit_seconds=inf; options.cancellation=std::make_shared(); options.cancellation->cancel(); + result=presolve_integer(model,options); + assert(result.termination==Termination::Cancelled && !result.model && !result.infeasible_row); +} +void unsupported_and_overflow() { + auto unsupported=[](const Model& model) { + auto result=presolve_integer(model); + assert(result.status==PresolveStatus::Unsupported && result.termination==Termination::Unsupported); + assert(!result.model && !result.infeasible_row); + }; + Model continuous; continuous.add_continuous(0,1); unsupported(continuous); + Model semi; semi.add_variable(VariableType::SemiInteger,2,4); unsupported(semi); + Model unbounded; unbounded.add_integer(0,inf); unsupported(unbounded); + Model fractional; fractional.add_integer(0.25,1); unsupported(fractional); + Model coefficient; auto c=coefficient.add_binary(); coefficient.add_row({{c,0.5}},0,1); unsupported(coefficient); + Model sides; auto s=sides.add_binary(); sides.add_row({{s,1}},0.5,1); unsupported(sides); + Model offset; offset.minimize({},0.5); unsupported(offset); + Model logical; auto b=logical.add_binary(); auto x=logical.add_integer(0,2); + auto indicator=add_indicator(logical,b,true,{{x,1}},1,inf); unsupported(logical); + remove_indicator(logical,indicator.indicator); equivalent(logical.snapshot()); + auto global=add_all_different(logical,{b,x}); unsupported(logical); + logical.remove(global); equivalent(logical.snapshot()); + auto malformed=logical.snapshot(); malformed.variables[0].variable.model_id++; + assert(presolve_integer(malformed).status==PresolveStatus::InvalidModel); + Model product; auto large=product.add_integer(4294967296.0,4294967296.0); + product.add_row({{large,4294967296.0}},-inf,0); unsupported(product); + Model sum; std::vector terms; + for(int i=0;i<4;++i) terms.push_back({sum.add_integer(2147483648.0,2147483648.0),1073741824.0}); + sum.add_row(terms,-inf,0); unsupported(sum); + Model quotient; auto a=quotient.add_integer(0,1); auto y=quotient.add_integer(-2147483647.0,2147483647.0); + auto constant=quotient.add_integer(4294967295.0,4294967295.0); + quotient.add_row({{a,-1},{y,4294967296.0},{constant,1}},-1,inf); + auto division=presolve_integer(quotient); + assert(division.status==PresolveStatus::Unsupported && division.message.find("division")!=std::string::npos && !division.model); + Model export_range; auto small=export_range.add_integer(1,1); + auto big=export_range.add_integer(9007199254740992.0,9007199254740992.0); + export_range.minimize({{small,1},{big,1}}); unsupported(export_range); +} +void postsolve_guards() { + Model model; auto x=model.add_integer(0,2); model.minimize({{x,3}},4); + auto result=presolve_integer(model); const auto& artifact=*result.model; + auto raw=candidate(artifact.reduced(),{1}); raw.values[0]+=4e-7; + auto good=artifact.postsolve(raw); assert(good.exact_witness_validated && good.solution.objective==7); + assert(!good.solution.best_bound && good.solution.termination==Termination::Unknown); + assert(artifact.postsolve(raw,0).solution.termination==Termination::NumericalFailure); + raw=candidate(artifact.reduced(),{1}); raw.model_id++; + assert(artifact.postsolve(raw).solution.termination==Termination::InvalidModel); + raw=candidate(artifact.reduced(),{1}); raw.revision++; + assert(artifact.postsolve(raw).solution.termination==Termination::InvalidModel); + raw=candidate(artifact.reduced(),{1}); raw.active_variables[0]=false; + assert(artifact.postsolve(raw).solution.termination==Termination::InvalidModel); + raw=candidate(artifact.reduced(),{1}); raw.values[0]=std::numeric_limits::quiet_NaN(); + assert(artifact.postsolve(raw).solution.termination==Termination::NumericalFailure); + raw=candidate(artifact.reduced(),{3}); + assert(artifact.postsolve(raw).solution.termination==Termination::NumericalFailure); + raw=candidate(artifact.reduced(),{1}); raw.guarantee=Guarantee::Certified; + assert(artifact.postsolve(raw).solution.termination==Termination::Unsupported); + assert(artifact.postsolve(raw,0.5).solution.termination==Termination::InvalidModel); + Model row_model; auto a=row_model.add_integer(0,2); auto b=row_model.add_integer(0,2); + row_model.add_row({{a,1},{b,1}},-inf,2); + auto rows=presolve_integer(row_model); + auto invalid=rows.model->postsolve(candidate(rows.model->reduced(),{2,2})); + assert(invalid.solution.termination==Termination::NumericalFailure && !invalid.exact_witness_validated && !invalid.solution.has_solution()); + Model future; auto snapshot=future.snapshot(); ++snapshot.model_id; + auto distinct=presolve_integer(snapshot); + assert(distinct.model && distinct.model->reduced().model_id!=snapshot.model_id); +} +} +int main() { + signed_oracles(); substitution_and_history(); limits(); unsupported_and_overflow(); postsolve_guards(); +} diff --git a/test/optimize/presolve_solve.cpp b/test/optimize/presolve_solve.cpp new file mode 100644 index 0000000000..15ee6af820 --- /dev/null +++ b/test/optimize/presolve_solve.cpp @@ -0,0 +1,63 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include + +using namespace Gecode::Optimize; +int main() { + std::size_t solved = 0; + for (bool maximize : {false, true}) for (std::size_t passes : {0U, 100U}) { + Model source; + auto removed = source.add_integer(-3, 3); source.remove(removed); + auto fixed = source.add_integer(2, 2), variable = source.add_integer(-2, 2); + source.add_row({{fixed, 3}, {variable, -2}}, 4, 8, "range"); + source.set_objective({{fixed, -3}, {variable, 2}}, + maximize ? ObjectiveSense::Maximize : ObjectiveSense::Minimize, 11); + PresolveOptions options; options.max_passes = passes; + const auto prepared = presolve_integer(source, options); + assert(prepared.model && prepared.fixed_variables == 1); + assert(prepared.status == (passes ? PresolveStatus::Fixpoint : PresolveStatus::Incomplete)); + const auto saved_revision = source.revision(); + source.set_bounds(variable, 0, 0); // transformation remains historical + for (int backend = 0; backend < 3; ++backend) { + const bool available = backend == 0 ? capabilities(Backend::Highs).available : + backend == 1 ? native_capabilities().available : native_lp_capabilities().available; + if (!available) continue; + SolveOptions solve_options; + solve_options.backend = backend == 0 ? Backend::Highs : Backend::Native; + solve_options.guarantee = backend == 0 ? Guarantee::Numerical : Guarantee::Exact; + SolveResult reduced; + if (backend == 2) { + NativeLpOptions hybrid; hybrid.solve = solve_options; + reduced = solve_native_lp(prepared.model->reduced(), hybrid).result; + } else reduced = solve(prepared.model->reduced(), solve_options); + assert(reduced.termination == Termination::Optimal && reduced.has_solution()); + const auto restored = prepared.model->postsolve(reduced); + assert(restored.exact_witness_validated && restored.solution.has_solution()); + assert(restored.solution.objective == (maximize ? 7 : 3)); + assert(restored.solution.value(fixed) == 2 && restored.solution.value(variable) == (maximize ? 1 : -1)); + assert(restored.solution.model_id == source.id() && restored.solution.revision == saved_revision); + assert(restored.solution.revision != source.revision()); + assert(!restored.solution.active_variables[removed.id] && std::isnan(restored.solution.values[removed.id])); + assert(restored.solution.termination == Termination::Unknown && !restored.solution.best_bound && !restored.solution.absolute_gap); + assert(validate(prepared.model->original(), restored.solution.values, 0, 0).valid); + assert(!validate(source.snapshot(), restored.solution.values, 0, 0).valid); + ++solved; + } + } + // A propagation fixpoint is not a feasibility or optimization result. + Model parity; + const auto x = parity.add_binary(), y = parity.add_binary(), z = parity.add_binary(); + parity.add_row({{x, 2}, {y, 2}, {z, 2}}, 3, 3); + const auto prepared = presolve_integer(parity); + assert(prepared.status == PresolveStatus::Fixpoint && prepared.model); + for (const auto backend : {Backend::Highs, Backend::Native}) if (capabilities(backend).available) { + SolveOptions options; options.backend = backend; + const auto result = solve(prepared.model->reduced(), options); + assert(result.termination == Termination::Infeasible && !result.has_solution()); + } + std::cout << solved << " actual backend solves with exact historical reconstruction pass\n"; +} diff --git a/test/optimize/process_containment.py b/test/optimize/process_containment.py new file mode 100644 index 0000000000..fce3eec3c9 --- /dev/null +++ b/test/optimize/process_containment.py @@ -0,0 +1,349 @@ +"""Bounded Windows process-tree lifetime for compiler and solver tests. + +Requires Windows 10 or later. The helper owns the launched process and its job, +so cleanup also terminates descendants started by the compiler or solver. +""" +import ctypes as C +import math +import ntpath +import os +import subprocess +import time + +# Windows uses LLP64, including when these declarations are tested on Unix LP64. +DWORD, BOOL, WORD = C.c_uint32, C.c_int32, C.c_uint16 +HANDLE, SIZE_T = C.c_void_p, C.c_size_t +POINTER = C.c_void_p +CREATE_SUSPENDED = 0x00000004 +CREATE_NO_WINDOW = 0x08000000 +EXTENDED_STARTUPINFO_PRESENT = 0x00080000 +STARTF_USESTDHANDLES = 0x00000100 +PROC_THREAD_ATTRIBUTE_HANDLE_LIST = 0x00020002 +PROC_THREAD_ATTRIBUTE_JOB_LIST = 0x0002000D +JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000 +JOB_OBJECT_EXTENDED_LIMIT_INFORMATION = 9 +JOB_OBJECT_BASIC_ACCOUNTING_INFORMATION = 1 +DUPLICATE_SAME_ACCESS = 2 +ERROR_INSUFFICIENT_BUFFER = 122 +WAIT_OBJECT_0, WAIT_TIMEOUT, WAIT_FAILED = 0, 258, 0xFFFFFFFF + + +class STARTUPINFOW(C.Structure): + _fields_ = [("cb", DWORD), ("lpReserved", POINTER), ("lpDesktop", POINTER), + ("lpTitle", POINTER), ("dwX", DWORD), ("dwY", DWORD), + ("dwXSize", DWORD), ("dwYSize", DWORD), ("dwXCountChars", DWORD), + ("dwYCountChars", DWORD), ("dwFillAttribute", DWORD), ("dwFlags", DWORD), + ("wShowWindow", WORD), ("cbReserved2", WORD), ("lpReserved2", POINTER), + ("hStdInput", HANDLE), ("hStdOutput", HANDLE), ("hStdError", HANDLE)] + + +class STARTUPINFOEXW(C.Structure): + _fields_ = [("StartupInfo", STARTUPINFOW), ("lpAttributeList", POINTER)] + + +class PROCESS_INFORMATION(C.Structure): + _fields_ = [("hProcess", HANDLE), ("hThread", HANDLE), + ("dwProcessId", DWORD), ("dwThreadId", DWORD)] + + +class JOBOBJECT_BASIC_LIMIT_INFORMATION(C.Structure): + _fields_ = [("PerProcessUserTimeLimit", C.c_int64), ("PerJobUserTimeLimit", C.c_int64), + ("LimitFlags", DWORD), ("MinimumWorkingSetSize", SIZE_T), + ("MaximumWorkingSetSize", SIZE_T), ("ActiveProcessLimit", DWORD), + ("Affinity", SIZE_T), ("PriorityClass", DWORD), ("SchedulingClass", DWORD)] + + +class IO_COUNTERS(C.Structure): + _fields_ = [(name, C.c_uint64) for name in ( + "ReadOperationCount", "WriteOperationCount", "OtherOperationCount", + "ReadTransferCount", "WriteTransferCount", "OtherTransferCount")] + + +class JOBOBJECT_EXTENDED_LIMIT_INFORMATION(C.Structure): + _fields_ = [("BasicLimitInformation", JOBOBJECT_BASIC_LIMIT_INFORMATION), + ("IoInfo", IO_COUNTERS), ("ProcessMemoryLimit", SIZE_T), + ("JobMemoryLimit", SIZE_T), ("PeakProcessMemoryUsed", SIZE_T), + ("PeakJobMemoryUsed", SIZE_T)] + + +class JOBOBJECT_BASIC_ACCOUNTING_INFORMATION(C.Structure): + _fields_ = [("TotalUserTime", C.c_int64), ("TotalKernelTime", C.c_int64), + ("ThisPeriodTotalUserTime", C.c_int64), ("ThisPeriodTotalKernelTime", C.c_int64), + ("TotalPageFaultCount", DWORD), ("TotalProcesses", DWORD), + ("ActiveProcesses", DWORD), ("TotalTerminatedProcesses", DWORD)] + + +class WinAPI: + """Explicit stdcall signatures; kernel32 is never loaded on other systems.""" + def __init__(self): + if os.name != "nt": + raise OSError("Windows Job Objects require Windows 10 or newer") + dll = C.WinDLL("kernel32", use_last_error=True) + ptr = C.c_void_p + signatures = { + "CreateJobObjectW": ([ptr, C.c_wchar_p], HANDLE), + "SetInformationJobObject": ([HANDLE, C.c_int32, ptr, DWORD], BOOL), + "QueryInformationJobObject": ([HANDLE, C.c_int32, ptr, DWORD, ptr], BOOL), + "InitializeProcThreadAttributeList": ([ptr, DWORD, DWORD, C.POINTER(SIZE_T)], BOOL), + "UpdateProcThreadAttribute": ([ptr, DWORD, SIZE_T, ptr, SIZE_T, ptr, ptr], BOOL), + "DeleteProcThreadAttributeList": ([ptr], None), + "CreateProcessW": ([C.c_wchar_p, C.c_wchar_p, ptr, ptr, BOOL, DWORD, + ptr, C.c_wchar_p, ptr, C.POINTER(PROCESS_INFORMATION)], BOOL), + "IsProcessInJob": ([HANDLE, HANDLE, C.POINTER(BOOL)], BOOL), + "ResumeThread": ([HANDLE], DWORD), + "WaitForSingleObject": ([HANDLE, DWORD], DWORD), + "GetExitCodeProcess": ([HANDLE, C.POINTER(DWORD)], BOOL), + "TerminateJobObject": ([HANDLE, DWORD], BOOL), + "TerminateProcess": ([HANDLE, DWORD], BOOL), + "CloseHandle": ([HANDLE], BOOL), + "GetCurrentProcess": ([], HANDLE), + "DuplicateHandle": ([HANDLE, HANDLE, HANDLE, C.POINTER(HANDLE), + DWORD, BOOL, DWORD], BOOL), + } + for name, (args, result) in signatures.items(): + try: + function = getattr(dll, name) + except AttributeError as error: + raise OSError("required Windows containment API is unavailable: " + name) from error + function.argtypes, function.restype = args, result + setattr(self, name, function) + + @staticmethod + def last_error(): + return C.get_last_error() + + +def checked_command(command, directory): + """Use an explicit executable and CRT argument quoting, without a shell.""" + command = [os.fspath(arg) for arg in command] + directory = os.fspath(directory) + if (not command or any(not isinstance(arg, str) or "\0" in arg for arg in command) + or not isinstance(directory, str) or "\0" in directory): + raise ValueError("Windows command and working directory must be NUL-free strings") + # ntpath also works in platform-independent conformance tests. + if any(not ntpath.isabs(path) or not ntpath.splitdrive(path)[0] for path in (command[0], directory)): + raise ValueError("Windows executable and working directory must be fully qualified absolute paths") + line = subprocess.list2cmdline(command) + if len(line.encode("utf-16-le")) // 2 + 1 > 32767: + raise ValueError("Windows command exceeds the 32767 UTF-16-unit CreateProcessW limit") + return command, directory, line + + +class WindowsJobProcess: + """One launch, owned process/thread/job handles, explicit bounded cleanup. + + Publish this object to the outer watchdog BEFORE start(). Creation-time job + assignment closes the suspended-process orphan window even if that watchdog + exits between CreateProcessW and its return to Python. The job handle is never + inherited. There is no fallback to launching an uncontained process. + """ + def __init__(self, api=None, fd_to_handle=None): + self.api = WinAPI() if api is None else api + if fd_to_handle is None: + import msvcrt + fd_to_handle = msvcrt.get_osfhandle + self.fd_to_handle = fd_to_handle + self.job = self.process = self.thread = None + self.pid = self.returncode = None + self.command = [] + self._stdio = [] + self._attributes = None + self._attribute_storage = None + self._attribute_values = [] + self._started = False + self._membership_verified = False + + def _error(self, operation): + return OSError("{} failed (Windows error {})".format(operation, self.api.last_error())) + + def _check(self, result, operation): + if not result: + raise self._error(operation) + + def _duplicate(self, stream): + handle = HANDLE() + current = self.api.GetCurrentProcess() + self._check(self.api.DuplicateHandle(current, self.fd_to_handle(stream.fileno()), current, + C.byref(handle), 0, True, DUPLICATE_SAME_ACCESS), + "DuplicateHandle(stdio)") + self._stdio.append(handle.value) + return handle.value + + def start(self, command, stdout, stderr, directory): + if self._started: + raise ValueError("WindowsJobProcess permits exactly one start") + self.command, directory, line = checked_command(command, directory) + self._started = True + try: + try: + self.job = self.api.CreateJobObjectW(None, None) or None + self._check(self.job, "CreateJobObjectW") + limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION() + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + self._check(self.api.SetInformationJobObject( + self.job, JOB_OBJECT_EXTENDED_LIMIT_INFORMATION, C.byref(limits), C.sizeof(limits)), + "SetInformationJobObject(KILL_ON_JOB_CLOSE)") + with open(os.devnull, "rb") as stdin: + inherited = (HANDLE * 3)(self._duplicate(stdin), self._duplicate(stdout), + self._duplicate(stderr)) + jobs = (HANDLE * 1)(self.job) + self._attribute_values = [inherited, jobs] + size = SIZE_T() + result = self.api.InitializeProcThreadAttributeList(None, 2, 0, C.byref(size)) + if result or self.api.last_error() != ERROR_INSUFFICIENT_BUFFER or not size.value: + raise self._error("InitializeProcThreadAttributeList(size)") + self._attribute_storage = C.create_string_buffer(size.value) + self._check(self.api.InitializeProcThreadAttributeList( + self._attribute_storage, 2, 0, C.byref(size)), "InitializeProcThreadAttributeList") + self._attributes = self._attribute_storage + for key, value in ((PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherited), + (PROC_THREAD_ATTRIBUTE_JOB_LIST, jobs)): + self._check(self.api.UpdateProcThreadAttribute( + self._attributes, 0, key, C.byref(value), C.sizeof(value), None, None), + "UpdateProcThreadAttribute({:#x})".format(key)) + startup = STARTUPINFOEXW() + startup.StartupInfo.cb = C.sizeof(startup) + startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES + startup.StartupInfo.hStdInput, startup.StartupInfo.hStdOutput, startup.StartupInfo.hStdError = inherited + startup.lpAttributeList = C.cast(self._attributes, POINTER) + info = PROCESS_INFORMATION() + self._check(self.api.CreateProcessW( + self.command[0], C.create_unicode_buffer(line), None, None, True, + CREATE_SUSPENDED | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT, + None, directory, C.byref(startup), C.byref(info)), "CreateProcessW(atomic job assignment)") + self.process, self.thread, self.pid = info.hProcess, info.hThread, info.dwProcessId + member = BOOL() + self._check(self.api.IsProcessInJob(self.process, self.job, C.byref(member)), "IsProcessInJob") + if not member.value: + raise OSError("CreateProcessW did not assign the process to its required Job Object") + self._membership_verified = True + previous = self.api.ResumeThread(self.thread) + if previous == WAIT_FAILED: + raise self._error("ResumeThread") + if previous != 1: + raise OSError("ResumeThread returned unexpected suspend count {}".format(previous)) + self._check(self.api.CloseHandle(self.thread), "CloseHandle(primary thread)") + self.thread = None + finally: + self._release_setup() + except BaseException: + # This also handles a post-create verification failure: the primary + # thread remains suspended, and both job and process are terminated. + self.cleanup(timeout=0.2) + raise + return self + + def _release_setup(self): + if self._attributes is not None: + self.api.DeleteProcThreadAttributeList(self._attributes) + self._attributes = None + self._attribute_values = [] + self._attribute_storage = None + errors = [] + for handle in self._stdio[:]: + if self.api.CloseHandle(handle): + self._stdio.remove(handle) + else: + errors.append(str(self._error("CloseHandle(stdio duplicate)"))) + if errors: + raise OSError("; ".join(errors)) + + def _exit_code(self): + result = DWORD() + self._check(self.api.GetExitCodeProcess(self.process, C.byref(result)), "GetExitCodeProcess") + self.returncode = result.value + return self.returncode + + def poll(self): + if self.returncode is not None: + return self.returncode + if self.process is None: + raise OSError("process has not been started or has already been closed") + state = self.api.WaitForSingleObject(self.process, 0) + if state == WAIT_TIMEOUT: + return None + if state != WAIT_OBJECT_0: + raise self._error("WaitForSingleObject") + return self._exit_code() + + def wait(self, timeout): + if not math.isfinite(timeout) or timeout < 0: + raise ValueError("cleanup timeout must be finite and nonnegative") + if self.returncode is not None: + return self.returncode + if self.process is None: + raise OSError("process has not been started or has already been closed") + # Never use INFINITE, nor round a wait beyond its caller's reserve. + milliseconds = int(min((WAIT_FAILED - 1) / 1000, timeout) * 1000) + state = self.api.WaitForSingleObject(self.process, milliseconds) + if state == WAIT_TIMEOUT: + raise subprocess.TimeoutExpired(self.command, timeout) + if state != WAIT_OBJECT_0: + raise self._error("WaitForSingleObject") + return self._exit_code() + + def terminate(self): + """Nonwaiting watchdog action; final process exit also closes the job.""" + errors = [] + terminated = False + if self.job is not None: + terminated = bool(self.api.TerminateJobObject(self.job, 124)) + if not terminated: + errors.append(str(self._error("TerminateJobObject"))) + if self.process is not None and (not terminated or not self._membership_verified): + if not self.api.TerminateProcess(self.process, 124): + errors.append(str(self._error("TerminateProcess"))) + if errors: + raise OSError("; ".join(errors)) + + def cleanup(self, timeout): + """Terminate descendants even after root success; close every owned handle. + + Any cleanup failure fails the case. No unbounded wait is used. Keep any + unsuccessfully closed handles available to the outer watchdog for retry. + """ + if not math.isfinite(timeout) or timeout < 0: + raise ValueError("cleanup timeout must be finite and nonnegative") + deadline = time.monotonic() + timeout + errors = [] + try: + try: + self.terminate() + except OSError as error: + errors.append(str(error)) + if self.process is not None: + try: + self.wait(max(0, deadline-time.monotonic())) + except (OSError, subprocess.TimeoutExpired) as error: + errors.append(str(error)) + if self.job is not None: + # Waiting for the root cannot establish that grandchildren died. + while True: + accounting = JOBOBJECT_BASIC_ACCOUNTING_INFORMATION() + if not self.api.QueryInformationJobObject( + self.job, JOB_OBJECT_BASIC_ACCOUNTING_INFORMATION, + C.byref(accounting), C.sizeof(accounting), None): + errors.append(str(self._error("QueryInformationJobObject"))) + break + if accounting.ActiveProcesses == 0: + break + remaining = deadline-time.monotonic() + if remaining <= 0: + errors.append("Job Object descendants did not exit within cleanup reserve") + break + time.sleep(min(0.005, remaining)) + finally: + try: + self._release_setup() + except OSError as error: + errors.append(str(error)) + # Closing the last, non-inherited job handle is the final kill path. + for name in ("thread", "process", "job"): + handle = getattr(self, name) + if handle is not None: + if self.api.CloseHandle(handle): + setattr(self, name, None) + else: + errors.append(str(self._error("CloseHandle({})".format(name)))) + if errors: + raise OSError("; ".join(errors)) diff --git a/test/optimize/quadratic.cpp b/test/optimize/quadratic.cpp new file mode 100644 index 0000000000..2cfd3a13d9 --- /dev/null +++ b/test/optimize/quadratic.cpp @@ -0,0 +1,251 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#if __has_include() +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace Gecode::Optimize; +namespace { +templateclass Expression,class=void>struct Callable:std::false_type{}; +templateclass Expression>struct Callable>>:std::true_type{}; +templateusing LinearCall=decltype(solve(std::declval())); +templateusing NativeCall=decltype(solve_native(std::declval())); +templateusing HybridCall=decltype(solve_native_lp(std::declval())); +templateusing FrontierCall=decltype(solve_native_search(std::declval())); +templateusing PoolCall=decltype(solve_pool(std::declval())); +templateusing RepairCall=decltype(relax_feasibility(std::declval(),RelaxationOptions{})); +templateusing LexCall=decltype(solve_lexicographic(std::declval(),std::vector{})); +templateusing ConflictCall=decltype(analyze_conflict(std::declval())); +templateusing SessionCall=decltype(std::declval().solve(std::declval())); +templateusing ValidateCall=decltype(validate(std::declval(),std::vector{})); +templateusing WriteCall=decltype(write_model(std::declval(),std::string{})); +#if __has_include() +templateusing PresolveCall=decltype(presolve_integer(std::declval())); +#endif +templatevoid type_boundary(){ + static_assert(!Callable::value&&!Callable::value&&!Callable::value,""); + static_assert(!Callable::value&&!Callable::value&&!Callable::value,""); + static_assert(!Callable::value&&!Callable::value&&!Callable::value,""); + static_assert(!Callable::value&&!Callable::value,""); +#if __has_include() + static_assert(!Callable::value,""); +#endif +} +std::size_t solved=0; +void close(double a,double b,double t=1e-6){assert(std::abs(a-b)<=t);} +void optimum(const QuadraticResult& r,double expected){ + if(r.result.termination!=Termination::Optimal)std::cerr<<"Unexpected "<void rejects(F f){bool thrown=false;try{f();}catch(const ModelError&){thrown=true;}assert(thrown);} +// Small exact rational KKT oracle. Test coefficients keep int64 arithmetic bounded. +struct Rat { + std::int64_t n=0,d=1; + Rat(std::int64_t a=0,std::int64_t b=1):n(a),d(b){assert(b);if(d<0){n=-n;d=-d;}auto g=std::gcd(n,d);n/=g;d/=g;} + double value()const{return double(n)/d;} +}; +Rat operator+(Rat a,Rat b){return {a.n*b.d+b.n*a.d,a.d*b.d};} +Rat operator-(Rat a,Rat b){return {a.n*b.d-b.n*a.d,a.d*b.d};} +Rat operator*(Rat a,Rat b){return {a.n*b.n,a.d*b.d};} +Rat operator/(Rat a,Rat b){return {a.n*b.d,a.d*b.n};} +bool operator<(Rat a,Rat b){return a.n*b.d& rows){ + const Rat q00=2*(1+a*a),q01=2*a*b,q11=2*(1+b*b),p0=2*a*c+d,p1=2*b*c+e; + std::vector points; + const auto det=q00*q11-q01*q01; + points.push_back({(q01*p1-q11*p0)/det,(q01*p0-q00*p1)/det}); + for(const auto& r:rows){ + Point base=r.x?Point{Rat(r.b,r.x),0}:Point{0,Rat(r.b,r.y)}; + const Rat tx=-r.y,ty=r.x; + const Rat curvature=q00*tx*tx+Rat(2)*q01*tx*ty+q11*ty*ty; + const Rat slope=tx*(q00*base.x+q01*base.y+p0)+ty*(q01*base.x+q11*base.y+p1); + const Rat t=(Rat(0)-slope)/curvature;points.push_back({base.x+t*tx,base.y+t*ty}); + } + for(std::size_t i=0;i best; + for(auto p:points){bool feasible=true;for(auto r:rows)feasible &= Rat(r.x)*p.x+Rat(r.y)*p.y<=Rat(r.b);if(!feasible)continue; + const auto z=Rat(a)*p.x+Rat(b)*p.y+Rat(c); + const auto f=p.x*p.x+p.y*p.y+z*z+Rat(d)*p.x+Rat(e)*p.y; + if(!best||f<*best)best=f; + } + assert(best);return *best; +} +#ifdef GECODE_QUADRATIC_TEST_HOOKS +int fake=0; +std::string cancel_event; +std::string throw_event; +std::shared_ptr token; +#endif +} +#ifdef GECODE_QUADRATIC_TEST_HOOKS +namespace Gecode{namespace Optimize{namespace Detail{ +double quadratic_test_regularization=0; +void quadratic_test_event(const char* event){if(cancel_event==event&&token)token->cancel();if(throw_event==event)throw std::bad_alloc();} +bool quadratic_test_oracle(const QuadraticSnapshot& q,const QuadraticOptions&,QuadraticRaw& raw){ + if(!fake)return false; + raw.model_id=q.id();raw.revision=q.revision();for(auto& v:q.variables())raw.active_variables.push_back(v.active); + raw.values.assign(q.variables().size(),0);raw.residual_values.assign(q.squares().size(),0); + raw.row_duals.assign(q.rows().size(),0);raw.column_duals.assign(q.variables().size(),0); + raw.value_valid=true;raw.dual_valid=true;raw.objective=q.linear_part().offset;raw.termination=Termination::Optimal; + if(fake==2)raw.values[0]=0.25; + if(fake==3)++raw.model_id; + if(fake==4)++raw.revision; + if(fake==5)raw.active_variables.clear(); + if(fake==6)raw.objective=1; + if(fake==7)raw.residual_values[0]=1; + if(fake==8)raw.column_duals[0]=1; + if(fake==9)raw.dual_valid=false; + if(fake==10){raw.termination=Termination::Infeasible;raw.value_valid=false;} + if(fake==11)raw.termination=Termination::Unbounded; + if(fake==12)raw.termination=Termination::IterationLimit; + if(fake==13)raw.objective=std::numeric_limits::quiet_NaN(); + if(fake==14)raw.values[0]=std::numeric_limits::quiet_NaN(); + if(fake==15){raw.values[0]=1;raw.residual_values[0]=1;raw.objective=q.linear_part().offset+1;raw.column_duals[0]=2;} + return true; +} +}}} +#endif +int main(){ + type_boundary();type_boundary(); + static_assert(!std::is_convertible::value,""); + static_assert(!std::is_convertible::value,""); + using Ordinary=SolveResult(*)(const ModelSnapshot&,const SolveOptions&); + static_assert(!std::is_invocable::value,""); + static_assert(!std::is_constructible::value,""); + QuadraticModel m;const auto x=m.add_continuous(-2,2);const auto dead=m.add_continuous(-1,1);m.remove(dead); + m.minimize_squares({{{{x,2},{x,-1},{x,0}},0,1,"square"}}); + const auto historical=m.snapshot();const auto rev=m.revision();assert(historical.squares()[0].terms[0].coefficient==1); + rejects([&]{m.remove(x);});assert(m.revision()==rev); + rejects([&]{m.minimize_squares({{{{x,1}},0,-1,""}});});assert(m.revision()==rev); + rejects([&]{m.add_continuous(0,std::numeric_limits::infinity());});assert(m.revision()==rev); + QuadraticModel foreign;auto alien=foreign.add_continuous(-1,1); + rejects([&]{m.minimize_squares({{{{alien,1}},0,1,""}});});assert(m.revision()==rev); + m.minimize_squares({{{{x,1}},-1,1,""}});assert(m.revision()==rev+1);assert(historical.squares()[0].offset==0); + auto snap=historical;auto moved_snapshot=std::move(snap);assert(snap.id()==moved_snapshot.id()); + QuadraticModel moved=std::move(m);assert(m.id()==0);rejects([&]{m.snapshot();});assert(moved.id()==historical.id()); + auto check=validate_quadratic(historical,{0,std::numeric_limits::quiet_NaN()});assert(check.primal_valid&&check.objective_valid); + assert(!validate_quadratic(historical,{3,0}).primal_valid); + QuadraticOptions o;o.solve.time_limit_seconds=0;assert(solve_quadratic(historical,o).result.termination==Termination::TimeLimit); + o={};o.solve.cancellation=std::make_shared();o.solve.cancellation->cancel();assert(solve_quadratic(historical,o).result.termination==Termination::Cancelled); + o={};o.optimality_tolerance=-1;assert(solve_quadratic(historical,o).result.termination==Termination::InvalidModel); + for(int k=0;k<5;++k){o={};if(k==0)o.solve.backend=Backend::Native;if(k==1)o.solve.guarantee=Guarantee::Exact;if(k==2)o.solve.node_limit=0;if(k==3)o.solve.threads=2;if(k==4)o.solve.primal_start={{x,0}};assert(solve_quadratic(historical,o).result.termination==Termination::Unsupported);} + const int rounding=std::fegetround();assert(std::fesetround(FE_DOWNWARD)==0);assert(solve_quadratic(historical).result.termination==Termination::Unsupported);assert(std::fesetround(rounding)==0); +#ifdef GECODE_QUADRATIC_TEST_HOOKS + QuadraticModel fm;auto fx=fm.add_continuous(-2,2);fm.minimize_squares({{{{fx,1}},0,1,""}}); + for(fake=1;fake<=14;++fake){auto r=solve_quadratic(fm);if(fake==1)optimum(r,0);else if(fake==12){assert(r.result.termination==Termination::IterationLimit);assert(r.result.has_solution());}else assert(r.result.termination==Termination::NumericalFailure);} + // Large constants cannot make a nonoptimal point pass the closure/KKT gate. + fm.minimize_squares({{{{fx,1}},0,1,""}},{},1e16);fake=15;assert(solve_quadratic(fm).result.termination==Termination::NumericalFailure); + fake=1; + for(const char* name:{"after_preflight","after_validation"}){token=std::make_shared();cancel_event=name;o={};o.solve.cancellation=token;auto r=solve_quadratic(fm,o);assert(r.result.termination==Termination::Cancelled);assert(!r.result.has_solution());assert(!r.checks.bound_valid);} + cancel_event.clear();token.reset();fake=0; + fake=1;throw_event="after_validation";const auto failed=solve_quadratic(fm); + assert(failed.result.termination==Termination::MemoryLimit&&!failed.result.has_solution()); + assert(failed.result.model_id==fm.id()&&failed.result.revision==fm.revision());throw_event.clear();fake=0; +#endif + if(!quadratic_capabilities().available){auto r=solve_quadratic(historical);assert(r.result.termination==Termination::Unsupported);assert(!r.result.has_solution());std::cout<<"quadratic foundation/backend-off passed\n";return 0;} + optimum(solve_quadratic(historical),0);optimum(solve_quadratic(moved),0); + const auto old_result=solve_quadratic(historical);close(old_result.result.value(x),0);rejects([&]{old_result.result.value(dead);});rejects([&]{old_result.result.value(alien);}); + std::mt19937 rng(119); +#ifdef GECODE_QUADRATIC_TEST_HOOKS + unsigned perturbed_accepted=0,perturbed_rejected=0; +#endif + for(int k=0;k<180;++k){ + const int a=int(rng()%5)-2,b=int(rng()%5)-2,c=int(rng()%5)-2,d=int(rng()%5)-2,e=int(rng()%5)-2; + std::vector rows={{1,0,2},{-1,0,2},{0,1,2},{0,-1,2},{1,1,1},{-1,2,2}}; + const auto exact=oracle(a,b,c,d,e,rows); + for(int sense=0;sense<2;++sense){QuadraticModel q;auto u=q.add_continuous(-2,2),v=q.add_continuous(-2,2); + for(std::size_t i=4;i::infinity(),rows[i].b); + std::vector squares={{{{u,1}},0,1,"x"},{{{v,1}},0,1,"y"},{{{u,double(a)},{v,double(b)}},double(c),1,"coupled"}}; + if(sense)q.maximize_concave_squares(squares,{{u,double(-d)},{v,double(-e)}},7);else q.minimize_squares(squares,{{u,double(d)},{v,double(e)}},7); + optimum(solve_quadratic(q),7+(sense?-1:1)*exact.value()); +#ifdef GECODE_QUADRATIC_TEST_HOOKS + Detail::quadratic_test_regularization=1e-7; + const auto perturbed=solve_quadratic(q); + if(perturbed.result.termination==Termination::Optimal){ + close(*perturbed.result.objective,7+(sense?-1:1)*exact.value()); + assert(perturbed.checks.kkt_valid&&perturbed.checks.bound_valid);++perturbed_accepted; + }else{ + assert(perturbed.result.termination==Termination::NumericalFailure || perturbed.result.termination==Termination::IterationLimit); + ++perturbed_rejected; + } + Detail::quadratic_test_regularization=0; +#endif + } + } + // Singular, equality-constrained, constant, fixed, and ill-conditioned cases. + for(int k=0;k<6;++k){QuadraticModel q;auto u=q.add_continuous(-4,4),v=q.add_continuous(-4,4); + if(k==0)q.minimize_squares({{{{u,1},{v,1}},-2,1,"rank-one"}}); + if(k==1){q.minimize_squares({{{{u,1}},0,1,""},{{{v,1}},0,1,""}});q.add_row({{u,1},{v,1}},2,2);} + if(k==2)q.minimize_squares({{{},3,2,"constant"}}); + if(k==3){q.set_bounds(u,2,2);q.minimize_squares({{{{u,1}},0,1,""}});} + if(k==4)q.minimize_squares({{{{u,1}},-1,1e-6,""},{{{v,1}},-2,1e6,""}}); + if(k==5)q.minimize_squares({},{{u,1}},5); + optimum(solve_quadratic(q),k==1?2:k==2?18:k==3?4:k==5?1:0); + } + QuadraticModel empty;empty.minimize_squares({}, {}, 7);optimum(solve_quadratic(empty),7); + QuadraticModel constant_only;constant_only.minimize_squares({{{},3,2,"constant"}}); + optimum(solve_quadratic(constant_only),18); + for(int variant=0;variant<3;++variant){ + QuadraticModel q;auto a=q.add_continuous(-3,3),b=q.add_continuous(-3,3); + q.add_row({{a,1},{b,1}},1,1); + std::vector squares={{{{a,1}},0,1,"a"},{{{b,1}},0,1,"b"}}; + if(variant==1)squares[0].terms[0].coefficient=-1; + if(variant==2){squares[0].weight=0.5;squares.push_back(squares[0]);} + q.minimize_squares(squares);optimum(solve_quadratic(q),0.5); + } + QuadraticModel offset_model;auto offset_x=offset_model.add_continuous(-1,1); + offset_model.minimize_squares({{{{offset_x,1}},-1,1,""}}, {{offset_x,2}}, 1e16); + const auto offset_result=solve_quadratic(offset_model);optimum(offset_result,1e16); + assert(*offset_result.result.absolute_gap>=1); + assert(*offset_result.checks.gap_upper_bound<=1e-6); + assert(*offset_result.result.best_bound<*offset_result.result.objective); + empty.add_row({},1,2);auto infeasible=solve_quadratic(empty);assert(infeasible.result.termination==Termination::Infeasible&&!infeasible.result.has_solution()); + QuadraticModel bad;auto u=bad.add_continuous(-1,1);bad.minimize_squares({{{{u,1}},0,1,""}});bad.add_row({{u,1}},2,3);assert(solve_quadratic(bad).result.termination==Termination::Infeasible); + QuadraticModel tiny;tiny.minimize_squares({{{},0,1e-15,""}});assert(solve_quadratic(tiny).result.termination==Termination::Unsupported); + o={};o.max_auxiliary_variables=0;assert(solve_quadratic(historical,o).result.termination==Termination::Unsupported); + o={};o.max_lifted_nonzeros=0;assert(solve_quadratic(historical,o).result.termination==Termination::Unsupported); + o={};o.iteration_limit=0;auto limited=solve_quadratic(moved,o);assert(limited.result.termination==Termination::IterationLimit || limited.result.termination==Termination::Optimal); +#ifdef GECODE_QUADRATIC_TEST_HOOKS + std::cout<<"regularization exact-oracle acceptance: zero360/360, vendor default "<();cancel_event=name;o={};o.solve.cancellation=token;auto r=solve_quadratic(moved,o);assert(r.result.termination==Termination::Cancelled);assert(!r.result.has_solution());} +#endif + std::cout<<"quadratic: "< +#include +#include +#include +#include +#include +#include +using namespace Gecode::Optimize; +using namespace Gecode::Optimize::Detail; +namespace { +// Independent exact signed dyadic reference: arbitrary-width base-2^32 integer. +// No floating arithmetic is used to compare the exact sum/product with endpoints. +struct Dyadic { + int sign=0, exponent=0; + std::vector words; + explicit Dyadic(double d=0) { + assert(std::isfinite(d));std::uint64_t bits;std::memcpy(&bits,&d,8); + const auto e=(bits>>52)&2047, fraction=bits&((std::uint64_t{1}<<52)-1); + const auto mantissa=e?fraction+(std::uint64_t{1}<<52):fraction; + if(!mantissa)return; + sign=(bits>>63)?-1:1;exponent=e?static_cast(e)-1023-52:-1074; + words={static_cast(mantissa),static_cast(mantissa>>32)};trim(); + } + void trim(){while(!words.empty()&&!words.back())words.pop_back();if(words.empty())sign=0;} + std::vector shifted(int shift)const { + assert(shift>=0);if(!sign)return {}; + std::vector out(words.size()+shift/32+1,0); + const unsigned r=shift%32;std::uint64_t carry=0; + for(std::size_t i=0;i(z);carry=z>>32;} + out[words.size()+shift/32]=static_cast(carry);while(!out.empty()&&!out.back())out.pop_back();return out; + } +}; +int magcmp(const std::vector& a,const std::vector& b){ + if(a.size()!=b.size())return a.size()(z);c=z>>32;}out.words[n]=static_cast(c); + }else{if(magcmp(x,y)<0){x.swap(y);out.sign=b.sign;}y.resize(x.size());out.words.resize(x.size());std::uint64_t borrow=0; + for(std::size_t i=0;i(std::uint64_t(x[i])-sub);borrow=std::uint64_t(x[i])(v);carry=v>>32;}out.words[i+b.words.size()]=static_cast(carry);}out.trim();return out; +} +void enclosed(QpInterval interval,const Dyadic& exact){assert(compare(Dyadic(interval.lower),exact)<=0);assert(compare(Dyadic(interval.upper),exact)>=0);} +} +int main(){ +#if defined(__FAST_MATH__) || defined(GECODE_QUADRATIC_EXPECT_UNSUPPORTED_ARITHMETIC) + assert(!quadratic_arithmetic_supported()); + QuadraticModel m;auto x=m.add_continuous(-1,1);m.minimize_squares({{{{x,1}},0,1,""}}); + assert(!quadratic_bound(m.snapshot(),{0},{0},{}).normalized_lower); + std::cout<<"quadratic fast-math proof boundary rejected\n";return 0; +#else + assert(quadratic_arithmetic_supported());std::mt19937_64 rng(813);std::size_t checked=0; + for(int i=0;i<12000;++i){ + const double a=std::ldexp(double(static_cast(rng()%2000001)-1000000),int(rng()%2050)-1074); + const double b=std::ldexp(double(static_cast(rng()%2000001)-1000000),int(rng()%2050)-1074); + if(!std::isfinite(a)||!std::isfinite(b))continue; + try{enclosed(qp_add(qp_point(a),qp_point(b)),add(Dyadic(a),Dyadic(b)));++checked;}catch(const std::overflow_error&){} + try{enclosed(qp_multiply(qp_point(a),qp_point(b)),multiply(Dyadic(a),Dyadic(b)));++checked;}catch(const std::overflow_error&){} + } + const double tiny=std::numeric_limits::denorm_min(); + enclosed(qp_multiply(qp_point(tiny),qp_point(0.5)),multiply(Dyadic(tiny),Dyadic(0.5))); + enclosed(qp_add(qp_point(1e16),qp_point(1)),add(Dyadic(1e16),Dyadic(1))); + enclosed(qp_add(qp_point(-1e16),qp_point(-1)),add(Dyadic(-1e16),Dyadic(-1))); + for(int k=0;k<400;++k){ + QuadraticModel m;const double lo=-3,hi=4; + auto x=m.add_continuous(lo,hi);const double a=int(rng()%9)-4,b=int(rng()%9)-4,w=1+int(rng()%4),t=int(rng()%11)-5; + const double c=int(rng()%9)-4,d=int(rng()%9)-4; + m.minimize_squares({{{{x,a}},b,w,""}},{{x,c}},1e16); + m.add_row({{x,2}},-4,6);const auto q=m.snapshot(); + const auto got=quadratic_bound(q,{1},{t},{d});assert(got.normalized_lower&&got.gap_upper); + // Exact tangent/row bound formula for this one-variable instance. + const double residual=c+2*w*t*a-2*d; + const double constant=2*w*t*b-w*t*t+d*(d>=0?-4:6); + auto exact=add(Dyadic(1e16),add(Dyadic(constant),multiply(Dyadic(residual),Dyadic(residual>=0?lo:hi)))); + assert(compare(Dyadic(*got.normalized_lower),exact)<=0); + const double primal=c+w*(a+b)*(a+b); + const double dual=constant+residual*(residual>=0?lo:hi); + assert(compare(Dyadic(*got.gap_upper),Dyadic(primal-dual))>=0); + // Also check the claimed global bound at every feasible integer point. + for(int value=-2;value<=3;++value){auto objective=add(Dyadic(1e16),Dyadic(c*value+w*(a*value+b)*(a*value+b)));assert(compare(Dyadic(*got.normalized_lower),objective)<=0);} + } + QuadraticModel m;auto x=m.add_continuous(-1,1);m.minimize_squares({{{{x,1}},0,1,""}});const auto q=m.snapshot(); + const int old=std::fegetround();assert(std::fesetround(FE_UPWARD)==0);assert(!quadratic_arithmetic_supported());assert(!quadratic_bound(q,{0},{0},{}).normalized_lower);assert(std::fesetround(old)==0); + assert(!quadratic_bound(q,{0},{std::numeric_limits::infinity()},{}).normalized_lower); + assert(!quadratic_bound(q,{0},{1e308},{}).normalized_lower); + SolveOptions o;o.time_limit_seconds=0;SolveBudget budget(o);assert(!quadratic_bound(q,{0},{0},{},&budget).normalized_lower); + std::cout<<"quadratic bound: "< +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace Gecode::Optimize; +using I=std::int64_t; +using Word=std::vector; +namespace { +std::size_t configurations=0,assignments=0,solves=0; +// Generate the finite accepted language from labeled paths. This never looks +// up the next transition by a proposed word and never uses the common checker. +std::set language(const RegularData& data) { + std::set accepted;Word word; + const auto visit=[&](const auto& self,std::uint64_t state)->void { + if(word.size()==data.variables.size()){ + if(std::find(data.final_states.begin(),data.final_states.end(),state)!=data.final_states.end())accepted.insert(word); + return; + } + for(const auto& edge:data.transitions)if(edge.from==state){word.push_back(edge.symbol);self(self,edge.to);word.pop_back();} + };visit(visit,data.initial_state);return accepted; +} +I objective(const ModelSnapshot& model,const std::vector& point) { + I result=static_cast(model.objective.offset); + for(const auto& term:model.objective.terms)result+=static_cast(term.coefficient)*static_cast(point[term.variable.id]); + return result; +} +std::vector> points(const ModelSnapshot& model,const RegularData& data) { + const auto accepted=language(data);std::vector> feasible; + std::vector point(model.variables.size(),std::numeric_limits::quiet_NaN()); + const auto visit=[&](const auto& self,std::size_t index)->void { + if(index(variable.lower),upper=static_cast(variable.upper); + assert(upper-lower<=5); + for(I value=lower;value<=upper;++value)if(value>=variable.lower||(variable.type==VariableType::SemiInteger&&value==0)){point[index]=value;self(self,index+1);} + return; + } + Word word;for(auto variable:data.variables)word.push_back(static_cast(point[variable.id])); + const bool expected=accepted.count(word)!=0;const auto checked=validate(model,point,0,0); + assert(checked.valid==expected);++assignments; + if(expected){assert(checked.objective==objective(model,point));feasible.push_back(point);} + };visit(visit,0);++configurations;return feasible; +} +void checked_result(const ModelSnapshot& model,const RegularData& data,const SolveResult& result, + const std::vector>& feasible,bool complete) { + assert(result.model_id==model.model_id&&result.revision==model.revision); + if(result.has_solution()){ + assert(result.solution_validated&&result.guarantee==Guarantee::Exact); + assert(result.values.size()==model.variables.size()&&result.active_variables.size()==model.variables.size()); + for(std::size_t i=0;i(result.values[variable.id])); + assert(language(data).count(word));assert(result.objective==objective(model,result.values)); + } + std::optional best; + for(const auto& point:feasible){auto value=objective(model,point);if(!best||(model.objective.sense==ObjectiveSense::Minimize?value<*best:value>*best))best=value;} + if(result.best_bound&&best)assert(model.objective.sense==ObjectiveSense::Minimize?*result.best_bound<=*best:*result.best_bound>=*best); + if(complete){ + assert(result.termination==(best?Termination::Optimal:Termination::Infeasible)); + assert(result.has_solution()==bool(best));if(best)assert(result.objective==*best&&result.best_bound==*best); + } +} +void solve_routes(const ModelSnapshot& model,const RegularData& data,const std::vector>& feasible,bool limits=false) { + SolveOptions options;options.backend=Backend::Native;options.guarantee=Guarantee::Exact; + if(!native_capabilities().available){auto result=solve_native(model,options);assert(result.termination==Termination::Unsupported&&!result.has_solution());++solves;return;} + auto result=solve_native(model,options);checked_result(model,data,result,feasible,true);++solves; + NativeSearchOptions search;search.solve=options; + for(auto strategy:{NativeSearchOrder::DepthFirst,NativeSearchOrder::BestBound}){ + search.order=strategy;auto solved=solve_native_search(model,search);checked_result(model,data,solved.result,feasible,true);++solves; + if(limits)for(std::uint64_t cap=0;cap<=solved.frontier.admitted_nodes+1;++cap){search.solve.node_limit=cap;auto stopped=solve_native_search(model,search);checked_result(model,data,stopped.result,feasible,false);search.solve.node_limit.reset();++solves;} + } + if(native_lp_capabilities().available){NativeLpOptions lp;lp.solve=options;auto solved=solve_native_lp(model,lp);checked_result(model,data,solved.result,feasible,true);++solves;} + for(const auto& point:feasible){options.primal_start.clear();for(const auto& variable:model.variables)if(variable.active)options.primal_start.push_back({variable.variable,point[variable.variable.id]}); + auto started=solve_native(model,options);assert(started.start_submitted==!options.primal_start.empty());checked_result(model,data,started,feasible,true);++solves;if(!limits)break; + } +} +void exhaustive(){ + // Every partial deterministic transition function on two states and signed + // alphabet {-1,1}, every initial/final set, word lengths zero through three. + for(unsigned code=0;code<81;++code)for(unsigned initial=0;initial<2;++initial)for(unsigned final=0;final<4;++final)for(unsigned size=0;size<=3;++size){ + Model model;RegularData data;data.state_count=2;data.initial_state=initial; + for(unsigned i=0;i::max(); + RegularData sparse_data{{a},huge,huge-2,{{huge-2,-1,huge-2},{2,1,3}},{huge-2,huge-2,huge-3}}; + sparse.add_global(sparse_data);sparse.minimize({{a,1}});auto s=sparse.snapshot();solve_routes(s,sparse_data,points(s,sparse_data),true); + // Zero is a real typed state and symbol; missing transitions reject. + Model zero;auto z=zero.add_binary();RegularData zdata{{z},1,0,{{0,0,0}},{0}};zero.add_global(zdata); + auto zs=zero.snapshot();solve_routes(zs,zdata,points(zs,zdata),true); + SolveOptions opts;opts.backend=Backend::Native;opts.guarantee=Guarantee::Exact;opts.primal_start={{z,1}}; + assert(solve_native(zero,opts).termination==(native_capabilities().available?Termination::InvalidModel:Termination::Unsupported)); + for(int stop=0;stop<3;++stop){opts.primal_start={{z,0}};if(stop==0)opts.node_limit=0;if(stop==1){opts.node_limit.reset();opts.time_limit_seconds=0;} + if(stop==2){opts.time_limit_seconds=std::numeric_limits::infinity();opts.cancellation=std::make_shared();opts.cancellation->cancel();} + const auto result=solve_native(zero,opts);assert(!result.start_submitted&&!result.has_solution()); + } + const auto zpoints=points(zs,zdata); + std::vector> jobs;for(int i=0;i<4;++i)jobs.push_back(std::async(std::launch::async,[zs,zdata,zpoints]{SolveOptions o;o.backend=Backend::Native;o.guarantee=Guarantee::Exact;auto r=solve_native(zs,o);if(native_capabilities().available)checked_result(zs,zdata,r,zpoints,true);})); + for(auto& job:jobs)job.get(); +} +void invalid(){ + Model model;const auto x=model.add_integer(0,1);RegularData valid{{x},2,0,{{0,0,1}},{1}}; + const auto reject=[&](RegularData data){const auto before=model.revision();bool failed=false;try{model.add_global(data);}catch(const ModelError&){failed=true;}assert(failed&&model.revision()==before&&model.snapshot().globals.empty());++configurations;}; + auto data=valid;data.state_count=0;reject(data);data=valid;data.initial_state=2;reject(data); + data=valid;data.transitions[0].from=2;reject(data);data=valid;data.transitions[0].to=2;reject(data); + data=valid;data.final_states={2};reject(data);data=valid;data.transitions.push_back(data.transitions[0]);reject(data); + data.transitions.back().to=0;reject(data);data=valid;data.transitions[0].symbol=9007199254740993LL;reject(data); + Model other;auto foreign=other.add_binary();data=valid;data.variables={foreign};reject(data); + auto dead=model.add_integer(0,1);model.remove(dead);data=valid;data.variables={dead};reject(data); + auto continuous=model.add_continuous(0,1);data=valid;data.variables={continuous};reject(data); + model.add_global(valid);auto snapshot=model.snapshot(); + for(int failure=0;failure<6;++failure){auto corrupt=snapshot;auto& original=std::get(corrupt.globals[0].payload); + if(failure==0)original.initial_state=2;if(failure==1)original.transitions.push_back(original.transitions[0]); + if(failure==2)original.final_states={2};if(failure==3)original.variables={foreign};if(failure==4)original.variables={dead}; + if(failure==5){corrupt.globals[0].active=false;original.variables.clear();original.state_count=0;} + bool failed=false;try{validate_structure(corrupt);}catch(const ModelError&){failed=true;}assert(failed);++configurations; + assert(solve_native(corrupt).termination==Termination::InvalidModel); + } + // Exact public constants can exceed native symbols even for an empty word; + // unused transitions do not bypass full native admission. + for(bool empty:{false,true}){Model large;auto v=large.add_integer(0,0); + RegularData wide{empty?std::vector{}:std::vector{v},1,0,{{0,0,0},{0,9007199254740992LL,0}},{0}}; + large.add_global(wide);assert(validate(large.snapshot(),{0},0,0).valid); + assert(solve_native(large).termination==Termination::Unsupported); + } +} +void combined_original(){ + Model model;auto x=model.add_binary(),y=model.add_binary(),b=model.add_binary(); + RegularData data{{x,y},3,0,{{0,0,1},{0,1,2},{1,0,1},{1,1,1},{2,0,1}},{1}}; + model.add_global(data);add_all_different(model,{x,y}); + model.add_row({{x,1},{y,1}},1,std::numeric_limits::infinity()); + const auto indicator=add_indicator(model,b,true,{{x,1}},1,std::numeric_limits::infinity()); + assert(indicator.inactive_gate);model.maximize({{x,-2},{y,1},{b,4}},-3); + const auto snapshot=model.snapshot();std::vector> feasible; + for(int xv=0;xv<=1;++xv)for(int yv=0;yv<=1;++yv)for(int bv=0;bv<=1;++bv)for(int gate=0;gate<=1;++gate){ + std::vector point(snapshot.variables.size());point[x.id]=xv;point[y.id]=yv;point[b.id]=bv;point[indicator.inactive_gate->id]=gate; + const bool expected=!(xv&&yv)&&xv!=yv&&xv+yv>=1&&(!bv||xv==1)&&gate==1-bv; + assert(validate(snapshot,point,0,0).valid==expected);++assignments;if(expected)feasible.push_back(point); + } + ++configurations;solve_routes(snapshot,data,feasible,true); + if(native_capabilities().available){ + NativeSearchOptions options;options.solve.backend=Backend::Native;options.solve.guarantee=Guarantee::Exact; + options.branching=NativeBranchingSettings{}; + if(native_lp_capabilities().available)options.relaxation=NativeLpSettings{}; + const auto searched=solve_native_search(snapshot,options);checked_result(snapshot,data,searched.result,feasible,true);++solves; + } +} +void cross_boundaries(){ + Model model;auto dead=model.add_binary();model.remove(dead);auto x=model.add_integer(-1,1); + RegularData data{{x},1,0,{{0,-1,0},{0,1,0}},{0}}; + const auto handle=model.add_global(data);model.minimize({{x,1}},2); + const auto snapshot=model.snapshot();const auto feasible=points(snapshot,data);solve_routes(snapshot,data,feasible,true); + auto automatic=solve(snapshot);if(native_capabilities().available){assert(automatic.backend=="Gecode native");assert(automatic.objective==1);}else assert(automatic.termination==Termination::Unsupported); + SolveOptions highs;highs.backend=Backend::Highs; + assert(solve(snapshot,highs).termination==Termination::Unsupported); + assert(analyze_conflict(snapshot).status==ConflictStatus::Unsupported); + assert(presolve_integer(snapshot).status==PresolveStatus::Unsupported); + assert(relax_feasibility(snapshot).termination==Termination::Unsupported); + SolveSession session;assert(session.solve(snapshot).termination==Termination::Unsupported); + const auto path=std::filesystem::temp_directory_path()/("gecode-regular-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())+".lp"); + {std::ofstream out(path);out<<"unchanged destination";} + bool rejected=false;try{write_model(snapshot,path.string());}catch(const ModelError&){rejected=true;}assert(rejected); + {std::ifstream in(path);std::string contents((std::istreambuf_iterator(in)),{});assert(contents=="unchanged destination");} + std::filesystem::remove(path); + // The original word is rechecked after numerical rounding, with no row-only + // shortcut; exact native start admission never rounds a near-integer hint. + assert(validate(snapshot,{0,-1.0000001},1e-6,1e-6).valid); + assert(!validate(snapshot,{0,0},0,0).valid); + SolveOptions start;start.backend=Backend::Native;start.guarantee=Guarantee::Exact;start.primal_start={{x,-1.0000001}}; + assert(solve_native(snapshot,start).termination==(native_capabilities().available?Termination::InvalidModel:Termination::Unsupported)); + model.remove(handle);model.minimize({});model.remove(x);validate_structure(model.snapshot()); + const auto id=model.id();Model moved(std::move(model));assert(moved.id()==id);validate_structure(moved.snapshot()); + assert(snapshot.globals[0].active&&snapshot.variables[x.id].active); +} +void size_boundaries(){ + const auto okay=[](std::uint64_t w,std::uint64_t q,std::uint64_t t,std::uint64_t a,bool small=false){return Detail::native_regular_size_error(w,q,t,a,small)==nullptr;}; + constexpr auto imax=static_cast(std::numeric_limits::max()); + constexpr auto umax=static_cast(std::numeric_limits::max()); + assert(okay(imax-1,1,0,0));assert(!okay(imax,1,0,0));assert(okay(0,imax-1,0,0));assert(!okay(0,imax,0,0)); + assert(okay(0,1,imax-1,0));assert(!okay(0,1,imax,0));assert(!okay(0,0,0,0)); + assert(okay(1,imax/2,0,0));assert(!okay(1,imax/2+1,0,0)); + assert(okay(100000,1,umax/100000,1));assert(!okay(100000,1,umax/100000+1,1)); + const auto hash=imax/2+1;assert(okay(0,1,hash-1,hash-1));assert(!okay(0,1,hash,hash)); + const auto short_max=std::numeric_limits::max();assert(okay(1,1,short_max,short_max,true));assert(!okay(1,1,std::uint64_t(short_max)+1,std::uint64_t(short_max)+1,true)); + assert(!okay(std::numeric_limits::max(),1,0,0)); + // This concrete alphabet fits int16 values but has 65536 possible supports. + // Do not enter the native graph's ushort support-count overflow. + Model model;auto x=model.add_integer(0,0);RegularData data{{x},1,0,{},{0}}; + for(I symbol=std::numeric_limits::min();symbol<=std::numeric_limits::max();++symbol)data.transitions.push_back({0,symbol,0}); + model.add_global(data);assert(validate(model.snapshot(),{0},0,0).valid); + auto result=solve_native(model);assert(result.termination==Termination::Unsupported); + if(native_capabilities().available)assert(result.message.find("short-symbol")!=std::string::npos); +} +} +int main(){size_boundaries();exhaustive();variants_and_history();invalid();combined_original();cross_boundaries();std::cout< +#include +#include +#include +#include +#define OK(call) do {int32_t code=(call);if(code)fprintf(stderr,"%s: %s\n",#call,gecode_opt_v1_last_error());assert(code==GECODE_OPT_OK);}while(0) +static gecode_opt_regular_transition_v1 edge(uint64_t from,int64_t symbol,uint64_t to) { + gecode_opt_regular_transition_v1 out;memset(&out,0,sizeof(out));out.struct_size=sizeof(out);out.from=from;out.symbol=symbol;out.to=to;return out; +} +static int accepted(int x,int y,int z) { + /* This language is exactly signed words with an even count of +1 symbols. */ + return (x==-1||x==1)&&(y==-1||y==1)&&(z==-1||z==1)&&((x==1)+(y==1)+(z==1))%2==0; +} +int main(void) { + int32_t native=0,lp=0,mip=0,maximize; + OK(gecode_opt_v1_capabilities(GECODE_OPT_NATIVE,&native,&lp,&mip)); + for(maximize=0;maximize<2;++maximize) { + gecode_opt_handle model=0,foreign_model=0,result=0,rejected=0; + gecode_opt_id vars[3],dead,foreign,global,row,other,bad,out; + gecode_opt_regular_transition_v1 edges[4],duplicate[2];uint64_t finals[2]={0,0},owner,revision,after; + gecode_opt_term terms[3];gecode_opt_options_v1 options;gecode_opt_result_info_v1 info; + int x,y,z,found=0,best=0;double value=0;int32_t present=0;size_t i; + char label[]="parity"; + edges[0]=edge(0,-1,0);edges[1]=edge(0,1,1);edges[2]=edge(1,-1,1);edges[3]=edge(1,1,0); + OK(gecode_opt_v1_model_create(&model));OK(gecode_opt_v1_model_create(&foreign_model)); + for(i=0;i<3;++i){OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_INTEGER,-1,1,"",&vars[i]));terms[i].variable=vars[i];terms[i].coefficient=1;} + OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_INTEGER,0,1,"deleted",&dead));OK(gecode_opt_v1_model_remove_variable(model,dead)); + OK(gecode_opt_v1_model_add_variable(foreign_model,GECODE_OPT_INTEGER,0,1,"foreign",&foreign)); + OK(gecode_opt_v1_model_identity(model,&owner,&revision)); +#define ADD(v,n,st,in,es,en,sz,fs,fn,name,target) gecode_opt_v1_model_add_regular(model,v,n,st,in,es,en,sz,fs,fn,name,target) +#define BAD(call,expected) do {memset(&out,0x55,sizeof(out));assert((call)==(expected));assert(out.model_id==0&&out.slot==0&&out.kind==0&&out.reserved==0);OK(gecode_opt_v1_model_identity(model,&owner,&after));assert(after==revision);}while(0) + assert(ADD(vars,3,2,0,edges,4,sizeof(edges[0]),finals,2,label,NULL)==GECODE_OPT_INVALID_ARGUMENT); + BAD(ADD(NULL,3,2,0,edges,4,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_INVALID_ARGUMENT); + BAD(ADD(vars,UINT64_MAX,2,0,edges,4,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_INVALID_ARGUMENT); + BAD(ADD(vars,3,2,0,NULL,4,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_INVALID_ARGUMENT); + BAD(ADD(vars,3,2,0,edges,UINT64_MAX,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_INVALID_ARGUMENT); + BAD(ADD(vars,3,2,0,edges,4,sizeof(edges[0])-1,finals,2,label,&out),GECODE_OPT_INVALID_ARGUMENT); + BAD(ADD(vars,3,2,0,edges,4,sizeof(edges[0]),NULL,2,label,&out),GECODE_OPT_INVALID_ARGUMENT); + BAD(ADD(vars,3,2,0,edges,4,sizeof(edges[0]),finals,UINT64_MAX,label,&out),GECODE_OPT_INVALID_ARGUMENT); + edges[3].reserved=1;BAD(ADD(vars,3,2,0,edges,4,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_INVALID_ARGUMENT);edges[3].reserved=0; + edges[3].struct_size--;BAD(ADD(vars,3,2,0,edges,4,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_INVALID_ARGUMENT);edges[3].struct_size++; + BAD(ADD(vars,3,0,0,edges,4,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_MODEL_ERROR); + BAD(ADD(vars,3,2,2,edges,4,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_MODEL_ERROR); + edges[3].to=2;BAD(ADD(vars,3,2,0,edges,4,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_MODEL_ERROR);edges[3].to=0; + edges[3].symbol=INT64_C(9007199254740993);BAD(ADD(vars,3,2,0,edges,4,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_MODEL_ERROR);edges[3].symbol=1; + finals[1]=2;BAD(ADD(vars,3,2,0,edges,4,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_MODEL_ERROR);finals[1]=0; + duplicate[0]=duplicate[1]=edges[0];BAD(ADD(vars,3,2,0,duplicate,2,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_MODEL_ERROR); + duplicate[1].to=1;BAD(ADD(vars,3,2,0,duplicate,2,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_MODEL_ERROR); + BAD(ADD(&foreign,1,2,0,edges,4,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_MODEL_ERROR); + BAD(ADD(&dead,1,2,0,edges,4,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_MODEL_ERROR); + bad=vars[0];bad.reserved=1;BAD(ADD(&bad,1,2,0,edges,4,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_INVALID_ARGUMENT); + bad=vars[0];bad.kind=GECODE_OPT_ROW_ID;BAD(ADD(&bad,1,2,0,edges,4,sizeof(edges[0]),finals,2,label,&out),GECODE_OPT_INVALID_ARGUMENT); + BAD(ADD(vars,3,2,0,edges,4,sizeof(edges[0]),finals,2,"\xed\xa0\x80",&out),GECODE_OPT_INVALID_ARGUMENT); + OK(ADD(vars,3,2,0,edges,4,sizeof(edges[0]),finals,2,label,&global)); + OK(gecode_opt_v1_model_identity(model,&owner,&after));assert(after==revision+1&&global.kind==GECODE_OPT_GLOBAL_ID&&global.model_id==owner); + memset(edges,0,sizeof(edges));memset(finals,0xff,sizeof(finals));label[0]='X'; /* Caller storage cannot alter posted meaning. */ + OK(gecode_opt_v1_model_add_all_different(model,vars,2,"",&other)); + OK(gecode_opt_v1_model_add_row(model,terms,3,-1,INFINITY,"",&row)); + terms[0].coefficient=2;terms[1].coefficient=-1;terms[2].coefficient=3; + OK(gecode_opt_v1_model_set_objective(model,terms,3,maximize,7)); + for(x=-1;x<=1;++x)for(y=-1;y<=1;++y)for(z=-1;z<=1;++z)if(accepted(x,y,z)&&x!=y&&x+y+z>=-1) { + int objective=2*x-y+3*z+7;if(!found||(maximize?objective>best:objective +#include + +#include +#include +#include +#include +#include + +using namespace Gecode::Optimize; +namespace { +constexpr double inf = std::numeric_limits::infinity(); +bool near(double a, double b) { return std::fabs(a-b) < 1e-6; } +void repaired(const RelaxationResult& result, double violation) { + assert(result.termination == Termination::Optimal); + assert(result.has_repair() && result.minimum_violation_established); + assert(result.guarantee == Guarantee::Numerical); + assert(result.minimum_weighted_violation && near(*result.minimum_weighted_violation, violation)); + assert(result.weighted_violation && near(*result.weighted_violation, violation)); + assert(result.private_model && result.private_model->model_id != result.source_model_id); + assert(result.workflow.final_solution.model_id == result.private_model->model_id); + assert(validate(*result.private_model, result.workflow.final_solution.values).valid); + double sum = 0; + for (const auto& item : result.items) { + assert(item.activity && item.violation && item.weighted_violation && item.slack_value); + assert(*item.violation >= 0 && *item.slack_value >= -1e-7); + assert(near(*item.weighted_violation, item.penalty * *item.violation)); + assert(item.source_row.has_value() != item.source_variable.has_value()); + sum += *item.weighted_violation; + } + assert(near(sum, violation)); +} +} + +#ifdef GECODE_RELAXATION_TEST_FAKE_SOLVER +namespace { +enum class Scenario { Good, IncompleteFirst, IncompleteSecond, CancelFirst, CancelSecond, + BadPrimal, BadObjective, BadBound, OpenBound, MissingBound, BadSecondBound, + FalseInfeasible, FirstInfeasible, BadOwner, BadMask, IncompleteBadOwner, IncompleteBadRevision, OffsetHonest, OffsetForged, ResidualHonest, ResidualForged }; +Scenario scenario = Scenario::Good; +unsigned calls = 0; +double previous_remaining = inf; +} +namespace Gecode { namespace Optimize { +SolveResult solve(const ModelSnapshot& model, const SolveOptions& options) { + assert(options.primal_start.empty()); + assert(options.relative_gap == 0 && options.absolute_gap == 0); + assert(options.time_limit_seconds <= previous_remaining); + previous_remaining = options.time_limit_seconds; + const bool residual_case = scenario == Scenario::ResidualHonest || scenario == Scenario::ResidualForged; + const bool offset_case = scenario == Scenario::OffsetHonest || scenario == Scenario::OffsetForged || residual_case; + assert(model.variables.size() == (offset_case ? 3u : 2u)); + SolveResult result; + result.model_id = model.model_id; result.revision = model.revision; + result.backend = "deterministic repair oracle"; + result.values = offset_case ? std::vector{1, 1e6, 1} : std::vector{1, 1}; + if (scenario == Scenario::ResidualForged) result.values[2] = 0; + result.active_variables.assign(model.variables.size(), true); + result.objective = calls == 0 || offset_case ? 1.0 : 8.0; + if ((scenario == Scenario::OffsetForged && calls == 1) || scenario == Scenario::ResidualForged) result.objective = 0; + result.best_bound = result.objective; + result.solution_validated = true; + result.termination = Termination::Optimal; + result.update_gaps(model.objective.sense); + if (scenario == Scenario::BadOwner || scenario == Scenario::IncompleteBadOwner) ++result.model_id; + if (scenario == Scenario::IncompleteBadRevision) ++result.revision; + if (scenario == Scenario::BadMask) result.active_variables[0] = false; + if (scenario == Scenario::BadPrimal) result.values = {2, 0}; + if (scenario == Scenario::BadObjective) { + result.objective = 0; result.best_bound = 0; + } + if (scenario == Scenario::BadBound || (scenario == Scenario::BadSecondBound && calls == 1)) + result.best_bound = *result.objective + 1; + if (scenario == Scenario::OpenBound) result.best_bound = 0; + // Keep a forged zero absolute_gap to verify callers recompute bound evidence. + if (scenario == Scenario::MissingBound) { + result.best_bound.reset(); result.absolute_gap.reset(); result.relative_gap.reset(); + } + if (scenario == Scenario::IncompleteBadOwner || scenario == Scenario::IncompleteBadRevision) + result.termination = Termination::TimeLimit; + if ((calls == 0 && scenario == Scenario::IncompleteFirst) || + (calls == 1 && scenario == Scenario::IncompleteSecond)) result.termination = Termination::TimeLimit; + if ((calls == 0 && scenario == Scenario::CancelFirst) || + (calls == 1 && scenario == Scenario::CancelSecond)) options.cancellation->cancel(); + if (scenario == Scenario::FirstInfeasible || (calls == 1 && scenario == Scenario::FalseInfeasible)) { + result.termination = Termination::Infeasible; + result.solution_validated = false; result.values.clear(); result.objective.reset(); + } + ++calls; + return result; +} +}} +int main() { + Model model; + auto x = model.add_integer(0, 1); + auto demand = model.add_row({{x, 1}}, 2, inf); + model.minimize({{x, 1}}, 7); + for (auto selected : {Scenario::Good, Scenario::IncompleteFirst, Scenario::IncompleteSecond, + Scenario::CancelFirst, Scenario::CancelSecond, Scenario::BadPrimal, + Scenario::BadObjective, Scenario::BadBound, Scenario::OpenBound, Scenario::MissingBound, + Scenario::BadSecondBound, Scenario::FalseInfeasible, Scenario::FirstInfeasible, + Scenario::BadOwner, Scenario::BadMask, Scenario::IncompleteBadOwner, Scenario::IncompleteBadRevision}) { + scenario = selected; calls = 0; previous_remaining = inf; + RelaxationOptions options; + options.rows = {{demand, RelaxationSide::Lower, 1}}; + options.optimize_original_objective = true; + options.solve.time_limit_seconds = 60; + auto result = relax_feasibility(model, options); + assert(calls >= 1 && calls <= 2); + assert(model.row(demand).lower == 2); + if (result.minimum_violation_established) { + assert(result.violation_lock && result.private_model); + assert(result.violation_lock->model_id == result.private_model->model_id); + assert(result.violation_lock->id + 1 == result.private_model->rows.size()); + assert(result.private_model->rows.back().upper == 1); + if (result.has_repair()) assert(validate(*result.private_model, result.workflow.final_solution.values).valid); + } + if (selected == Scenario::Good) { + repaired(result, 1); + assert(result.original_objective_optimized && result.violation_lock); + assert(!result.original_validation.valid); + } else { + assert(!result.original_objective_optimized); + if (selected == Scenario::IncompleteFirst) { + assert(result.termination == Termination::TimeLimit && calls == 1); + assert(result.has_repair() && !result.minimum_violation_established); + } else if (selected == Scenario::IncompleteSecond) { + assert(result.termination == Termination::TimeLimit && calls == 2); + assert(result.has_repair() && result.minimum_violation_established && result.violation_lock); + } else if (selected == Scenario::CancelFirst || selected == Scenario::CancelSecond) { + assert(result.termination == Termination::Cancelled && !result.has_repair()); + assert(!result.minimum_violation_established); + } else if (selected == Scenario::FirstInfeasible) { + assert(result.termination == Termination::Infeasible && !result.has_repair()); + assert(!result.minimum_violation_established); + } else { + assert(result.termination == Termination::NumericalFailure); + // A later contradictory oracle does not revoke earlier valid evidence. + assert(!result.minimum_violation_established || selected == Scenario::FalseInfeasible || + selected == Scenario::BadSecondBound); + } + } + } + // Exact arithmetic total is 1, even when long double == double and the + // objective offset cancels a 1e16 linear term. A forged zero bound is invalid. + for (auto selected : {Scenario::OffsetHonest, Scenario::OffsetForged}) { + scenario = selected; calls = 0; previous_remaining = inf; + Model cancellation; + auto a = cancellation.add_integer(1, 1); + auto b = cancellation.add_integer(1e6, 1e6); + auto constant = cancellation.add_row({}, 1, inf); + cancellation.minimize({{a, 1}, {b, 1e10}}, -1e16); + RelaxationOptions options; + options.rows = {{constant, RelaxationSide::Lower, 1}}; + options.optimize_original_objective = true; + auto result = relax_feasibility(cancellation, options); + assert(calls == 2); + if (selected == Scenario::OffsetHonest) { + repaired(result, 1); + assert(result.original_objective == 1 && result.original_objective_optimized); + } else { + assert(result.termination == Termination::NumericalFailure); + assert(!result.original_objective_optimized); + assert(result.minimum_violation_established); // the valid first stage remains established + } + } + for (auto selected : {Scenario::ResidualHonest, Scenario::ResidualForged}) { + scenario = selected; calls = 0; previous_remaining = inf; + Model cancellation; + auto a = cancellation.add_integer(1, 1); + auto b = cancellation.add_integer(1e6, 1e6); + auto row = cancellation.add_row({{a, 1}, {b, 1e10}}, -inf, 1e16); + RelaxationOptions options; options.rows = {{row, RelaxationSide::Upper, 1}}; + auto result = relax_feasibility(cancellation, options); + assert(calls == 1); + if (selected == Scenario::ResidualHonest) { + repaired(result, 1); + assert(result.items[0].violation == 1); + assert(!result.original_validation.valid && result.original_validation.max_row_violation == 1); + } else { + assert(result.termination == Termination::NumericalFailure); + assert(!result.has_repair() && !result.minimum_violation_established); + } + } + calls = 0; + RelaxationOptions options; options.rows = {{demand, RelaxationSide::Lower, 1}}; + options.solve.time_limit_seconds = 0; + auto result = relax_feasibility(model, options); + assert(result.termination == Termination::TimeLimit && calls == 0 && !result.has_repair()); + options.solve.time_limit_seconds = inf; + options.solve.node_limit = 0; + result = relax_feasibility(model, options); + assert(result.termination == Termination::NodeLimit && calls == 0); + options.optimize_original_objective = true; + options.solve.node_limit = 10; + result = relax_feasibility(model, options); + assert(result.termination == Termination::Unsupported && calls == 0); +} +#else +namespace { +void row_and_bound_oracles() { + for (auto sense : {ObjectiveSense::Minimize, ObjectiveSense::Maximize}) { + Model model; + auto x = model.add_integer(0, 4, "x"); + auto y = model.add_integer(1, 2, "y"); + model.add_row({{x, 1}}, -2, inf); // hard finite range for independent enumeration + model.add_row({{y, 1}}, -inf, 3); + model.add_row({{x, 1}, {y, 1}}, -inf, 4); + auto demand = model.add_row({{x, 1}, {y, 1}}, 5, inf, "demand"); + auto balance = model.add_row({{x, 1}, {y, -1}}, -inf, 0, "balance"); + model.set_objective({{x, -2}, {y, 3}}, sense, -17); + const auto before = model.snapshot(); + RelaxationOptions options; + options.rows = {{balance, RelaxationSide::Upper, 2}, {demand, RelaxationSide::Lower, 3}}; + options.bounds = {{y, RelaxationSide::Upper, 4}, {x, RelaxationSide::Lower, 1}}; + options.optimize_original_objective = true; + double best_penalty = inf, best_objective = sense == ObjectiveSense::Minimize ? inf : -inf; + std::vector> best_points; + for (int a = -2; a <= 4; ++a) for (int b = 1; b <= 3; ++b) if (a+b <= 4) { + const double penalty = 3*std::max(0, 5-a-b) + 2*std::max(0, a-b) + + std::max(0, -a) + 4*std::max(0, b-2); + const double objective = -2*a + 3*b - 17; + if (penalty < best_penalty) { + best_penalty = penalty; + best_objective = sense == ObjectiveSense::Minimize ? inf : -inf; + best_points.clear(); + } + if (penalty != best_penalty) continue; + if ((sense == ObjectiveSense::Minimize && objective < best_objective) || + (sense == ObjectiveSense::Maximize && objective > best_objective)) { + best_objective = objective; best_points.clear(); + } + if (objective == best_objective) best_points.push_back({double(a), double(b)}); + } + auto result = relax_feasibility(model, options); + repaired(result, best_penalty); + assert(result.original_objective_optimized && result.violation_lock); + assert(near(*result.original_objective, best_objective)); + assert(std::find(best_points.begin(), best_points.end(), result.original_values) != best_points.end()); + assert(!result.original_validation.valid && result.original_validation.model_valid); + assert(result.items[0].source_row->id == demand.id && result.items[1].source_row->id == balance.id); + assert(result.items[2].source_variable == x && result.items[3].source_variable == y); + assert(result.private_variables.size() == 2); + assert(result.private_model->variables[0].type == VariableType::Integer); + bool foreign = false; + try { (void)result.workflow.final_solution.value(x); } catch (const ModelError&) { foreign = true; } + assert(foreign); + assert(model.id() == before.model_id && model.revision() == before.revision); + assert(model.row(demand).lower == 5 && model.row(balance).upper == 0); + assert(model.variable(x).lower == 0 && model.variable(y).upper == 2); + assert(model.snapshot().rows.size() == before.rows.size()); + assert(model.snapshot().objective.offset == -17 && model.snapshot().objective.sense == sense); + } +} +void penalty_priorities_and_ties() { + for (auto sense : {ObjectiveSense::Minimize, ObjectiveSense::Maximize}) { + Model model; auto x = model.add_integer(0, 4); + auto lower = model.add_row({{x, 1}}, 3, inf); + auto upper = model.add_row({{x, 1}}, -inf, 1); + model.set_objective({{x, 2}}, sense, -11); + RelaxationOptions options; + options.rows = {{lower, RelaxationSide::Lower, 1}, {upper, RelaxationSide::Upper, 1}}; + options.optimize_original_objective = true; + auto result = relax_feasibility(model, options); repaired(result, 2); + assert(result.original_objective_optimized); + assert(near(result.original_values[0], sense == ObjectiveSense::Minimize ? 1 : 3)); + assert(near(*result.original_objective, sense == ObjectiveSense::Minimize ? -9 : -5)); + // Positive weights change the optimum before the original objective matters. + options.rows[0].penalty = 3; + result = relax_feasibility(model, options); repaired(result, 2); + assert(near(result.original_values[0], 3)); + options.rows[0].penalty = 1; options.rows[1].penalty = 3; + result = relax_feasibility(model, options); repaired(result, 2); + assert(near(result.original_values[0], 1)); + } +} +void continuous_and_constants() { + Model model; + auto x = model.add_continuous(0, 10); + auto row = model.add_row({{x, 1}}, 3, 7); + auto hard = model.add_row({{x, 1}}, 9, inf); + RelaxationOptions options; options.rows = {{row, RelaxationSide::Upper, 2}}; + auto result = relax_feasibility(model, options); + repaired(result, 4); assert(near(result.original_values[0], 9)); + assert(!result.original_objective_optimized && !result.violation_lock); + model.remove(hard); + result = relax_feasibility(model, options); repaired(result, 0); + assert(result.original_validation.valid); + + Model constants; + auto positive = constants.add_row({}, 2, inf, "constant lower"); + auto negative = constants.add_row({}, -inf, -3, "constant upper"); + constants.minimize({}, 13); + options.rows = {{negative, RelaxationSide::Upper, 5}, {positive, RelaxationSide::Lower, 2}}; + options.optimize_original_objective = true; + result = relax_feasibility(constants, options); repaired(result, 19); + assert(result.original_values.empty() && result.original_objective_optimized); + assert(result.original_objective == 13 && !result.original_validation.valid); + assert(result.items[0].violation == 2 && result.items[1].violation == 3); + + Model hard_integer; + auto integer = hard_integer.add_integer(0.2, 0.8); + result = relax_feasibility(hard_integer); + assert(result.termination == Termination::Infeasible && !result.has_repair()); + options = {}; options.bounds = {{integer, RelaxationSide::Lower, 2}}; + result = relax_feasibility(hard_integer, options); repaired(result, 0.4); + assert(result.original_values[0] == 0 && result.private_model->variables[0].type == VariableType::Integer); + + Model free; + auto z = free.add_continuous(-inf, inf); + free.maximize({{z, 1}}, -4); + options = {}; options.optimize_original_objective = true; + result = relax_feasibility(free, options); + assert(result.termination == Termination::Unbounded); + assert(result.minimum_violation_established && !result.original_objective_optimized); + assert(result.has_repair() && result.weighted_violation == 0); +} +void binary_semi_indicator() { + for (bool fixed_one : {false, true}) { + Model model; + auto x = model.add_binary(); + model.set_bounds(x, fixed_one ? 1 : 0, fixed_one ? 1 : 0); + model.add_row({{x, 1}}, fixed_one ? 0 : 1, fixed_one ? 0 : 1); + RelaxationOptions options; + options.bounds = {{x, fixed_one ? RelaxationSide::Lower : RelaxationSide::Upper, 3}}; + auto result = relax_feasibility(model, options); repaired(result, 3); + assert(result.original_values[0] == (fixed_one ? 0 : 1)); + assert(result.private_model->variables[0].type == VariableType::Binary); + assert(result.private_model->variables[0].lower == 0 && result.private_model->variables[0].upper == 1); + assert(!result.original_validation.valid); + } + { + Model model; + auto x = model.add_binary(); model.set_bounds(x, 0.25, 0.75); + RelaxationOptions options; + options.bounds = {{x, RelaxationSide::Lower, 2}, {x, RelaxationSide::Upper, 3}}; + auto result = relax_feasibility(model, options); repaired(result, 0.5); + assert(result.original_values[0] == 0); + assert(result.private_model->variables[0].lower == 0 && result.private_model->variables[0].upper == 1); + } + for (auto type : {VariableType::SemiContinuous, VariableType::SemiInteger}) { + Model model; + auto x = model.add_variable(type, 2, 4); + auto row = model.add_row({{x, 1}}, 1, 1); + RelaxationOptions options; + options.rows = {{row, RelaxationSide::Lower, 1}, {row, RelaxationSide::Upper, 3}}; + auto result = relax_feasibility(model, options); repaired(result, 1); + assert(result.original_values[0] == 0 && !result.original_validation.valid); + options.bounds = {{x, RelaxationSide::Upper, 1}}; + result = relax_feasibility(model, options); + assert(result.termination == Termination::Unsupported && !result.has_repair()); + } + Model model; + auto active = model.add_binary(); auto x = model.add_integer(0, 4); + model.set_bounds(active, 1, 1); + auto indicator = add_indicator(model, active, true, {{x, 1}}, 3, inf); + auto row = model.add_row({{x, 1}}, -inf, 1); + model.maximize({{x, 1}}, 31); + RelaxationOptions options; options.rows = {{row, RelaxationSide::Upper, 1}}; + options.optimize_original_objective = true; + const auto before = model.snapshot(); + auto result = relax_feasibility(model, options); repaired(result, 2); + assert(result.original_values[x.id] == 3 && result.original_objective_optimized); + assert(result.private_model->indicators.size() == 1); + assert(result.private_model->indicators[0].activator.model_id == result.private_model->model_id); + assert(model.revision() == before.revision && model.snapshot().indicators[0].active); + options.bounds = {{x, RelaxationSide::Upper, 1}}; + assert(relax_feasibility(model, options).termination == Termination::Unsupported); + options.bounds.clear(); options.rows = {{indicator.rows[0], RelaxationSide::Lower, 1}}; + assert(relax_feasibility(model, options).termination == Termination::Unsupported); +} +void rejected_and_stopped() { + Model model; auto x = model.add_continuous(0, 2); + auto row = model.add_row({{x, 1}}, 3, inf); + RelaxationOptions options; options.rows = {{row, RelaxationSide::Lower, 1}}; + for (double penalty : {0.0, -1.0, inf, std::numeric_limits::quiet_NaN()}) { + options.rows[0].penalty = penalty; + assert(relax_feasibility(model, options).termination == Termination::InvalidModel); + } + options.rows[0].penalty = 1; + options.rows.push_back(options.rows[0]); + assert(relax_feasibility(model, options).termination == Termination::InvalidModel); + options.rows.pop_back(); options.rows[0].side = RelaxationSide::Upper; + assert(relax_feasibility(model, options).termination == Termination::InvalidModel); + options.rows[0].side = static_cast(42); + assert(relax_feasibility(model, options).termination == Termination::InvalidModel); + options.rows[0].side = RelaxationSide::Lower; + options.rows[0].row.model_id += 100000; + assert(relax_feasibility(model, options).termination == Termination::InvalidModel); + options.rows[0].row = row; + options.solve.primal_start = {{x, 0}}; + assert(relax_feasibility(model, options).termination == Termination::Unsupported); + options.solve.primal_start.clear(); + for (auto guarantee : {Guarantee::Exact, Guarantee::Certified}) { + options.solve.guarantee = guarantee; + assert(relax_feasibility(model, options).termination == Termination::Unsupported); + } + options.solve.guarantee = Guarantee::Numerical; + options.solve.time_limit_seconds = 0; + assert(relax_feasibility(model, options).termination == Termination::TimeLimit); + options.solve.time_limit_seconds = inf; + options.solve.cancellation = std::make_shared(); options.solve.cancellation->cancel(); + assert(relax_feasibility(model, options).termination == Termination::Cancelled); + options.solve.cancellation.reset(); options.solve.node_limit = 0; + assert(relax_feasibility(model, options).termination == Termination::NodeLimit); + options.solve.node_limit = 1; options.optimize_original_objective = true; + assert(relax_feasibility(model, options).termination == Termination::Unsupported); + options.solve.node_limit.reset(); options.optimize_original_objective = false; + auto malformed = model.snapshot(); malformed.rows[0].terms[0].coefficient = inf; + assert(relax_feasibility(malformed, options).termination == Termination::InvalidModel); + auto with_global = model.snapshot(); + with_global.globals.push_back({{model.id(), 0}, AllDifferentData{}, "", true}); + assert(relax_feasibility(with_global, options).termination == Termination::Unsupported); + with_global.globals[0].active = false; + auto result = relax_feasibility(with_global, options); repaired(result, 1); + assert(result.private_model->globals.empty()); + with_global.globals[0].global.model_id += 100000; + assert(relax_feasibility(with_global, options).termination == Termination::InvalidModel); + model.remove(row); + assert(relax_feasibility(model, options).termination == Termination::InvalidModel); + Model tombstones; + auto deleted = tombstones.add_binary(); tombstones.remove(deleted); + auto only = tombstones.add_integer(0, 1); + auto missing = tombstones.add_row({{only, 1}}, 2, inf); + options = {}; options.rows = {{missing, RelaxationSide::Lower, 1}}; + result = relax_feasibility(tombstones, options); repaired(result, 1); + assert(result.original_values.size() == 2 && result.private_variables.size() == 2); + assert(!result.private_model->variables[0].active); + // Untrusted snapshots can use the allocator's next otherwise valid identity. + Model future; + auto snapshot = future.snapshot(); ++snapshot.model_id; + result = relax_feasibility(snapshot); repaired(result, 0); + assert(result.private_model->model_id != snapshot.model_id); +} +} +int main() { + if (!capabilities(Backend::Highs).available) { + Model model; auto x = model.add_integer(0, 1); + auto row = model.add_row({{x, 1}}, 2, inf); + const auto revision = model.revision(); + RelaxationOptions options; options.rows = {{row, RelaxationSide::Lower, 1}}; + auto result = relax_feasibility(model, options); + assert(result.termination == Termination::Unsupported); + assert(!result.has_repair() && !result.minimum_violation_established && !result.original_objective_optimized); + assert(model.revision() == revision && model.row(row).lower == 2); + options.solve.time_limit_seconds = 0; + assert(relax_feasibility(model, options).termination == Termination::TimeLimit); + options.solve.time_limit_seconds = inf; + options.solve.cancellation = std::make_shared(); options.solve.cancellation->cancel(); + assert(relax_feasibility(model, options).termination == Termination::Cancelled); + return 0; + } + row_and_bound_oracles(); + penalty_priorities_and_ties(); + continuous_and_constants(); + binary_semi_indicator(); + rejected_and_stopped(); +} +#endif diff --git a/test/optimize/result.cpp b/test/optimize/result.cpp new file mode 100644 index 0000000000..5ab8cafdc5 --- /dev/null +++ b/test/optimize/result.cpp @@ -0,0 +1,287 @@ +#include + +#include +#include +#include +#include +#include +#include + +using namespace Gecode::Optimize; + +namespace { +template void rejects(F action) { + bool rejected = false; + try { action(); } catch (const ModelError&) { rejected = true; } + assert(rejected); +} + +void option_tests() { + SolveOptions options; + options.validate(); + assert(options.threads == 1 && options.random_seed == 0); + assert(options.guarantee == Guarantee::Numerical); + const double inf = std::numeric_limits::infinity(); + const double nan = std::numeric_limits::quiet_NaN(); + + for (double invalid : {-1.0, -inf, nan}) { + auto changed = options; + changed.time_limit_seconds = invalid; + rejects([&] { changed.validate(); }); + rejects([&] { SolveBudget budget(changed); }); + } + for (double valid : {0.0, -0.0, 1.0, std::numeric_limits::max(), inf}) { + auto changed = options; + changed.time_limit_seconds = valid; + changed.validate(); + } + for (int invalid : {0, -1}) { + auto changed = options; + changed.threads = invalid; + rejects([&] { changed.validate(); }); + } + auto changed = options; + changed.random_seed = -1; + rejects([&] { changed.validate(); }); + changed = options; + changed.backend = static_cast(999); + rejects([&] { changed.validate(); }); + changed = options; + changed.guarantee = static_cast(999); + rejects([&] { changed.validate(); }); + for (Guarantee policy : {Guarantee::Exact, Guarantee::Certified}) { + changed = options; + changed.guarantee = policy; + changed.validate(); // unsupported policy is a backend result, not bad input + } + for (double invalid : {-1.0, inf, nan}) { + changed = options; + changed.relative_gap = invalid; + rejects([&] { changed.validate(); }); + changed = options; + changed.absolute_gap = invalid; + rejects([&] { changed.validate(); }); + } + for (double invalid : {0.0, -1.0, inf, nan}) { + changed = options; + changed.feasibility_tolerance = invalid; + rejects([&] { changed.validate(); }); + changed = options; + changed.integrality_tolerance = invalid; + rejects([&] { changed.validate(); }); + } + changed = options; + changed.integrality_tolerance = 0.5; + rejects([&] { changed.validate(); }); + changed = options; + changed.relative_gap = 0.0; + changed.absolute_gap = 0.0; + changed.node_limit = 0; + changed.validate(); +} + +void budget_tests() { + SolveOptions options; + SolveBudget unlimited(options); + assert(!unlimited.expired() && !unlimited.stop_reason()); + assert(std::isinf(unlimited.remaining_seconds())); + assert(!unlimited.node_limit_reached()); + unlimited.add_nodes(std::numeric_limits::max()); + unlimited.add_nodes(); + assert(unlimited.nodes() == std::numeric_limits::max()); + assert(!unlimited.node_limit_reached()); // no implicit limit at UINT64_MAX + + SolveBudget parent(options); + auto child=parent.slice(3600,2); + auto nested=child.slice(7200,100); + assert(nested.remaining_seconds()<=3600); + nested.add_nodes(2); + assert(child.node_limit_reached() && nested.node_limit_reached()); + assert(parent.nodes()==2 && !parent.expired()); + auto next=parent.slice(3600,1); + next.add_nodes(); + assert(next.node_limit_reached() && parent.nodes()==3 && !parent.expired()); + auto zero=parent.slice(0,10); + assert(zero.stop_reason()==Termination::TimeLimit && !parent.expired()); + rejects([&]{parent.slice(-1,1);}); + rejects([&]{parent.slice(std::numeric_limits::quiet_NaN(),1);}); + parent.cancellation()->cancel(); + assert(next.cancelled() && child.stop_reason()==Termination::Cancelled); + + options.time_limit_seconds = 0.0; + SolveBudget immediate(options); + assert(immediate.remaining_seconds() == 0.0); + assert(immediate.stop_reason() == Termination::TimeLimit); + options.time_limit_seconds = std::numeric_limits::infinity(); + options.node_limit = 0; + SolveBudget no_nodes(options); + assert(no_nodes.stop_reason() == Termination::NodeLimit); + + options.node_limit = 2; + SolveBudget nodes(options); + auto shared = nodes; + nodes.add_nodes(); + assert(!shared.expired()); + shared.add_nodes(); + assert(nodes.nodes() == 2 && nodes.stop_reason() == Termination::NodeLimit); + shared.cancellation()->cancel(); + assert(nodes.cancelled()); + assert(nodes.stop_reason() == Termination::Cancelled); + + options.node_limit.reset(); + options.time_limit_seconds = 3600.0; + options.cancellation = std::make_shared(); + SolveBudget clock(options); + auto same_clock = clock; + const auto elapsed = clock.elapsed_seconds(); + const auto remaining = clock.remaining_seconds(); + assert(elapsed >= 0.0 && remaining >= 0.0 && remaining <= 3600.0); + assert(same_clock.elapsed_seconds() >= elapsed); + assert(same_clock.remaining_seconds() <= remaining); + options.time_limit_seconds = 0.0; // options are copied, not retained by reference + assert(!clock.time_limit_reached()); + options.cancellation->cancel(); + assert(clock.stop_reason() == Termination::Cancelled); + SolveBudget pre_cancelled(options); + assert(pre_cancelled.stop_reason() == Termination::Cancelled); + + SolveOptions parallel_options; + SolveBudget parallel(parallel_options); + std::vector workers; + for (unsigned i = 0; i < 4; ++i) + workers.emplace_back([parallel]() mutable { + for (unsigned j = 0; j < 1000; ++j) parallel.add_nodes(); + parallel.cancellation()->cancel(); + }); + for (auto& worker : workers) worker.join(); + assert(parallel.nodes() == 4000 && parallel.cancelled()); +} + +void result_tests() { + const double nan = std::numeric_limits::quiet_NaN(); + SolveResult result; + assert(!result.has_solution()); + assert(result.guarantee == Guarantee::Numerical); + result.model_id = 42; + result.revision = 7; + result.objective = 3.0; + result.values = {1.0, nan, 2.0}; + result.active_variables = {true, false, true}; + assert(!result.has_solution()); + result.solution_validated = true; + result.termination = Termination::TimeLimit; + assert(result.has_solution()); + assert(result.value({42, 0}) == 1.0 && result.value({42, 2}) == 2.0); + rejects([&] { result.value({43, 0}); }); + rejects([&] { result.value({42, 1}); }); + rejects([&] { result.value({42, 3}); }); + rejects([&] { result.value({42, std::numeric_limits::max()}); }); + + const auto historical = result; + result.revision = 8; + result.values[0] = 9.0; + result.active_variables[0] = false; + assert(historical.revision == 7 && historical.value({42, 0}) == 1.0); + result.active_variables[1] = true; + assert(!result.has_solution()); + result = historical; + result.values.pop_back(); + assert(!result.has_solution()); + result = historical; + result.objective = nan; + assert(!result.has_solution()); + result = historical; + result.solution_validated = false; + rejects([&] { result.value({42, 0}); }); + + SolveResult empty; + empty.model_id = 9; + empty.objective = 0.0; + empty.solution_validated = true; + assert(empty.has_solution()); // a zero-variable feasible solution is present + rejects([&] { empty.value({9, 0}); }); + assert(std::string(to_string(Termination::Unsupported)) == "unsupported"); + assert(std::string(to_string(Termination::MemoryLimit)) == "memory_limit"); + assert(std::string(to_string(Termination::InfeasibleOrUnbounded)) == + "infeasible_or_unbounded"); +} + +void gap_tests() { + SolveResult result; + result.objective = -10.0; + result.best_bound = -12.0; + result.native_backend_gap = 0.125; + result.update_gaps(ObjectiveSense::Minimize); + assert(result.absolute_gap == 2.0); + assert(std::fabs(*result.relative_gap - 1.0/6.0) < 1e-15); + assert(result.native_backend_gap == 0.125); + result.objective = -12.0; + result.best_bound = -10.0; + result.update_gaps(ObjectiveSense::Maximize); + assert(result.absolute_gap == 2.0); + assert(std::fabs(*result.relative_gap - 1.0/6.0) < 1e-15); + result.objective = 1.0; + result.best_bound = -1.0; + result.update_gaps(ObjectiveSense::Minimize); + assert(result.absolute_gap == 2.0 && result.relative_gap == 2.0); + + result.objective = 0.0; + result.best_bound = -1.0; + result.update_gaps(ObjectiveSense::Minimize); + assert(result.absolute_gap == 1.0 && result.relative_gap == 1.0); + result.best_bound = 1.0; + result.update_gaps(ObjectiveSense::Maximize); + assert(result.absolute_gap == 1.0 && result.relative_gap == 1.0); + result.best_bound = -0.0; + result.update_gaps(ObjectiveSense::Minimize); + assert(result.absolute_gap == 0.0 && result.relative_gap == 0.0); + result.objective = 0.25; + result.best_bound = -0.25; + result.update_gaps(ObjectiveSense::Minimize); + assert(result.absolute_gap == 0.5 && result.relative_gap == 0.5); + result.objective.reset(); + result.update_gaps(ObjectiveSense::Minimize); + assert(!result.absolute_gap && !result.relative_gap); + result.objective = 2.0; + result.best_bound.reset(); + result.update_gaps(ObjectiveSense::Minimize); + assert(!result.absolute_gap && !result.relative_gap); + + result.best_bound = 3.0; + rejects([&] { result.update_gaps(ObjectiveSense::Minimize); }); + assert(!result.absolute_gap && !result.relative_gap); + result.best_bound = std::nextafter(2.0, 3.0); + rejects([&] { result.update_gaps(ObjectiveSense::Minimize); }); + result.best_bound = 1.0; + rejects([&] { result.update_gaps(ObjectiveSense::Maximize); }); + rejects([&] { result.update_gaps(static_cast(999)); }); + + const auto inf = std::numeric_limits::infinity(); + result.best_bound = -inf; + result.update_gaps(ObjectiveSense::Minimize); + assert(!result.absolute_gap && !result.relative_gap); + rejects([&] { result.update_gaps(ObjectiveSense::Maximize); }); + result.best_bound = inf; + result.update_gaps(ObjectiveSense::Maximize); + assert(!result.absolute_gap && !result.relative_gap); + rejects([&] { result.update_gaps(ObjectiveSense::Minimize); }); + result.best_bound = std::numeric_limits::quiet_NaN(); + rejects([&] { result.update_gaps(ObjectiveSense::Minimize); }); + result.best_bound = 0.0; + result.objective = inf; + rejects([&] { result.update_gaps(ObjectiveSense::Minimize); }); + + result.objective = std::numeric_limits::max(); + result.best_bound = -std::numeric_limits::max(); + result.update_gaps(ObjectiveSense::Minimize); + assert(std::isinf(*result.absolute_gap)); + assert(result.relative_gap == 2.0); +} +} + +int main() { + option_tests(); + budget_tests(); + result_tests(); + gap_tests(); +} diff --git a/test/optimize/scenarios.cpp b/test/optimize/scenarios.cpp new file mode 100644 index 0000000000..2d8f7c0f10 --- /dev/null +++ b/test/optimize/scenarios.cpp @@ -0,0 +1,201 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace O=Gecode::Optimize; +constexpr double inf=std::numeric_limits::infinity(); +static std::size_t cases=0,points=0; +templatestatic void invalid(F fn){bool caught=false;try{fn();}catch(const O::ModelError&){caught=true;}assert(caught);} +struct Oracle {std::optional objective;std::vector,bool>> membership;}; +// This oracle reads raw base+patch records. It does not materialize the batch or +// call its validator, sparse normalizer, mappings, or solver implementation. +static Oracle enumerate(const O::ModelSnapshot& base,const O::ScenarioDefinition& def) { + Oracle out;std::vector values(base.variables.size(),std::numeric_limits::quiet_NaN()); + const auto visit=[&] { + ++points; + const auto allowed=[&] { + for(const auto& v:base.variables)if(v.active){ + double lo=v.lower,hi=v.upper; + for(const auto& p:def.variable_bounds)if(p.variable.id==v.variable.id){if(p.lower)lo=*p.lower;if(p.upper)hi=*p.upper;} + if(values[v.variable.id]hi)return false; + } + for(const auto& row:base.rows)if(row.active){ + double lo=row.lower,hi=row.upper; + for(const auto& p:def.row_bounds)if(p.row.id==row.constraint.id){if(p.lower)lo=*p.lower;if(p.upper)hi=*p.upper;} + long long sum=0;for(const auto& t:row.terms)sum+=static_cast(t.coefficient)*static_cast(values[t.variable.id]); + if(sumhi)return false; + } + return true; + }; + const bool accepted=allowed();out.membership.emplace_back(values,accepted);if(!accepted)return; + long long objective=static_cast(def.objective_offset.value_or(base.objective.offset)); + for(std::size_t i=0;i(coefficient)*static_cast(values[i]); + } + if(!out.objective || (base.objective.sense==O::ObjectiveSense::Minimize?objective<*out.objective:objective>*out.objective))out.objective=static_cast(objective); + }; + std::function next=[&](std::size_t slot) { + if(slot==values.size()){visit();return;} + const auto& v=base.variables[slot];if(!v.active){next(slot+1);return;} + double lo=v.lower,hi=v.upper;for(const auto& p:def.variable_bounds)if(p.variable.id==slot){if(p.lower)lo=std::min(lo,*p.lower);if(p.upper)hi=std::max(hi,*p.upper);} + assert(std::isfinite(lo)&&std::isfinite(hi)&&hi-lo<10); + for(int x=static_cast(std::ceil(lo));x<=static_cast(std::floor(hi));++x){values[slot]=x;next(slot+1);} + }; + next(0);return out; +} +static void assert_outcome(const O::ScenarioBatchResult& batch,std::size_t i,const Oracle& oracle) { + ++cases;const auto& outcome=batch.outcomes[i];assert(outcome.result&&outcome.state==O::ScenarioRunState::Attempted); + const auto& result=*outcome.result; + const auto model=batch.batch->materialize(outcome.scenario); + for(const auto& point:oracle.membership)assert(O::validate(model,point.first).valid==point.second); + if(!oracle.objective){assert(result.termination==O::Termination::Infeasible&&!result.has_solution());return;} + if(result.termination!=O::Termination::Optimal)std::cerr<validation.valid); + assert(outcome.check->identity_valid&&outcome.check->objective_matches); + assert(result.model_id==batch.batch->id()&&result.revision==i+1); + assert(O::validate(model,result.values).valid); +} +static void discrete(O::Backend backend) { + for(bool maximize:{false,true})for(int a=-2;a<=2;++a)for(int b=-1;b<=1;++b){ + O::Model model;const auto removed=model.add_integer(-1,1);model.remove(removed); + const auto x=model.add_integer(-2,2),y=model.add_binary(); + const auto deleted=model.add_row({},-1,1);model.remove(deleted); + const auto row=model.add_row({{x,static_cast(a)},{y,static_cast(b)}},-1,2); + model.set_objective({{x,2},{y,-1}},maximize?O::ObjectiveSense::Maximize:O::ObjectiveSense::Minimize,7); + const auto base=model.snapshot(); + std::vector definitions(6); + definitions[1].objective_coefficients={{x,0},{y,3}};definitions[1].objective_offset=-9; + definitions[2].variable_bounds={{x,-1,1},{y,1,1}}; + definitions[3].row_bounds={{row,2,2}}; + definitions[5].variable_bounds={{x,-3,3}};definitions[5].row_bounds={{row,-4,4}}; + definitions[5].objective_coefficients={{x,-3}};definitions[5].objective_offset=8; + O::ScenarioBatchOptions options;options.solve.backend=backend; + if(backend==O::Backend::Native)options.solve.guarantee=O::Guarantee::Exact; + const auto result=O::solve_scenarios(model,definitions,options); + if(!result.all_resolved())std::cerr<id()!=model.id()); + assert(model.revision()==base.revision&&model.snapshot().objective.offset==7); + for(std::size_t i=0;ihas_solution()) { + assert(result.value(result.batch->scenario(i),x)==result.outcomes[i].result->values[x.id]); + if(backend==O::Backend::Native)assert(result.outcomes[i].check->exact_witness_validated); + } + } + invalid([&]{result.batch->map(removed);});invalid([&]{result.batch->map(deleted);}); + invalid([&]{result.value({model.id(),0},x);});invalid([&]{result.batch->scenario(6);}); + invalid([&]{result.value(result.batch->scenario(0),O::Variable{model.id()+1,x.id});}); + } +} +static void admission(){ + O::Model model;auto x=model.add_integer(0,3);auto row=model.add_row({{x,1}},0,3);model.minimize({{x,1}},1); + std::vector definitions(2);O::ScenarioBatchOptions options; + for(int mode=0;mode<15;++mode){ + auto defs=definitions; + if(mode==0)defs[1].objective_coefficients={{x,1},{x,2}}; + if(mode==1)defs[1].objective_coefficients={{{999,x.id},1}}; + if(mode==2)defs[1].objective_coefficients={{x,inf}}; + if(mode==3)defs[1].objective_offset=inf; + if(mode==4)defs[1].variable_bounds={{x,2,1}}; + if(mode==5)defs[1].variable_bounds={{x,{},{} }}; + if(mode==6)defs[1].variable_bounds={{x,0,2},{x,0,1}}; + if(mode==7)defs[1].variable_bounds={{x,inf,{}}}; + if(mode==8)defs[1].row_bounds={{row,0,1},{row,0,2}}; + if(mode==9)defs[1].row_bounds={{row,{},{} }}; + if(mode==10)defs[1].row_bounds={{row,4,2}}; + if(mode==11)defs[1].row_bounds={{{999,row.id},0,1}}; + if(mode==12)defs[1].row_bounds={{row,{},-inf}}; + if(mode==13)defs[1].variable_bounds={{x,std::numeric_limits::quiet_NaN(),{}}}; + if(mode==14)defs[1].objective_coefficients={{{model.id(),99},1}}; + const auto out=O::solve_scenarios(model,defs,options); + assert(!out.batch&&out.outcomes.empty()&&out.attempted==0&&out.stop_reason==O::Termination::InvalidModel&&out.offending_scenario==1); + } + for(int mode=0;mode<4;++mode){auto capped=options; + if(mode==0)capped.max_scenarios=1; + if(mode==1){capped.max_patch_entries=0;definitions[1].objective_offset=2;definitions[1].objective_coefficients={{x,1}};} + if(mode==2)capped.max_saved_value_slots=1; + if(mode==3)capped.max_work=0; + const auto out=O::solve_scenarios(model,definitions,capped); + assert(!out.batch&&!out.attempted&&out.stop_reason==(mode==3?O::Termination::IterationLimit:O::Termination::MemoryLimit)); + definitions=std::vector(2); + } + auto empty=O::solve_scenarios(model,{});assert(empty.all_resolved()&&empty.attempted==0&&empty.outcomes.empty()); + options.solve.node_limit=1;auto nodes=O::solve_scenarios(model,definitions,options);assert(nodes.stop_reason==O::Termination::Unsupported&&!nodes.attempted); + options.solve.node_limit=0;nodes=O::solve_scenarios(model,definitions,options);assert(nodes.stop_reason==O::Termination::NodeLimit&&!nodes.attempted); + options={};options.solve.time_limit_seconds=0;assert(O::solve_scenarios(model,definitions,options).stop_reason==O::Termination::TimeLimit); + options={};options.solve.cancellation=std::make_shared();options.solve.cancellation->cancel(); + assert(O::solve_scenarios(model,definitions,options).stop_reason==O::Termination::Cancelled); + options={};options.solve.primal_start={{x,0}};assert(O::solve_scenarios(model,definitions,options).stop_reason==O::Termination::Unsupported); + options={};options.reuse=static_cast(55);assert(O::solve_scenarios(model,definitions,options).stop_reason==O::Termination::InvalidModel); + auto malformed=model.snapshot();malformed.rows[0].terms[0].variable.model_id=999; + assert(O::solve_scenarios(malformed,definitions).stop_reason==O::Termination::InvalidModel); + O::Model binary;auto b=binary.add_binary();O::ScenarioDefinition bad;bad.variable_bounds={{b,-1,2}}; + assert(O::solve_scenarios(binary,{bad}).stop_reason==O::Termination::InvalidModel); + O::Model semi;semi.add_variable(O::VariableType::SemiInteger,2,4); + assert(O::solve_scenarios(semi,{{}}).stop_reason==O::Termination::Unsupported); + auto g=binary.add_global(O::AllDifferentData{{b}}); + assert(O::solve_scenarios(binary,{{}}).stop_reason==O::Termination::Unsupported); + binary.remove(g);assert(O::solve_scenarios(binary,{{}}).stop_reason==O::Termination::Unsupported); +} +static void guarantees(){ + O::Model model;auto x=model.add_integer(0,2);model.minimize({{x,1}}); + for(auto backend:{O::Backend::Auto,O::Backend::Highs,O::Backend::Native}) + for(auto guarantee:{O::Guarantee::Numerical,O::Guarantee::Exact,O::Guarantee::Certified}) + for(std::size_t count:{std::size_t(0),std::size_t(1)}){ + O::ScenarioBatchOptions opts;opts.solve.backend=backend;opts.solve.guarantee=guarantee; + const auto result=O::solve_scenarios(model,std::vector(count),opts); + const bool rejected=guarantee==O::Guarantee::Certified || (guarantee==O::Guarantee::Exact&&backend!=O::Backend::Native); + if(rejected){assert(result.stop_reason==O::Termination::Unsupported&&!result.batch&&!result.attempted);} + else if(count==0){assert(result.all_resolved()&&result.attempted==0);} + else if(O::capabilities(backend==O::Backend::Auto?O::Backend::Highs:backend).available){ + assert(result.all_resolved()&&result.outcomes[0].result->guarantee==guarantee); + }else{assert(result.stop_reason==O::Termination::Unsupported&&result.attempted==1);} + } +} +static void numerical(){ + O::Model model;auto x=model.add_continuous(0,10),y=model.add_continuous(0,10); + auto row=model.add_row({{x,1},{y,1}},3,inf);model.minimize({{x,2},{y,3}},7); + std::vector defs(5);defs[1].objective_coefficients={{x,4}}; + defs[2].variable_bounds={{x,{},1}};defs[3].row_bounds={{row,4,{}}}; + O::ScenarioBatchOptions options;options.solve.backend=O::Backend::Highs; + const auto warm=O::solve_scenarios(model,defs,options);assert(warm.all_resolved()); + const double expected[]={13,16,15,15,13}; + for(std::size_t i=0;i<5;++i)assert(std::abs(*warm.outcomes[i].result->objective-expected[i])<1e-7); + assert(warm.reuse_statistics.solve_calls==5&&warm.reuse_statistics.model_loads==1&&warm.reuse_statistics.incremental_updates==4); + options.reuse=O::ScenarioReuse::Cold;const auto cold=O::solve_scenarios(model,defs,options); + assert(cold.all_resolved()&&cold.reuse_statistics.solve_calls==0); + for(std::size_t i=0;i<5;++i)assert(cold.outcomes[i].result->objective==warm.outcomes[i].result->objective); + model.set_bounds(x,9,10);defs.clear(); + assert(warm.batch->base().variables[x.id].lower==0&&warm.value(warm.batch->scenario(0),x)==3); + auto changed=warm.batch->materialize(warm.batch->scenario(0));changed.variables[x.id].lower=8; + assert(warm.batch->materialize(warm.batch->scenario(0)).variables[x.id].lower==0); + O::Model unbounded;auto z=unbounded.add_continuous();unbounded.minimize({{z,-1}}); + O::ScenarioDefinition finite;finite.variable_bounds={{z,{},2}}; + auto result=O::solve_scenarios(unbounded,{finite,{}});assert(result.all_resolved()); + assert(result.outcomes[0].result->objective==-2&&result.outcomes[1].result->termination==O::Termination::Unbounded); + O::Model constant;auto c=constant.add_row({},0,inf);O::ScenarioDefinition impossible;impossible.row_bounds={{c,1,{}}}; + result=O::solve_scenarios(constant,{{},impossible,{}});assert(result.all_resolved()); + assert(result.outcomes[0].result->objective==0&&result.outcomes[1].result->termination==O::Termination::Infeasible); + O::Model recourse;auto decision=recourse.add_integer(0,2),completion=recourse.add_continuous(0,10); + recourse.add_row({{decision,1},{completion,1}},2,inf);recourse.minimize({{decision,1},{completion,2}}); + O::ScenarioDefinition fixed;fixed.variable_bounds={{decision,0,0}}; + result=O::solve_scenarios(recourse,{{},fixed});assert(result.all_resolved()); + assert(result.outcomes[0].result->objective==2&&result.outcomes[1].result->objective==4); + O::Model cancellation;auto a=cancellation.add_continuous(1,1),b=cancellation.add_continuous(1e6,1e6); + cancellation.minimize({{a,1},{b,1e10}},-1e16); + result=O::solve_scenarios(cancellation,{{}});assert(result.all_resolved()&&result.outcomes[0].result->objective==1); +} +int main(){ + admission();guarantees(); + if(O::capabilities(O::Backend::Highs).available){discrete(O::Backend::Highs);numerical();} + else {O::Model m;m.add_integer(0,1);auto out=O::solve_scenarios(m,{{}});assert(out.stop_reason==O::Termination::Unsupported&&!out.all_resolved());} + if(O::native_capabilities().available)discrete(O::Backend::Native); + std::cout<<"Scenario admission, histories and cold/reuse: "< +#include +#include +#include +#include +#include +#define OK(call) do { int32_t code_=(call); if(code_){fprintf(stderr,"%s: %d %s\n",#call,(int)code_,gecode_opt_v1_last_error());assert(code_==0);} } while(0) +static gecode_opt_scenario_definition_v1 definition(void){ + gecode_opt_scenario_definition_v1 d;memset(&d,0,sizeof(d));d.struct_size=sizeof(d);return d; +} +static gecode_opt_scenario_bounds_v1 bound(gecode_opt_id id,int32_t lo,double lower,int32_t up,double upper){ + gecode_opt_scenario_bounds_v1 d;memset(&d,0,sizeof(d));d.struct_size=sizeof(d);d.entity=id; + d.has_lower=lo;d.lower=lower;d.has_upper=up;d.upper=upper;return d; +} +static gecode_opt_scenario_info_v1 info(gecode_opt_handle h){ + gecode_opt_scenario_info_v1 i;memset(&i,0xa5,sizeof(i));OK(gecode_opt_v1_scenario_batch_info(h,&i,sizeof(i)));assert(!i.reserved);return i; +} +static gecode_opt_handle solve(gecode_opt_handle m,const gecode_opt_scenario_definition_v1* d,uint64_t n,const gecode_opt_scenario_options_v1* o){ + gecode_opt_handle r=0;OK(gecode_opt_v1_solve_scenarios(m,d,n,sizeof(*d),o,&r));assert(r);return r; +} +static void run(int backend){ + gecode_opt_handle model=0,result=0,child=0;gecode_opt_id x,y,dead,row,gone,mapped,mapped_row; + gecode_opt_term terms[2];gecode_opt_scenario_options_v1 options; + gecode_opt_scenario_definition_v1 defs[3];gecode_opt_scenario_bounds_v1 bounds[2]; + gecode_opt_scenario_id id,id1,id2,bad;gecode_opt_scenario_outcome_v1 outcome;gecode_opt_scenario_check_v1 checks; + gecode_opt_scenario_info_v1 summary;gecode_opt_result_info_v1 ordinary; + int32_t available,lp,mip,present;double value;uint64_t owner,revision,needed;char name[]="copied name"; + OK(gecode_opt_v1_capabilities(backend,&available,&lp,&mip)); + OK(gecode_opt_v1_model_create(&model)); + OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_INTEGER,0,8,"x",&x)); + OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_INTEGER,0,8,"y",&y)); + OK(gecode_opt_v1_model_add_variable(model,GECODE_OPT_INTEGER,0,8,"dead",&dead)); + OK(gecode_opt_v1_model_remove_variable(model,dead)); + OK(gecode_opt_v1_model_add_row(model,NULL,0,-INFINITY,INFINITY,"gone",&gone)); + OK(gecode_opt_v1_model_remove_row(model,gone)); + terms[0].variable=x;terms[0].coefficient=1;terms[1].variable=y;terms[1].coefficient=1; + OK(gecode_opt_v1_model_add_row(model,terms,2,4,INFINITY,"demand",&row)); + terms[0].coefficient=2;terms[1].coefficient=3; + OK(gecode_opt_v1_model_set_objective(model,terms,2,GECODE_OPT_MINIMIZE,7)); + OK(gecode_opt_v1_model_identity(model,&owner,&revision)); + OK(gecode_opt_v1_scenario_options_default(&options,sizeof(options)));assert(options.struct_size==sizeof(options)); + assert(!options.reserved&&!options.reserved_flags&&!options.solve.reserved);options.solve.backend=backend; + if(backend==GECODE_OPT_NATIVE)options.solve.guarantee=GECODE_OPT_EXACT; + defs[0]=definition();defs[0].name=name; + defs[1]=definition();bounds[0]=bound(x,0,NAN,1,1);defs[1].variable_bounds=bounds;defs[1].variable_count=1; + defs[2]=definition();bounds[1]=bound(y,0,0,1,1);defs[2].variable_bounds=bounds;defs[2].variable_count=2; + result=solve(model,defs,3,&options);name[0]='X';bounds[0].upper=7; + summary=info(result);assert(summary.model_id==owner&&summary.revision==revision&&summary.has_batch); + assert(summary.batch_id!=owner);assert(summary.scenario_count==3&&summary.outcome_count==3); + OK(gecode_opt_v1_scenario_batch_id(result,0,&id));OK(gecode_opt_v1_scenario_batch_id(result,1,&id1));OK(gecode_opt_v1_scenario_batch_id(result,2,&id2)); + OK(gecode_opt_v1_scenario_batch_map(result,x,&mapped));OK(gecode_opt_v1_scenario_batch_map(result,row,&mapped_row)); + assert(mapped.model_id==id.batch_id&&mapped.slot==x.slot&&mapped.kind==GECODE_OPT_VARIABLE_ID); + assert(mapped_row.model_id==id.batch_id&&mapped_row.slot==row.slot&&mapped_row.kind==GECODE_OPT_ROW_ID); + assert(gecode_opt_v1_scenario_batch_map(result,dead,&mapped_row)==GECODE_OPT_MODEL_ERROR); + assert(gecode_opt_v1_scenario_batch_map(result,gone,&mapped_row)==GECODE_OPT_MODEL_ERROR); + {char buffer[32];OK(gecode_opt_v1_scenario_batch_text(result,id,GECODE_OPT_SCENARIO_NAME,buffer,sizeof(buffer),&needed));assert(!strcmp(buffer,"copied name"));} + {gecode_opt_scenario_bounds_v1 b;OK(gecode_opt_v1_scenario_batch_bounds(result,id1,GECODE_OPT_VARIABLE_ID,&b,sizeof(b),1,&needed)); + assert(needed==1&&b.upper==1&&!b.has_lower&&b.lower==0&&b.reserved==0);} + {gecode_opt_scenario_bounds_v1 b;memset(&b,0xa5,sizeof(b)); + assert(gecode_opt_v1_scenario_batch_bounds(result,id2,GECODE_OPT_VARIABLE_ID,&b,sizeof(b),1,&needed)==GECODE_OPT_BUFFER_TOO_SMALL); + assert(needed==2&&b.struct_size==UINT64_C(0xa5a5a5a5a5a5a5a5)); + assert(gecode_opt_v1_scenario_batch_bounds(result,id,GECODE_OPT_VARIABLE_ID,NULL,sizeof(b)-1,0,&needed)==GECODE_OPT_INVALID_ARGUMENT);} + bad=id;bad.batch_id++; + assert(gecode_opt_v1_scenario_batch_outcome(result,bad,&outcome,sizeof(outcome))==GECODE_OPT_MODEL_ERROR); + bad=id;bad.index=3;assert(gecode_opt_v1_scenario_batch_check(result,bad,&checks,sizeof(checks))==GECODE_OPT_MODEL_ERROR); + assert(gecode_opt_v1_scenario_batch_info(model,&summary,sizeof(summary))==GECODE_OPT_INVALID_HANDLE); + assert(gecode_opt_v1_result_info(result,&ordinary,sizeof(ordinary))==GECODE_OPT_INVALID_HANDLE); + assert(gecode_opt_v1_scenario_batch_outcome(result,id,&outcome,sizeof(outcome)-1)==GECODE_OPT_INVALID_ARGUMENT); + OK(gecode_opt_v1_scenario_batch_outcome(result,id,&outcome,sizeof(outcome))); + assert(outcome.has_result&&outcome.state==GECODE_OPT_SCENARIO_ATTEMPTED&&!outcome.reserved&&!outcome.reserved_flags); + OK(gecode_opt_v1_scenario_batch_check(result,id,&checks,sizeof(checks)));assert(!checks.reserved&&!checks.reserved_flags); + if(available){ + summary=info(result);assert(summary.all_resolved&&summary.resolved==3&&summary.completion==GECODE_OPT_SCENARIO_COMPLETE); + assert(!summary.has_stop_reason&&!summary.has_offending_scenario); + assert(outcome.result.termination==GECODE_OPT_OPTIMAL&&checks.has_check&&checks.candidate_examined&&checks.validation.valid); + OK(gecode_opt_v1_scenario_batch_value(result,id,x,&value));assert(value==4); + OK(gecode_opt_v1_scenario_batch_value(result,id1,x,&value));assert(value==1); + OK(gecode_opt_v1_scenario_batch_check(result,id2,&checks,sizeof(checks))); + assert(checks.has_check&&!checks.candidate_examined&&!checks.validation.valid&&!checks.validation.objective.present); + assert(gecode_opt_v1_scenario_batch_value(result,id2,x,&value)==GECODE_OPT_NO_SOLUTION); + }else{ + assert(outcome.result.termination==GECODE_OPT_UNSUPPORTED);assert(!checks.candidate_examined&&!checks.validation.objective.present); + OK(gecode_opt_v1_scenario_batch_outcome(result,id1,&outcome,sizeof(outcome))); + assert(!outcome.has_result&&!outcome.has_check&&outcome.state==GECODE_OPT_SCENARIO_NOT_STARTED); + memset(&checks,0xa5,sizeof(checks));OK(gecode_opt_v1_scenario_batch_check(result,id1,&checks,sizeof(checks))); + assert(!checks.has_check&&!checks.candidate_examined&&!checks.validation.valid&&!checks.validation.objective.present); + child=99;assert(gecode_opt_v1_scenario_batch_copy_result(result,id1,&child)==GECODE_OPT_NO_SOLUTION&&child==0); + } + OK(gecode_opt_v1_scenario_batch_copy_result(result,id,&child));OK(gecode_opt_v1_model_destroy(model)); + OK(gecode_opt_v1_scenario_batch_destroy(result));OK(gecode_opt_v1_result_info(child,&ordinary,sizeof(ordinary))); + assert(ordinary.model_id==id.batch_id&&ordinary.revision==1); + if(available){OK(gecode_opt_v1_result_value(child,mapped,&value));assert(value==4); + assert(gecode_opt_v1_result_value(child,x,&value)==GECODE_OPT_MODEL_ERROR); + OK(gecode_opt_v1_result_number(child,GECODE_OPT_OBJECTIVE,&present,&value));assert(present&&value==15);} + OK(gecode_opt_v1_result_destroy(child));assert(gecode_opt_v1_scenario_batch_destroy(result)==GECODE_OPT_INVALID_HANDLE); +} +static void malformed_and_limits(void){ + gecode_opt_handle m=0,r=0,cancel=0;gecode_opt_id x;gecode_opt_scenario_options_v1 o,bad; + gecode_opt_scenario_definition_v1 d[2];gecode_opt_scenario_bounds_v1 bounds;gecode_opt_term t[2]; + gecode_opt_scenario_info_v1 summary;gecode_opt_scenario_definition_info_v1 definition_info; + uint64_t owner,revision,needed;int i;gecode_opt_scenario_id id; + OK(gecode_opt_v1_model_create(&m));OK(gecode_opt_v1_model_add_variable(m,GECODE_OPT_INTEGER,0,2,"x",&x)); + OK(gecode_opt_v1_model_identity(m,&owner,&revision));OK(gecode_opt_v1_scenario_options_default(&o,sizeof(o))); + d[0]=definition();d[1]=definition(); + for(i=0;i<7;i++){ + bad=o;switch(i){case 0:bad.struct_size--;break;case 1:bad.reserved=1;break;case 2:bad.reserved_flags=1;break; + case 3:bad.solve.struct_size--;break;case 4:bad.solve.reserved=1;break;case 5:bad.reuse=9;break;case 6:bad.solve.has_node_limit=2;break;} + r=99;assert(gecode_opt_v1_solve_scenarios(m,d,2,sizeof(d[0]),&bad,&r)==GECODE_OPT_INVALID_ARGUMENT&&r==0); + } + r=99;assert(gecode_opt_v1_solve_scenarios(m,d,1,sizeof(d[0])-1,&o,&r)==GECODE_OPT_INVALID_ARGUMENT&&r==0); + r=99;assert(gecode_opt_v1_solve_scenarios(m,NULL,1,sizeof(d[0]),&o,&r)==GECODE_OPT_INVALID_ARGUMENT&&r==0); + r=99;assert(gecode_opt_v1_solve_scenarios(m,d,UINT64_MAX,sizeof(d[0]),&o,&r)==GECODE_OPT_INVALID_ARGUMENT&&r==0); + for(i=0;i<5;i++){ + d[0]=definition();bounds=bound(x,1,1,0,0);d[0].variable_bounds=&bounds;d[0].variable_count=1; + switch(i){case 0:d[0].struct_size=0;break;case 1:d[0].reserved=1;break;case 2:d[0].objective_offset.present=2;break; + case 3:bounds.struct_size=0;break;case 4:bounds.has_lower=2;break;} + assert(gecode_opt_v1_solve_scenarios(m,d,1,sizeof(d[0]),&o,&r)==GECODE_OPT_INVALID_ARGUMENT&&r==0); + } + d[0]=definition();d[1]=definition();t[0].variable=x;t[0].coefficient=1;t[1]=t[0]; + d[1].objective_coefficients=t;d[1].objective_count=2;r=solve(m,d,2,&o);summary=info(r); + assert(summary.completion==GECODE_OPT_SCENARIO_REJECTED&&summary.stop_reason==GECODE_OPT_INVALID_MODEL); + assert(summary.has_offending_scenario&&summary.offending_scenario==1&&!summary.attempted&&!summary.has_batch); + assert(summary.model_id==owner&&summary.revision==revision);OK(gecode_opt_v1_scenario_batch_message(r,NULL,0,&needed));assert(needed>1); + OK(gecode_opt_v1_scenario_batch_destroy(r));d[1]=definition(); + for(i=0;i<5;i++){ + int expected;bad=o;switch(i){case 0:bad.solve.time_limit_seconds=0;expected=GECODE_OPT_TIME_LIMIT;break; + case 1:bad.solve.has_node_limit=1;bad.solve.node_limit=0;expected=GECODE_OPT_NODE_LIMIT;break; + case 2:bad.solve.has_node_limit=1;bad.solve.node_limit=1;expected=GECODE_OPT_UNSUPPORTED;break; + case 3:bad.max_scenarios=1;expected=GECODE_OPT_MEMORY_LIMIT;break; + default:bad.max_work=0;expected=GECODE_OPT_ITERATION_LIMIT;break;} + r=solve(m,d,2,&bad);summary=info(r);assert(summary.has_stop_reason&&summary.stop_reason==expected&&!summary.attempted); + OK(gecode_opt_v1_scenario_batch_destroy(r)); + } + OK(gecode_opt_v1_cancellation_create(&cancel));OK(gecode_opt_v1_cancellation_cancel(cancel));bad=o;bad.solve.cancellation=cancel; + r=solve(m,d,2,&bad);summary=info(r);assert(summary.stop_reason==GECODE_OPT_CANCELLED);OK(gecode_opt_v1_cancellation_destroy(cancel));OK(gecode_opt_v1_scenario_batch_destroy(r)); + r=solve(m,NULL,0,&o);summary=info(r);assert(summary.all_resolved&&summary.has_batch&&!summary.scenario_count); + assert(gecode_opt_v1_scenario_batch_id(r,0,&id)==GECODE_OPT_MODEL_ERROR);OK(gecode_opt_v1_scenario_batch_destroy(r)); + d[0].objective_coefficients=t;d[0].objective_count=1;d[0].objective_offset.present=1;d[0].objective_offset.value=0; + r=solve(m,d,1,&o);OK(gecode_opt_v1_scenario_batch_id(r,0,&id)); + memset(&definition_info,0xa5,sizeof(definition_info));OK(gecode_opt_v1_scenario_batch_definition(r,id,&definition_info,sizeof(definition_info))); + assert(definition_info.objective_count==1&&definition_info.objective_offset.present&&definition_info.objective_offset.value==0&&!definition_info.reserved); + OK(gecode_opt_v1_scenario_batch_destroy(r));OK(gecode_opt_v1_model_destroy(m)); +} +int main(void){run(GECODE_OPT_HIGHS);run(GECODE_OPT_NATIVE);malformed_and_limits();puts("C99 scenario ownership/boundary tests passed");return 0;} diff --git a/test/optimize/scenarios_coordinator.cpp b/test/optimize/scenarios_coordinator.cpp new file mode 100644 index 0000000000..6e566b8720 --- /dev/null +++ b/test/optimize/scenarios_coordinator.cpp @@ -0,0 +1,149 @@ +// Compile scenarios.cpp separately with GECODE_OPTIMIZE_TEST_SCENARIOS=1. +// The oracle below enumerates the original finite box; no backend calls occur. +#include +#include +#include +#include +#include +#include +#include + +namespace O=Gecode::Optimize; +namespace { +enum class Fault {None,Owner,Revision,Mask,Objective,NaNObjective,NaNBound,Bound, + FalseInfeasible,FalseUnbounded,NoWitness,Guarantee,Incomplete,InfiniteBound, + Infeasible,Exception,ExactFraction,UnvalidatedObjective,InvalidUnvalidatedPoint,VendorGap, + UnknownStatus,LooseExactBound,MissingExactBound}; +Fault fault=Fault::None; +std::size_t calls=0,fault_call=1; +std::string cancel_point; +std::size_t cancel_index=1; +std::shared_ptr token; +double last_allowance=std::numeric_limits::infinity(); +std::optional observed_nodes; +O::SolveResult oracle(const O::ModelSnapshot& model) { + O::SolveResult out;out.model_id=model.model_id;out.revision=model.revision; + for(const auto& v:model.variables)out.active_variables.push_back(v.active); + std::vector values(model.variables.size(),std::numeric_limits::quiet_NaN()); + std::function visit=[&](std::size_t slot) { + if(slot(std::ceil(v.lower));x<=static_cast(std::floor(v.upper));++x){values[slot]=x;visit(slot+1);}return; + } + for(const auto& r:model.rows)if(r.active){long long sum=0; + for(const auto& t:r.terms)sum+=static_cast(t.coefficient)*static_cast(values[t.variable.id]); + if(sumr.upper)return;} + long long objective=static_cast(model.objective.offset); + for(const auto& t:model.objective.terms)objective+=static_cast(t.coefficient)*static_cast(values[t.variable.id]); + if(!out.objective || (model.objective.sense==O::ObjectiveSense::Minimize?objective<*out.objective:objective>*out.objective)){ + out.objective=static_cast(objective);out.values=values;} + }; + visit(0);out.solution_validated=out.objective.has_value();out.best_bound=out.objective; + out.termination=out.objective?O::Termination::Optimal:O::Termination::Infeasible;return out; +} +void reset(){fault=Fault::None;calls=0;cancel_point.clear();token=std::make_shared();last_allowance=std::numeric_limits::infinity();observed_nodes.reset();} +} +namespace Gecode { namespace Optimize { namespace Detail { +SolveResult scenario_test_solve(const ModelSnapshot& model,const SolveOptions& options,SolveSession*) { + assert(options.time_limit_seconds<=last_allowance);last_allowance=options.time_limit_seconds;observed_nodes=options.node_limit; + auto out=oracle(model);out.guarantee=options.guarantee;const auto number=calls++; + if(number!=fault_call)return out; + switch(fault){ + case Fault::None:break; + case Fault::Owner:++out.model_id;break; + case Fault::Revision:++out.revision;break; + case Fault::Mask:out.active_variables[0]=true;break; + case Fault::Objective:*out.objective+=1;break; + case Fault::NaNObjective:out.objective=std::numeric_limits::quiet_NaN();break; + case Fault::NaNBound:out.best_bound=std::numeric_limits::quiet_NaN();break; + case Fault::Bound:*out.best_bound=*out.objective+1;break; + case Fault::FalseInfeasible:out.termination=Termination::Infeasible;out.solution_validated=false;break; + case Fault::FalseUnbounded:out.termination=Termination::Unbounded;break; + case Fault::NoWitness:out.values.clear();out.objective.reset();out.best_bound.reset();out.solution_validated=false;break; + case Fault::Guarantee:out.guarantee=Guarantee::Certified;break; + case Fault::Incomplete:out.termination=Termination::NodeLimit;break; + case Fault::InfiniteBound:out.termination=Termination::NodeLimit;out.best_bound=-std::numeric_limits::infinity();break; + case Fault::Infeasible:out.termination=Termination::Infeasible;out.values.clear();out.objective.reset();out.best_bound.reset();out.solution_validated=false;break; + case Fault::Exception:throw std::bad_alloc(); + case Fault::ExactFraction:out.values[1]+=.0000001;out.objective=*out.objective+.0000001;out.best_bound=out.objective;break; + case Fault::UnvalidatedObjective:out.solution_validated=false;out.termination=Termination::NodeLimit;*out.objective+=1;break; + case Fault::InvalidUnvalidatedPoint:out.solution_validated=false;out.termination=Termination::NodeLimit;out.values[1]=-100;break; + case Fault::VendorGap:out.native_backend_gap=-1;break; + case Fault::UnknownStatus:out.termination=static_cast(99);break; + case Fault::LooseExactBound:*out.best_bound=*out.objective-1;break; + case Fault::MissingExactBound:out.best_bound.reset();break; + } + return out; +} +void scenario_test_checkpoint(const char* point,std::size_t index) { + if(cancel_point==point&&index==cancel_index)token->cancel(); +} +}}} +int main(){ + O::Model model;auto removed=model.add_integer(0,1);model.remove(removed); + auto x=model.add_integer(0,4);model.minimize({{x,1}},3); + std::vector defs(3);defs[1].variable_bounds={{x,2,{}}};defs[2].objective_offset=-7; + O::ScenarioBatchOptions options;options.solve.time_limit_seconds=30; + reset();auto good=O::solve_scenarios(model,defs,options);assert(good.all_resolved()&&calls==3&&last_allowance<30); + assert(good.outcomes[0].result->objective==3&&good.outcomes[1].result->objective==5&&good.outcomes[2].result->objective==-7); + for(auto f:{Fault::Owner,Fault::Revision,Fault::Mask,Fault::Objective,Fault::NaNObjective,Fault::NaNBound, + Fault::Bound,Fault::FalseInfeasible,Fault::FalseUnbounded,Fault::NoWitness,Fault::Guarantee, + Fault::UnvalidatedObjective,Fault::InvalidUnvalidatedPoint,Fault::VendorGap,Fault::UnknownStatus}) { + reset();fault=f;const auto out=O::solve_scenarios(model,defs,options); + assert(out.stop_reason==O::Termination::NumericalFailure&&out.resolved==1&&out.attempted==2&&calls==2); + assert(out.outcomes[0].result->has_solution()&&out.outcomes[0].result->objective==3); + assert(out.outcomes[1].result&&out.outcomes[1].result->termination==O::Termination::NumericalFailure&&!out.outcomes[1].result->has_solution()); + assert(out.outcomes[2].state==O::ScenarioRunState::NotStarted&&!out.outcomes[2].result); + } + for(auto f:{Fault::Incomplete,Fault::InfiniteBound}){ + reset();fault=f;const auto out=O::solve_scenarios(model,defs,options); + assert(out.stop_reason==O::Termination::NodeLimit&&out.resolved==1&&calls==2); + assert(out.outcomes[1].result->has_solution()&&out.outcomes[1].check->validation.valid); + if(f==Fault::InfiniteBound)assert(out.outcomes[1].result->best_bound&&!std::isfinite(*out.outcomes[1].result->best_bound)&&!out.outcomes[1].result->absolute_gap); + } + reset();fault=Fault::Infeasible;auto infeasible=O::solve_scenarios(model,defs,options);assert(infeasible.all_resolved()&&calls==3); + reset();fault=Fault::Exception;auto allocation=O::solve_scenarios(model,defs,options); + assert(allocation.stop_reason==O::Termination::MemoryLimit&&allocation.resolved==1&&allocation.outcomes[1].result->termination==O::Termination::MemoryLimit); + for(const auto* point: {"admission","before_solve","after_solve","after_check","after_cleanup"}) { + reset();cancel_point=point;options.solve.cancellation=token; + const auto out=O::solve_scenarios(model,defs,options);assert(out.stop_reason==O::Termination::Cancelled); + if(cancel_point=="admission")assert(!out.batch&&calls==0); + else { + assert(out.batch&&out.resolved==1&&out.outcomes[0].result->has_solution()); + assert(!out.outcomes[1].result||!out.outcomes[1].result->has_solution()); + assert(calls==(cancel_point=="before_solve"?1:2)); + } + } + reset();cancel_point="batch_cleanup";cancel_index=3;options.solve.cancellation=token; + auto late=O::solve_scenarios(model,defs,options);assert(late.stop_reason==O::Termination::Cancelled&&!late.all_resolved()&&late.resolved==3); + for(const auto& stage:late.outcomes)assert(stage.result->has_solution()); + options={};reset();options.solve.backend=O::Backend::Native;options.solve.guarantee=O::Guarantee::Exact; + auto native=O::solve_scenarios(model,defs,options);assert(native.all_resolved());for(const auto& stage:native.outcomes)assert(stage.check->exact_witness_validated); + reset();fault=Fault::ExactFraction;auto fraction=O::solve_scenarios(model,defs,options);assert(fraction.stop_reason==O::Termination::NumericalFailure&&fraction.resolved==1); + for(auto f:{Fault::LooseExactBound,Fault::MissingExactBound}) { + reset();fault=f;auto bad=O::solve_scenarios(model,defs,options); + assert(bad.stop_reason==O::Termination::NumericalFailure&&bad.resolved==1&&!bad.all_resolved()); + } + reset();options.solve.node_limit=7;auto one=O::solve_scenarios(model,{{}},options);assert(one.all_resolved()&&calls==1&&observed_nodes==7); + reset();options.solve.node_limit=0;auto zero=O::solve_scenarios(model,{{}},options);assert(zero.stop_reason==O::Termination::NodeLimit&&calls==0); + options={};reset();auto measured=O::solve_scenarios(model,defs,options);assert(measured.all_resolved()); + bool partial=false; + for(std::size_t cap=0;cap<=measured.work;++cap){reset();options.max_work=cap;auto capped=O::solve_scenarios(model,defs,options); + assert(capped.work<=cap);if(capped.resolved>0&&capped.resolved<3)partial=true; + if(!capped.all_resolved())assert(capped.stop_reason==O::Termination::IterationLimit); + if(capped.resolved)assert(capped.outcomes[0].result->has_solution());} + assert(partial); + // The private historical snapshot survives original edits/destruction. + O::ScenarioBatchResult historical; + {O::Model local;auto v=local.add_integer(1,2);local.minimize({{v,1}},5);reset();options={};historical=O::solve_scenarios(local,{{}},options);} + assert(historical.all_resolved()&&historical.batch->materialize(historical.batch->scenario(0)).objective.offset==5); + // A public snapshot can guess the next process-local owner number; the + // artifact must still keep its private identity distinct from that source. + O::Model predicted;auto p=predicted.add_integer(0,1);predicted.minimize({{p,1}}); + auto forged=predicted.snapshot();++forged.model_id; + forged.variables[0].variable.model_id=forged.model_id;forged.objective.terms[0].variable.model_id=forged.model_id; + reset();auto distinct=O::solve_scenarios(forged,{{}},options); + assert(distinct.all_resolved()&&distinct.batch->id()!=forged.model_id); + std::cout<<"Scenario coordinator evidence, every work boundary and interruption gates passed\n"; +} diff --git a/test/optimize/search_checkpoint.cpp b/test/optimize/search_checkpoint.cpp new file mode 100644 index 0000000000..bd91e98adb --- /dev/null +++ b/test/optimize/search_checkpoint.cpp @@ -0,0 +1,176 @@ +// Standalone correctness checks for propagation-count checkpoints. +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace Gecode; + +static void require(bool condition, const char* message) { + if (!condition) + throw std::runtime_error(message); +} + +class Queens : public Space { +public: + IntVarArray q; + Queens(int n, bool nary) : q(*this,n,0,n-1) { + IntArgs up(n), down(n); + for (int i=0; i values(void) const { + std::vector result(q.size()); + for (int i=0; i> queens_expected(int n) { + std::set> expected; + std::vector permutation(n); + for (int i=0; i(b); + rel(*this,objective,maximize ? IRT_GR : IRT_LE,best.objective.val()); + } +}; + +static int knapsack_expected(bool maximize) { + int best=maximize ? -1 : std::numeric_limits::max(); + for (unsigned int mask=0; mask<(1U<=capacity)) + best=maximize ? std::max(best,v) : std::min(best,v); + } + return best; +} + +int main(void) { + try { + require(Search::Options().c_p == 0,"checkpointing must default off"); + const unsigned long thresholds[]={0,1,10}; + const unsigned int distances[]={1,8,32}; + const unsigned int adaptive[]={2,16}; + const int sizes[]={3,4,6,7}; + unsigned int runs=0; + std::vector disabled_propagations; + bool checkpoint_policy_exercised=false; + for (unsigned long threshold : thresholds) { + unsigned int profile_index=0; + for (unsigned int distance : distances) + for (unsigned int adapt : adaptive) { + Search::Options options; + options.threads=1; + options.c_p=threshold; + options.c_d=distance; + options.a_d=adapt; + for (int size : sizes) + for (bool nary : {false,true}) { + const auto expected=queens_expected(size); + Queens* root=new Queens(size,nary); + DFS engine(root,options); + delete root; + std::set> actual; + while (Queens* solution=engine.next()) { + const auto assignment=solution->values(); + delete solution; + require(actual.insert(assignment).second,"duplicate DFS solution"); + } + require(!engine.stopped(),"DFS stopped before exhaustive completion"); + require(actual == expected,"DFS solution set differs from brute force"); + const unsigned long int propagations=engine.statistics().propagate; + if (threshold == 0) + disabled_propagations.push_back(propagations); + else if (propagations != disabled_propagations[profile_index]) + checkpoint_policy_exercised=true; + ++profile_index; + ++runs; + } + for (bool maximum : {false,true}) { + Knapsack* root=new Knapsack(maximum); + BAB engine(root,options); + delete root; + int best=maximum ? -1 : std::numeric_limits::max(); + while (Knapsack* solution=engine.next()) { + int w=0, v=0; + for (int i=0; ix[i].val(); + v+=item_values[i]*solution->x[i].val(); + } + const int candidate=solution->objective.val(); + delete solution; + require(maximum ? w<=capacity : w>=capacity,"infeasible BAB solution"); + require(candidate == v,"incorrect BAB objective"); + require(maximum ? candidate>best : candidate +#include +#include +#include +#include +#include +#include +#include + +namespace O = Gecode::Optimize; +constexpr double inf = std::numeric_limits::infinity(); +static void close(double a, double b) { assert(std::abs(a-b) < 1e-6); } +static O::SolveResult compare(O::SolveSession& session, const O::ModelSnapshot& model, + const O::SolveOptions& options = {}) { + const auto warm = session.solve(model, options); + const auto cold = O::solve(model, options); + assert(warm.termination == cold.termination); + assert(warm.has_solution() == cold.has_solution()); + if (warm.has_solution()) { + assert(O::validate(model, warm.values).valid); + close(*warm.objective, *cold.objective); + } + return warm; +} + +int main() { + O::Model model; + const auto x = model.add_continuous(0, 100); + const auto y = model.add_continuous(0, 100); + const auto demand = model.add_row({{x,1},{y,1}}, 4, inf); + model.minimize({{x,2},{y,3}}, -3); + O::SolveSession session; + O::SolveOptions options; + options.time_limit_seconds = 0; + assert(session.solve(model, options).termination == O::Termination::TimeLimit); + assert(session.statistics().model_loads == 0); + options = {}; + if (!O::capabilities().available) { + assert(session.solve(model).termination == O::Termination::Unsupported); + assert(session.statistics().model_loads == 0); + std::cout << "session backend-unavailable contract passed\n"; + return 0; + } + auto first = compare(session, model.snapshot()); + close(*first.objective, 5); + assert(session.statistics().model_loads == 1); + model.set_bounds(demand, 8, inf); + close(*compare(session, model.snapshot()).objective, 13); + assert(session.statistics().incremental_updates == 1); + assert(session.statistics().basis_warm_starts == 1); + model.set_bounds(x, 0, 3); + close(*compare(session, model.snapshot()).objective, 18); + model.maximize({{x,-4},{y,-2}}, 10); + close(*compare(session, model.snapshot()).objective, -6); + assert(session.statistics().model_loads == 1); + close(first.value(x), 4); // Historical result survives every mutation. + + // Public snapshots can change without a revision increment: content decides. + auto snapshot = model.snapshot(); + snapshot.rows[demand.id].lower = 9; + close(*compare(session, snapshot).objective, -8); + snapshot.rows[demand.id].terms[0].coefficient = 2; + compare(session, snapshot); + assert(session.statistics().model_loads == 2); + snapshot.variables[x.id].type = O::VariableType::Integer; + compare(session, snapshot); + assert(session.statistics().model_loads == 3); + + O::Model typed; + const auto bit = typed.add_integer(0, 1); + typed.minimize({{bit,1}}); + compare(session, typed.snapshot()); + auto typed_snapshot = typed.snapshot(); + typed_snapshot.variables[bit.id].type = O::VariableType::Binary; + const auto type_loads = session.statistics().model_loads; + compare(session, typed_snapshot); + assert(session.statistics().model_loads == type_loads+1); + + // Row replacement with the same shape cannot reuse stale row identity. + compare(session, model.snapshot()); + auto loads = session.statistics().model_loads; + model.remove(demand); + model.add_row({{x,1},{y,1}}, 8, inf); + compare(session, model.snapshot()); + assert(session.statistics().model_loads == loads+1); + model.set_name(x, "renamed"); + auto unchanged = session.statistics().unchanged_models; + compare(session, model.snapshot()); + assert(session.statistics().unchanged_models == unchanged+1); + auto bad = model.snapshot(); + bad.rows.back().terms[0].variable.model_id = 0; + assert(session.solve(bad).termination == O::Termination::InvalidModel); + compare(session, model.snapshot()); + + // MIP starts are revalidated after each edit and user hints have precedence. + O::Model mip; + const auto z = mip.add_integer(0, 5); + mip.minimize({{z,1}}, 2); + close(*compare(session, mip.snapshot()).objective, 2); + mip.set_objective_offset(3); + auto warm = compare(session, mip.snapshot()); + assert(warm.start_submitted); + assert(session.statistics().incumbent_starts == 1); + mip.set_bounds(z, 2, 5); + warm = compare(session, mip.snapshot()); + assert(!warm.start_submitted); + close(*warm.objective, 5); + options.primal_start = {{z,4}}; + auto before = session.statistics().incumbent_starts; + warm = compare(session, mip.snapshot(), options); + assert(warm.start_submitted); + assert(session.statistics().incumbent_starts == before); + options = {}; + options.node_limit = 1; + compare(session, mip.snapshot(), options); + options = {}; + compare(session, mip.snapshot(), options); // Resets persistent node option. + options.cancellation = std::make_shared(); + options.cancellation->cancel(); + assert(session.solve(mip, options).termination == O::Termination::Cancelled); + compare(session, mip.snapshot()); // Fresh token and no dangling callback. + + O::Model semi; + auto sc = semi.add_variable(O::VariableType::SemiContinuous, 3, 8); + auto si = semi.add_variable(O::VariableType::SemiInteger, 2.5, 7); + auto sc_row = semi.add_row({{sc,1}}, 0, inf); + auto si_row = semi.add_row({{si,1}}, 0, inf); + semi.minimize({{sc,2},{si,1}}, -1); + close(*compare(session, semi.snapshot()).objective, -1); + semi.set_bounds(sc_row, 1, inf); semi.set_bounds(si_row, 1, inf); + close(*compare(session, semi.snapshot()).objective, 8); + semi.set_bounds(sc_row, 0, inf); semi.set_bounds(si_row, 0, inf); + close(*compare(session, semi.snapshot()).objective, -1); + + O::Model logic; + auto on = logic.add_binary(); auto quantity = logic.add_continuous(0, 10); + O::add_indicator(logic, on, true, {{quantity,1}}, 7, inf); + logic.minimize({{on,-20},{quantity,1}}); + close(*compare(session, logic.snapshot()).objective, -13); + logic.set_bounds(quantity, 8, 10); + close(*compare(session, logic.snapshot()).objective, -12); + logic.minimize({{on,20},{quantity,1}}); + close(*compare(session, logic.snapshot()).objective, 8); + + // Diagnostic reductions can leave only constant rows and integer columns. + // These must never enter HiGHS MIP presolve with an empty nonzero matrix. + O::Model constants; + const auto unused_bit = constants.add_binary(); + const auto constant_row = constants.add_row({}, -1, 1); + constants.minimize({{unused_bit,2}}, -4); + close(*compare(session, constants.snapshot()).objective, -4); + constants.set_bounds(constant_row, 1, inf); + assert(compare(session, constants.snapshot()).termination == O::Termination::Infeasible); + constants.set_bounds(constant_row, 0, inf); + close(*compare(session, constants.snapshot()).objective, -4); + constants.set_bounds(constant_row, 1e-8, inf); + close(*compare(session, constants.snapshot()).objective, -4); + O::SolveOptions tighter; + tighter.feasibility_tolerance = 1e-9; + assert(compare(session, constants.snapshot(), tighter).termination == O::Termination::Infeasible); + + // Seeded finite integer edits checked by a separate exhaustive oracle. + O::Model finite; + const auto a = finite.add_integer(-3, 5); + const auto b = finite.add_integer(-3, 5); + const auto row = finite.add_row({{a,2},{b,-1}}, 1, inf); + std::mt19937 rng(219); + for (int iteration=0; iteration<80; ++iteration) { + const int cost_a = int(rng()%9)-4, cost_b = int(rng()%9)-4; + const int lower = int(rng()%14)-4; + finite.set_bounds(row, lower, inf); + finite.minimize({{a,double(cost_a)},{b,double(cost_b)}}, -7); + double expected = inf; + for (int av=-3; av<=5; ++av) + for (int bv=-3; bv<=5; ++bv) + if (2*av-bv >= lower) + expected = std::min(expected, double(cost_a*av+cost_b*bv-7)); + const auto answer = session.solve(finite); + assert(answer.termination == O::Termination::Optimal && answer.has_solution()); + close(*answer.objective, expected); + } + + // LP infeasibility/unboundedness must not poison a later feasible revision. + O::Model lp; + const auto free = lp.add_continuous(-inf, inf); + lp.minimize({{free,1}}); + assert(compare(session, lp.snapshot()).termination == O::Termination::Unbounded); + lp.set_bounds(free, 2, 5); + close(*compare(session, lp.snapshot()).objective, 2); + const auto impossible = lp.add_row({{free,1}}, 6, inf); + assert(compare(session, lp.snapshot()).termination == O::Termination::Infeasible); + lp.set_bounds(impossible, 3, inf); + close(*compare(session, lp.snapshot()).objective, 3); + options = {}; + options.time_limit_seconds = 0.5; + for (int i=0; i<30; ++i) { + lp.set_objective_offset(i); + close(*compare(session, lp.snapshot(), options).objective, i+3); + } + + auto transferred = std::move(session); + assert(session.solve(lp).termination == O::Termination::InvalidModel); + compare(transferred, lp.snapshot()); + session.reset(); + assert(session.statistics().solve_calls == 0); + compare(session, lp.snapshot()); + assert(session.statistics().model_loads == 1); + O::Model empty; + empty.minimize({}, 11); + close(*session.solve(empty).objective, 11); + compare(session, lp.snapshot()); + assert(session.statistics().model_loads == 2); + O::Model moved; + O::Model owner(std::move(moved)); + assert(session.solve(moved).termination == O::Termination::InvalidModel); + std::cout << "persistent session and exhaustive edit conformance passed\n"; +} diff --git a/test/optimize/session_limits.cpp b/test/optimize/session_limits.cpp new file mode 100644 index 0000000000..7c2659e830 --- /dev/null +++ b/test/optimize/session_limits.cpp @@ -0,0 +1,93 @@ +/* Persistent option reset and callback lifetime regression, HiGHS 1.15.1. */ +#include +#include +#include +#include +#include +#include +#include + +namespace O=Gecode::Optimize; +namespace { +constexpr double inf=std::numeric_limits::infinity(); +void require(bool condition,const char* message){if(!condition)throw std::runtime_error(message);} +void near(double a,double b){require(std::isfinite(a)&&std::isfinite(b)&&std::abs(a-b)<1e-6,"objective mismatch");} + +O::Model node_fixture(){ + // Fixed mt19937 outputs (no implementation-dependent uniform distribution). + // Five-dimensional 0/1 knapsack. With HiGHS 1.15.1, seed 219, one thread + // and zero gap, node one leaves incumbent 1579 and upper bound 1590. + // Both a cold run and a retained-incumbent run need more search to close + // that gap. Do not replace it with a root-solved bound-only MIP. + std::mt19937 random(219); + O::Model m;std::vector variables;std::vector objective; + for(int i=0;i<48;++i){variables.push_back(m.add_binary());objective.push_back({variables.back(),double(1+random()%100)});} + for(int row=0;row<5;++row){std::vector terms;int sum=0; + for(auto v:variables){const int weight=1+random()%100;sum+=weight;terms.push_back({v,double(weight)});} + m.add_row(terms,-inf,(45*sum)/100); + } + m.maximize(objective);return m; +} + +O::SolveOptions unlimited_options(){ + O::SolveOptions options;options.threads=1;options.random_seed=219; + options.relative_gap=options.absolute_gap=0;options.time_limit_seconds=120; + return options; +} + +void require_limited(const O::Model& model,const O::SolveResult& result){ + require(result.termination==O::Termination::NodeLimit,"fixture no longer exercises a post-registration node cutoff"); + require(result.has_solution(),"node-limited fixture lost its known incumbent"); + require(O::validate(model.snapshot(),result.values).valid,"limited incumbent fails original validation"); + require(result.best_bound&&*result.best_bound>*result.objective+1,"node-one bound already closes the gap; reset test is vacuous"); +} +} + +int main(){ + try{ + auto model=node_fixture();O::SolveSession session; + if(!O::capabilities().available){ + require(session.solve(model).termination==O::Termination::Unsupported,"missing-backend session contract"); + std::cout<<"session limits backend-unavailable contract passed\n";return 0; + } + auto limited_options=unlimited_options();limited_options.node_limit=1; + const auto first=session.solve(model,limited_options);require_limited(model,first); + require(session.statistics().model_loads==1,"first limited model was not loaded"); + + // Positive control: retained hints alone must not let node one solve the + // fixture. Thus removing the persistent mip_max_nodes reset is detected. + const auto again=session.solve(model,limited_options);require_limited(model,again); + require(again.start_submitted&&session.statistics().incumbent_starts==1,"retained incumbent path was not exercised"); + + // A NEW options object has no node limit. Neither the previous native + // option nor its budget/callback captures may survive this call. + const auto unlimited=unlimited_options(); + const auto warm=session.solve(model,unlimited); + const auto cold=O::solve(model,unlimited); + require(warm.termination==O::Termination::Optimal&&warm.has_solution(),"old node limit contaminated unlimited session solve"); + require(cold.termination==O::Termination::Optimal&&cold.has_solution(),"cold oracle did not complete"); + require(O::validate(model.snapshot(),warm.values).valid&&O::validate(model.snapshot(),cold.values).valid,"completed original-model validation failed"); + near(*warm.objective,*cold.objective);near(*warm.objective,1579); + require(session.statistics().model_loads==1,"option reset unexpectedly reloaded the model"); + require(warm.start_submitted&&session.statistics().incumbent_starts==2,"unlimited solve did not exercise incumbent reuse"); + + // NodeLimit with a positive limit is only obtained after highs.run() and + // callback registration; the adapter's pre-run node counter is zero. + // Follow an interruption with a changed model to exercise teardown + // and replacement of callbacks that captured expired budgets/vectors. + // This is deterministic post-registration LIMIT interruption, not a claim + // to deterministically observe an asynchronous CancellationToken callback. + // That narrower test needs a registration hook; sleep-based races are not + // used here. Run this executable with the full library under ASan/UBSan. + { + require_limited(model,session.solve(model,limited_options)); + O::Model lp;const auto x=lp.add_continuous(2,5);lp.minimize({{x,3}},-1); + const auto fresh=session.solve(lp,unlimited_options()); + require(fresh.termination==O::Termination::Optimal&&fresh.has_solution(),"post-interruption fresh model failed"); + near(fresh.value(x),2);near(*fresh.objective,5); + // The original owning result must survive replacements and interrupts. + near(*first.objective,1579);require(first.has_solution(),"historical limited result was invalidated"); + } + std::cout<<"persistent node-limit reset and post-registration callback lifetime passed\n";return 0; + }catch(const std::exception& error){std::cerr<<"FAIL: "< +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace O = Gecode::Optimize; +constexpr double inf = std::numeric_limits::infinity(); +static void close(double a, double b) { assert(std::abs(a-b) < 1e-6); } + +int main() { + O::Model basic; + auto x = basic.add_continuous(0, 100, "x"); + basic.add_row({{x, 1}}, 4, inf, "demand"); + basic.minimize({{x, 2}}, -3); + O::SolveOptions options; + options.guarantee = O::Guarantee::Certified; + assert(O::solve(basic, options).termination == O::Termination::Unsupported); + options.guarantee = O::Guarantee::Numerical; + options.time_limit_seconds = 0; + assert(O::solve(basic, options).termination == O::Termination::TimeLimit); + options.time_limit_seconds = inf; + options.cancellation = std::make_shared(); + options.cancellation->cancel(); + assert(O::solve(basic, options).termination == O::Termination::Cancelled); + options = {}; + auto malformed = basic.snapshot(); + malformed.rows[0].terms[0].variable.model_id = 0; + assert(O::solve(malformed).termination == O::Termination::InvalidModel); + O::Model moved_from; + O::Model moved_to(std::move(moved_from)); + assert(O::solve(moved_from).termination==O::Termination::InvalidModel); + if (!O::capabilities().available) { + assert(O::solve(basic).termination == O::Termination::Unsupported); + std::cout << "backend-unavailable contract passed\n"; + return 0; + } + auto result = O::solve(basic); + assert(result.termination == O::Termination::Optimal && result.has_solution()); + close(result.value(x), 4); close(*result.objective, 5); + assert(result.backend == "HiGHS" && !result.backend_version.empty()); + assert(result.guarantee == O::Guarantee::Numerical); + const auto old_revision = result.revision; + basic.set_bounds(x, 5, 100); + auto edited = O::solve(basic); + close(*edited.objective, 7); close(result.value(x), 4); + assert(edited.revision > old_revision); + basic.maximize({{x, -2}}, 3); + auto maximum = O::solve(basic); + close(*maximum.objective, -7); close(maximum.value(x), 5); + if (maximum.relative_gap) close(*maximum.relative_gap, 0); + + O::Model mixed; + auto open = mixed.add_binary("open"); + auto q = mixed.add_continuous(0, 100, "quantity"); + auto count = mixed.add_integer(0, 4, "count"); + mixed.add_row({{q,1},{open,-100}}, -inf, 0, "capacity"); + mixed.add_row({{q,1},{count,3}}, 43, inf, "demand"); + mixed.minimize({{open,12},{q,0.5},{count,2}}); + auto mip = O::solve(mixed); + assert(mip.termination == O::Termination::Optimal && mip.has_solution()); + close(*mip.objective, 33.5); + assert(O::validate(mixed.snapshot(), mip.values).valid); + + // Sparse starts guide the solver without fixing the supplied variables. + options.primal_start = {{open,1}}; + auto started = O::solve(mixed,options); + assert(started.start_submitted && started.has_solution()); + close(*started.objective,33.5); + options.primal_start = {{open,1},{q,50},{count,0}}; + started = O::solve(mixed,options); + assert(started.start_submitted && started.has_solution()); + close(*started.objective,33.5); + options.primal_start = {{open,0},{q,0},{count,0}}; + assert(O::solve(mixed,options).termination==O::Termination::InvalidModel); + options.primal_start = {{open,1},{open,1}}; + assert(O::solve(mixed,options).termination==O::Termination::InvalidModel); + options.primal_start = {{x,5}}; + assert(O::solve(mixed,options).termination==O::Termination::InvalidModel); + options.primal_start = {{count,1.5}}; + assert(O::solve(mixed,options).termination==O::Termination::InvalidModel); + options.primal_start = {{q,std::numeric_limits::quiet_NaN()}}; + assert(O::solve(mixed,options).termination==O::Termination::InvalidModel); + options = {}; + options.threads=2; + assert(O::solve(mixed,options).termination==O::Termination::Unsupported); + options = {}; + + // Cross-feature conformance: typed indicators retain their original meaning + // through native backend presolve, primal starts and multiobjective locks. + O::Model logical; + auto enabled=logical.add_binary("enabled"); + auto amount=logical.add_continuous(0,10,"amount"); + auto lower=O::add_indicator(logical,enabled,true,{{amount,1}},7,inf); + O::add_indicator(logical,enabled,false,{{amount,1}},-inf,3); + logical.minimize({{enabled,-20},{amount,1}}); + options.primal_start={{enabled,0},{amount,0}}; + auto lr=O::solve(logical,options); + assert(lr.has_solution() && lr.start_submitted); + close(lr.value(enabled),1); close(lr.value(amount),7); close(*lr.objective,-13); + O::ObjectiveData first{{{enabled,1}},0,O::ObjectiveSense::Maximize}; + O::ObjectiveData second{{{amount,1}},0,O::ObjectiveSense::Minimize}; + auto lex=O::solve_lexicographic(logical,{{first,0,0,"activate"},{second,0,0,"quantity"}},options); + assert(lex.completed_numerically() && lex.has_solution()); + close(lex.final_solution.value(amount),7); + O::remove_indicator(logical,lower.indicator); + options={}; + lr=O::solve(logical); + assert(lr.has_solution()); close(*lr.objective,-20); + + O::Model infeasible; + auto z = infeasible.add_integer(0, 0.5); + infeasible.add_row({{z,1}}, 0.25, inf); + assert(O::solve(infeasible).termination == O::Termination::Infeasible); + O::Model unbounded; + auto free = unbounded.add_continuous(-inf,inf); + unbounded.minimize({{free,1}}); + auto unb = O::solve(unbounded); + assert(unb.termination == O::Termination::Unbounded || unb.termination == O::Termination::InfeasibleOrUnbounded); + O::Model empty; + empty.minimize({},7); + auto emp = O::solve(empty); assert(emp.has_solution()); close(*emp.objective,7); + empty.add_row({},1,inf); + assert(O::solve(empty).termination == O::Termination::Infeasible); + options.time_limit_seconds=0; + assert(O::solve(empty,options).termination==O::Termination::TimeLimit); + options = {}; + + O::Model deleted; + auto dead = deleted.add_binary(); + deleted.remove(dead); + auto live = deleted.add_integer(2,5); + deleted.minimize({{live,1}}); + auto del = O::solve(deleted); close(del.value(live),2); + bool threw = false; try { del.value(dead); } catch (const O::ModelError&) { threw=true; } + assert(threw && std::isnan(del.values[dead.id])); + + O::Model semi; + auto s = semi.add_variable(O::VariableType::SemiContinuous,3,8,"semi"); + semi.add_row({{s,1}},1,inf); + semi.minimize({{s,1}}); + auto sr = O::solve(semi); assert(sr.has_solution()); close(sr.value(s),3); + semi.set_bounds(semi.snapshot().rows[0].constraint,0,inf); + sr=O::solve(semi); assert(sr.has_solution()); close(sr.value(s),0); + options.primal_start={{s,0}}; + sr=O::solve(semi,options); + assert(sr.has_solution() && sr.start_submitted); close(sr.value(s),0); + auto semi_other=semi.add_binary("other"); + sr=O::solve(semi,options); // A zero-valued semi variable in a partial start. + assert(sr.has_solution() && sr.start_submitted); close(sr.value(s),0); + options.primal_start={{s,0},{semi_other,0}}; + sr=O::solve(semi,options); assert(sr.has_solution() && sr.start_submitted); + options={}; + O::Model semi_integer; + auto si=semi_integer.add_variable(O::VariableType::SemiInteger,3,8); + semi_integer.minimize({{si,1}}); + options.primal_start={{si,0}}; + sr=O::solve(semi_integer,options); + assert(sr.has_solution() && sr.start_submitted); close(sr.value(si),0); + options={}; + + O::Model tiny; + auto t = tiny.add_continuous(0,1); + tiny.add_row({{t,1e-14}},0,1); + assert(O::solve(tiny).termination == O::Termination::Unsupported); + + // Differential tiny MILPs: exhaustive integer choice plus analytic recourse. + std::mt19937 rng(1701); + for (int iteration=0; iteration<30; ++iteration) { + const int demand=1+static_cast(rng()%12), fixed=1+static_cast(rng()%7); + const int rate=1+static_cast(rng()%4); + O::Model problem; + auto n=problem.add_integer(0,5), y=problem.add_continuous(0,20); + problem.add_row({{n,3},{y,1}},demand,inf); + problem.minimize({{n,static_cast(fixed)},{y,static_cast(rate)}},-10); + double oracle=inf; + for (int i=0;i<=5;++i) oracle=std::min(oracle,-10.0+fixed*i+rate*std::max(0,demand-3*i)); + auto r=O::solve(problem); assert(r.termination==O::Termination::Optimal && r.has_solution()); + close(*r.objective,oracle); + } + + const auto temp = std::filesystem::temp_directory_path()/ + ("gecode-optimize-test-"+std::to_string(std::random_device{}())); + std::filesystem::create_directory(temp); + for (const auto extension : {".lp",".mps"}) { + auto path=(temp/(std::string("roundtrip")+extension)).string(); + O::write_model(mixed,path); + auto restored=O::read_model(path); + auto r=O::solve(restored); assert(r.has_solution()); close(*r.objective,*mip.objective); + assert(restored.snapshot().rows.size()==mixed.snapshot().rows.size()); + } + auto qp=(temp/"quadratic.lp").string(); + { std::ofstream file(qp); file << "Minimize\n obj: [ x ^ 2 ] / 2\nSubject To\n c: x >= 1\nEnd\n"; } + threw=false; try { auto ignored=O::read_model(qp); } catch (const std::exception&) { threw=true; } + assert(threw); + std::filesystem::remove_all(temp); + std::cout << "LP/MILP integration, original validation, limits and I/O passed\n"; +} diff --git a/test/optimize/test_process_containment.py b/test/optimize/test_process_containment.py new file mode 100644 index 0000000000..0a07d90bf9 --- /dev/null +++ b/test/optimize/test_process_containment.py @@ -0,0 +1,467 @@ +"""Windows ABI/failure mocks plus real Windows containment gates. + +Passing mocks on POSIX is NOT evidence that the Windows runtime port works. +RealWindowsTests must execute (not skip) on both Windows Python architectures. +""" +import ctypes as C +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import time +import unittest +from unittest import mock + +SPEC = importlib.util.spec_from_file_location("process_containment", Path(__file__).with_name("process_containment.py")) +P = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(P) + + +class FakeAPI: + """Track kernel ownership and inject BOOL/DWORD failures without launching.""" + def __init__(self, fail=None): + self.fail = fail or {} + self.calls = [] + self.counts = {} + self.live = set() + self.closed = [] + self.attributes = {} + self.error = 5 + self.next_handle = (1 << 40) if C.sizeof(P.HANDLE) == 8 else 100 + self.process = self.thread = self.job = None + self.member = True + self.exited = False + self.active = 0 + self.keep_active = False + self.exit_code = 259 # This is a valid exit value AFTER a signalled wait. + self.resume_result = 1 + self.wait_result = None + + def ok(self, name): + self.calls.append(name) + self.counts[name] = self.counts.get(name, 0) + 1 + return self.fail.get(name) != self.counts[name] + + def handle(self): + self.next_handle += 1 + self.live.add(self.next_handle) + return self.next_handle + + def last_error(self): + return self.error + + def GetCurrentProcess(self): + return -1 + + def CreateJobObjectW(self, security, name): + assert security is None and name is None + if not self.ok("create_job"): + return None + self.job = self.handle() + return self.job + + def SetInformationJobObject(self, job, kind, data, size): + assert job == self.job and kind == 9 + assert size == C.sizeof(P.JOBOBJECT_EXTENDED_LIMIT_INFORMATION) + assert data._obj.BasicLimitInformation.LimitFlags == P.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + return self.ok("set_limits") + + def DuplicateHandle(self, source, handle, target, result, access, inherit, options): + assert source == target == -1 and access == 0 and inherit and options == 2 + if not self.ok("duplicate"): + return False + result._obj.value = self.handle() + return True + + def InitializeProcThreadAttributeList(self, storage, count, flags, size): + assert count == 2 and flags == 0 + if storage is None: + self.error = P.ERROR_INSUFFICIENT_BUFFER if self.ok("attribute_size") else 5 + size._obj.value = 128 + return False + self.error = 5 + return self.ok("attribute_init") + + def UpdateProcThreadAttribute(self, storage, flags, key, data, size, previous, returned): + assert flags == 0 and previous is None and returned is None + assert size == C.sizeof(data._obj) + self.attributes[key] = list(data._obj) + return self.ok("attribute_handles" if key == P.PROC_THREAD_ATTRIBUTE_HANDLE_LIST else "attribute_jobs") + + def DeleteProcThreadAttributeList(self, storage): + self.ok("attribute_delete") + + def CreateProcessW(self, application, command, ps, ts, inherit, flags, env, cwd, startup, result): + assert application == r"C:\python.exe" and command.value.startswith(r"C:\python.exe") + assert ps is None and ts is None and env is None and inherit + assert flags == P.CREATE_SUSPENDED | P.CREATE_NO_WINDOW | P.EXTENDED_STARTUPINFO_PRESENT + assert self.attributes[P.PROC_THREAD_ATTRIBUTE_JOB_LIST] == [self.job] + handles = self.attributes[P.PROC_THREAD_ATTRIBUTE_HANDLE_LIST] + assert len(handles) == 3 and self.job not in handles + si = startup._obj.StartupInfo + assert si.cb == C.sizeof(P.STARTUPINFOEXW) and si.dwFlags == P.STARTF_USESTDHANDLES + assert handles == [si.hStdInput, si.hStdOutput, si.hStdError] + if not self.ok("create_process"): + return False + self.process, self.thread = self.handle(), self.handle() + self.active = 1 + result._obj.hProcess, result._obj.hThread = self.process, self.thread + result._obj.dwProcessId, result._obj.dwThreadId = 73, 74 + return True + + def IsProcessInJob(self, process, job, result): + assert process == self.process and job == self.job + result._obj.value = self.member + return self.ok("membership") + + def ResumeThread(self, thread): + assert thread == self.thread + if not self.ok("resume"): + return P.WAIT_FAILED + self.exited = True + return self.resume_result + + def WaitForSingleObject(self, process, milliseconds): + assert process == self.process and 0 <= milliseconds < P.WAIT_FAILED + if not self.ok("wait"): + return P.WAIT_FAILED + if self.wait_result is not None: + return self.wait_result + return P.WAIT_OBJECT_0 if self.exited else P.WAIT_TIMEOUT + + def GetExitCodeProcess(self, process, result): + assert process == self.process + result._obj.value = self.exit_code + return self.ok("exit_code") + + def TerminateJobObject(self, job, code): + assert job == self.job and code == 124 + if not self.ok("terminate_job"): + return False + self.exited = True + if not self.keep_active: + self.active = 0 + return True + + def TerminateProcess(self, process, code): + assert process == self.process and code == 124 + if not self.ok("terminate_process"): + return False + self.exited = True + return True + + def QueryInformationJobObject(self, job, kind, data, size, returned): + assert job == self.job and kind == 1 and returned is None + assert size == C.sizeof(P.JOBOBJECT_BASIC_ACCOUNTING_INFORMATION) + data._obj.ActiveProcesses = self.active + return self.ok("query") + + def CloseHandle(self, handle): + assert handle in self.live, "double close or borrowed-handle close" + if not self.ok("close"): + return False + self.live.remove(handle) + self.closed.append(handle) + if handle == self.job: + self.active = 0 # KILL_ON_JOB_CLOSE fallback. + self.exited = True + return True + + +class MockWindowsTests(unittest.TestCase): + def start(self, api): + process = P.WindowsJobProcess(api=api, fd_to_handle=lambda fd: fd + 1000) + with tempfile.TemporaryFile() as output: + process.start([r"C:\python.exe", "-c", "pass"], output, output, r"C:\work") + return process + + def test_native_width_structures(self): + wide = C.sizeof(P.HANDLE) == 8 + self.assertEqual(C.sizeof(P.DWORD), 4) + self.assertEqual(C.sizeof(P.BOOL), 4) + self.assertEqual(C.sizeof(P.STARTUPINFOW), 104 if wide else 68) + self.assertEqual(P.STARTUPINFOW.dwFlags.offset, 60 if wide else 44) + self.assertEqual(P.STARTUPINFOW.hStdInput.offset, 80 if wide else 56) + self.assertEqual(C.sizeof(P.STARTUPINFOEXW), 112 if wide else 72) + self.assertEqual(C.sizeof(P.PROCESS_INFORMATION), 24 if wide else 16) + self.assertEqual(C.sizeof(P.JOBOBJECT_BASIC_LIMIT_INFORMATION), 64 if wide else 48) + self.assertEqual(C.sizeof(P.JOBOBJECT_EXTENDED_LIMIT_INFORMATION), 144 if wide else 112) + self.assertEqual(C.sizeof(P.IO_COUNTERS), 48) + self.assertEqual(C.sizeof(P.JOBOBJECT_BASIC_ACCOUNTING_INFORMATION), 48) + self.assertEqual(P.JOBOBJECT_BASIC_ACCOUNTING_INFORMATION.ActiveProcesses.offset, 40) + + def test_atomic_assignment_precedes_resume_and_handles_are_owned(self): + api = FakeAPI() + process = self.start(api) + order = [api.calls.index(name) for name in ( + "set_limits", "attribute_handles", "attribute_jobs", "create_process", "membership", "resume")] + self.assertEqual(order, sorted(order)) + self.assertEqual(api.live, {api.job, api.process}) + self.assertEqual(process.poll(), 259) + process.cleanup(0.1) + self.assertFalse(api.live) + self.assertEqual(api.closed[-1], api.job) + with self.assertRaises(ValueError): + process.start([], None, None, r"C:\work") + + def test_setup_failures_do_not_run_or_leave_children(self): + scenarios = [(name, 1) for name in ("create_job", "set_limits", "attribute_size", "attribute_init", + "attribute_handles", "attribute_jobs", "create_process", "membership", "resume")] + scenarios += [("duplicate", number) for number in (1, 2, 3)] + scenarios += [("close", number) for number in (1, 2, 3, 4)] + for operation, count in scenarios: + with self.subTest(operation=operation, count=count): + api = FakeAPI({operation: count}) + with self.assertRaises(OSError): + self.start(api) + self.assertFalse(api.live, api.calls) + self.assertEqual(api.active, 0) + if operation not in ("resume", "close"): + self.assertNotIn("resume", api.calls) + if api.process is not None: + self.assertIn("terminate_job", api.calls) + + def test_failed_membership_or_suspend_count_kills_created_process(self): + for member, count in ((False, 1), (True, 0), (True, 2)): + with self.subTest(member=member, count=count): + api = FakeAPI() + api.member, api.resume_result = member, count + with self.assertRaises(OSError): + self.start(api) + if not member: + self.assertNotIn("resume", api.calls) + self.assertIn("terminate_process", api.calls) + self.assertFalse(api.live) + + def test_cleanup_failures_are_reported_and_all_handles_attempted(self): + for operation in ("terminate_job", "wait", "exit_code", "query", "close"): + with self.subTest(operation=operation): + api = FakeAPI() + process = self.start(api) + api.fail[operation] = api.counts.get(operation, 0) + 1 + with self.assertRaises(OSError): + process.cleanup(0.01) + self.assertIsNone(process.job) + self.assertEqual(api.active, 0) + if operation == "terminate_job": + self.assertIn("terminate_process", api.calls) + if operation == "close": + # Retain a handle whose close failed; retry is explicit. + self.assertEqual(api.live, {api.process}) + process.cleanup(0.01) + self.assertFalse(api.live) + + def test_job_termination_failure_still_uses_kill_on_close(self): + api = FakeAPI() + process = self.start(api) + api.fail.update(terminate_job=1, terminate_process=1) + with self.assertRaises(OSError) as error: + process.cleanup(0) + self.assertIn("TerminateJobObject", str(error.exception)) + self.assertIn("TerminateProcess", str(error.exception)) + self.assertFalse(api.live) + self.assertEqual(api.active, 0) + + def test_failed_job_close_is_retained_for_watchdog_retry(self): + api = FakeAPI() + process = self.start(api) + api.fail["close"] = api.counts["close"] + 2 # process closes; job close fails. + with self.assertRaisesRegex(OSError, "CloseHandle\\(job\\)"): + process.cleanup(.01) + self.assertEqual(process.job, api.job) + self.assertEqual(api.live, {api.job}) + process.terminate() + process.cleanup(.01) + self.assertFalse(api.live) + + def test_descendant_wait_is_bounded_and_closes_job_on_timeout(self): + api = FakeAPI() + process = self.start(api) + api.keep_active = True + with mock.patch.object(P.time, "monotonic", side_effect=[10, 10, 10.1]): + with self.assertRaisesRegex(OSError, "descendants"): + process.cleanup(0.01) + self.assertFalse(api.live) + + def test_waits_never_use_infinite_and_failed_wait_is_not_completion(self): + api = FakeAPI() + process = self.start(api) + api.wait_result = P.WAIT_TIMEOUT + self.assertIsNone(process.poll()) + for timeout in (0, 0.0001, 1e308): + with self.assertRaises(subprocess.TimeoutExpired): + process.wait(timeout) + api.wait_result = P.WAIT_FAILED + with self.assertRaises(OSError): + process.poll() + for timeout in (-1, float("inf"), float("nan")): + with self.assertRaises(ValueError): + process.wait(timeout) + api.wait_result = None + process.cleanup(0.01) + + def test_command_validation_prevents_shell_and_length_ambiguity(self): + bad = ([], ["python.exe"], [r"\python.exe"], [r"C:python.exe"], + [r"C:\python.exe", "x\0y"], [r"C:\python.exe", "😀" * 16384]) + for command in bad: + with self.subTest(command=repr(command)[:60]), self.assertRaises(ValueError): + P.checked_command(command, r"C:\work") + with self.assertRaises(ValueError): + P.checked_command([r"C:\python.exe"], "relative") + arguments = [r"C:\Program Files\python.exe", "", "x y", 'a"b', "unicode-λ"] + _, _, line = P.checked_command(arguments, r"C:\work") + self.assertEqual(line, subprocess.list2cmdline(arguments)) + + +@unittest.skipUnless(os.name == "nt", "requires actual Windows APIs; mocks do not establish runtime support") +class RealWindowsTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix="optimize-containment-") + self.directory = Path(self.temp.name) + + def tearDown(self): + self.temp.cleanup() + + def launch(self, source, args=(), api=None): + output, error = tempfile.TemporaryFile(dir=self.directory), tempfile.TemporaryFile(dir=self.directory) + self.addCleanup(output.close) + self.addCleanup(error.close) + process = P.WindowsJobProcess(api=api) + self.addCleanup(process.cleanup, 1) + process.start([sys.executable, "-c", source, *args], output, error, self.directory) + return process, output, error + + def test_suspended_membership_before_resume_and_unicode_stdio(self): + marker = self.directory/"first-instruction" + api = P.WinAPI() + original = api.ResumeThread + observed = [] + def resume(thread): + self.assertFalse(marker.exists()) + observed.append(True) + return original(thread) + api.ResumeThread = resume + arguments = ("", "space here", 'a"b', "trailing\\", "λ😀") + source = ("import pathlib,sys,json; pathlib.Path(sys.argv[1]).write_text('ran'); " + "print(json.dumps(sys.argv[2:])); print('diagnostic',file=sys.stderr); sys.exit(3)") + process, output, error = self.launch(source, (str(marker), *arguments), api) + self.assertEqual(process.wait(5), 3) + process.cleanup(1) + output.seek(0); error.seek(0) + self.assertEqual(json.loads(output.read()), list(arguments)) + self.assertIn(b"diagnostic", error.read()) + self.assertEqual(observed, [True]) + self.assertTrue(marker.exists()) + + def test_postcreation_verification_failure_never_runs_child(self): + for failure in ("membership_false", "membership_api", "resume_api"): + with self.subTest(failure=failure): + marker = self.directory/failure + api = P.WinAPI() + if failure == "membership_false": + api.IsProcessInJob = lambda process, job, result: 1 + elif failure == "membership_api": + api.IsProcessInJob = lambda process, job, result: 0 + else: + api.ResumeThread = lambda thread: P.WAIT_FAILED + with self.assertRaises(OSError): + self.launch("import pathlib; pathlib.Path("+repr(str(marker))+").write_text('ran')", api=api) + self.assertFalse(marker.exists()) + + def test_successful_root_cleanup_kills_descendant(self): + marker = self.directory/"descendant" + ready = self.directory/"child-ready" + child = ("import pathlib,time; pathlib.Path("+repr(str(ready))+").write_text('ready'); " + "time.sleep(.6); pathlib.Path("+repr(str(marker))+").write_text('escaped')") + source = ("import subprocess,sys,pathlib,time; subprocess.Popen([sys.executable,'-c',"+repr(child)+"]); " + "p=pathlib.Path("+repr(str(ready))+"); " + "exec('while not p.exists(): time.sleep(.005)')") + process, _, _ = self.launch(source) + self.assertEqual(process.wait(5), 0) + process.cleanup(1) + self.assertTrue(ready.exists(), "the descendant must actually have run") + time.sleep(.8) + self.assertFalse(marker.exists()) + + def test_unlisted_inheritable_handle_is_not_inherited(self): + import msvcrt + with (self.directory/"unlisted").open("wb") as borrowed: + handle = msvcrt.get_osfhandle(borrowed.fileno()) + os.set_handle_inheritable(handle, True) + # BY_HANDLE_FILE_INFORMATION is thirteen DWORDs: its volume serial + # and high/low file index identify the underlying file. A numeric + # handle can legitimately be reused for another object in the child. + kernel = C.WinDLL("kernel32", use_last_error=True) + info_function = kernel.GetFileInformationByHandle + info_function.argtypes, info_function.restype = [C.c_void_p, C.c_void_p], C.c_int32 + info = (C.c_uint32 * 13)() + self.assertTrue(info_function(handle, C.byref(info))) + identity = [info[7], info[11], info[12]] + source = ("import ctypes as C,sys,json; k=C.WinDLL('kernel32',use_last_error=True); " + "k.GetFileInformationByHandle.argtypes=[C.c_void_p,C.c_void_p]; " + "k.GetFileInformationByHandle.restype=C.c_int32; info=(C.c_uint32*13)(); " + "ok=k.GetFileInformationByHandle(int(sys.argv[1]),C.byref(info)); " + "print(json.dumps([info[7],info[11],info[12]] if ok else None))") + process, output, _ = self.launch(source, (str(handle),)) + self.assertEqual(process.wait(5), 0) + process.cleanup(1) + output.seek(0) + self.assertNotEqual(json.loads(output.read()), identity, + "the unrelated inheritable file escaped the explicit handle allowlist") + + def test_nested_containment_and_breakaway_rejection(self): + module_dir = str(Path(__file__).resolve().parent) + inner = ("import sys,tempfile,subprocess; sys.path.insert(0,"+repr(module_dir)+"); " + "from process_containment import WindowsJobProcess; " + "p=WindowsJobProcess(); out=tempfile.TemporaryFile(); " + "p.start([sys.executable,'-c','print(42)'],out,out,"+repr(str(self.directory))+"); " + "assert p.wait(5)==0; p.cleanup(1)") + process, _, error = self.launch(inner) + self.assertEqual(process.wait(8), 0) + process.cleanup(1) + error.seek(0) + self.assertEqual(error.read(), b"") + breaker = """import subprocess,sys +try: + subprocess.Popen([sys.executable,'-c','pass'],creationflags=0x01000000) +except OSError: + sys.exit(0) +else: + sys.exit(9) +""" + process, _, _ = self.launch(breaker) + self.assertEqual(process.wait(5), 0, "a child was allowed to break away from the job") + process.cleanup(1) + + def test_owner_abrupt_exit_leaves_no_descendant(self): + # The owner closes its final job handle on exit, terminating the child. + module_dir = str(Path(__file__).resolve().parent) + marker = self.directory/"descendant" + ready = self.directory/"ready" + child = ("import pathlib,time; pathlib.Path("+repr(str(ready))+").write_text('ready'); " + "time.sleep(1.5); pathlib.Path("+repr(str(marker))+").write_text('escaped')") + source = ("import os,sys,time,tempfile,pathlib; sys.path.insert(0,"+repr(module_dir)+"); " + "from process_containment import WindowsJobProcess; p=WindowsJobProcess(); " + "out=tempfile.TemporaryFile(); p.start([sys.executable,'-c',"+repr(child)+"],out,out," + +repr(str(self.directory))+"); ready=pathlib.Path("+repr(str(ready))+"); " + "exec('while not ready.exists(): time.sleep(.005)'); os._exit(17)") + result = subprocess.run([sys.executable, "-c", source], capture_output=True, timeout=4) + self.assertEqual(result.returncode, 17, result.stderr) + self.assertTrue(ready.exists()) + time.sleep(1.7) + self.assertFalse(marker.exists(), "job outlived its final owner handle") + + +if __name__ == "__main__": + required = "--require-windows" in sys.argv + if required: + sys.argv.remove("--require-windows") + if os.name != "nt": + raise SystemExit("Windows verification requires actual Windows; skipped tests cannot pass") + if unittest.defaultTestLoader.loadTestsFromTestCase(RealWindowsTests).countTestCases() < 6: + raise SystemExit("Windows containment fixtures are missing") + outcome = unittest.main(exit=False).result + raise SystemExit(0 if outcome.wasSuccessful() and not (required and outcome.skipped) else 1) diff --git a/test/optimize/validate.cpp b/test/optimize/validate.cpp new file mode 100644 index 0000000000..b5297d3542 --- /dev/null +++ b/test/optimize/validate.cpp @@ -0,0 +1,336 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace O = Gecode::Optimize; + +namespace { + +constexpr double infinity = std::numeric_limits::infinity(); +constexpr double nan = std::numeric_limits::quiet_NaN(); + +void require(bool condition, const std::string& message) { + if (!condition) + throw std::runtime_error(message); +} + +void near(double actual, double expected, const std::string& message) { + require(std::fabs(actual - expected) <= 1e-12, message); +} + +O::ModelSnapshot empty_model() { + O::ModelSnapshot model; + model.model_id = 42; + return model; +} + +O::Variable add_variable(O::ModelSnapshot& model, O::VariableType type, + double lower, double upper, bool active = true) { + O::VariableData data; + data.variable = {model.model_id, static_cast(model.variables.size())}; + data.type = type; + data.lower = lower; + data.upper = upper; + data.active = active; + model.variables.push_back(data); + return data.variable; +} + +void add_row(O::ModelSnapshot& model, std::vector terms, + double lower, double upper, bool active = true) { + O::RowData data; + data.constraint = {model.model_id, static_cast(model.rows.size())}; + data.terms = std::move(terms); + data.lower = lower; + data.upper = upper; + data.active = active; + model.rows.push_back(std::move(data)); +} + +O::ModelSnapshot production() { + auto model = empty_model(); + const auto open = add_variable(model, O::VariableType::Binary, 0.0, 1.0); + const auto quantity = add_variable(model, O::VariableType::Continuous, 0.0, 100.0); + add_row(model, {{open, -100.0}, {quantity, 1.0}}, -infinity, 0.0); + add_row(model, {{quantity, 1.0}}, 40.0, infinity); + model.objective.terms = {{open, 12.0}, {quantity, 0.5}}; + model.objective.offset = 7.0; + return model; +} + +void malformed(const O::ModelSnapshot& model, const std::string& description) { + bool threw = false; + try { + O::validate_structure(model); + } catch (const O::ModelError&) { + threw = true; + } + require(threw, "structure accepted " + description); + const auto report = O::validate(model, std::vector(model.variables.size(), 0.0)); + require(!report.valid && !report.model_valid && !report.message.empty() && !report.objective, + "malformed model did not return an invalid report: " + description); +} + +void analytic_checks() { + // The residual is one even when a platform rounds the row activity to 1e16. + // Check both range sides and the independent original indicator predicate. + for (double sign : {-1.0, 1.0}) { + O::Model cancellation; + auto x = cancellation.add_continuous(1, 1); + auto y = cancellation.add_continuous(1e6, 1e6); + auto active = cancellation.add_binary(); + auto row = cancellation.add_row({{x, sign}, {y, 1e10}}, 1e16, 1e16); + auto snapshot = cancellation.snapshot(); + auto failed = O::validate(snapshot, {1, 1e6, 1}, 0.9, 0); + require(!failed.valid, "small residual lost after large row cancellation"); + near(failed.max_row_violation, 1, "large row cancellation residual incorrect"); + require(O::validate(snapshot, {1, 1e6, 1}, 1, 0).valid, + "absolute tolerance mishandled after large row cancellation"); + cancellation.remove(row); + O::add_indicator(cancellation, active, true, {{x, sign}, {y, 1e10}}, 1e16, 1e16); + snapshot = cancellation.snapshot(); + std::vector values(snapshot.variables.size(), 0); + values[x.id] = 1; values[y.id] = 1e6; values[active.id] = 1; + failed = O::validate(snapshot, values, 0.9, 0); + require(!failed.valid, "small residual lost after large indicator cancellation"); + near(failed.max_indicator_violation, 1, "indicator cancellation residual incorrect"); + } + const auto model = production(); + O::validate_structure(model); + auto report = O::validate(model, {1.0, 40.0}); + require(report.valid && report.model_valid && report.objective, "feasible production rejected"); + near(*report.objective, 39.0, "objective offset not included"); + near(report.max_bound_violation, 0.0, "valid bounds have violation"); + near(report.max_row_violation, 0.0, "valid rows have violation"); + near(report.max_integrality_violation, 0.0, "valid integers have violation"); + + report = O::validate(model, {1.0, 39.0}); + require(!report.valid && report.model_valid, "demand violation accepted"); + near(report.max_row_violation, 1.0, "demand residual incorrect"); + report = O::validate(model, {0.5, 50.0}); + require(!report.valid, "fractional binary accepted"); + near(report.max_integrality_violation, 0.5, "integrality residual incorrect"); + report = O::validate(model, {1.25, 40.0}); + require(!report.valid, "binary bound violation accepted"); + near(report.max_bound_violation, 0.25, "binary bound residual incorrect"); + + auto negative = empty_model(); + const auto x = add_variable(negative, O::VariableType::Integer, -10.0, -2.0); + add_row(negative, {{x, -2.0}}, 6.0, 8.0); + negative.objective.terms = {{x, 3.0}}; + negative.objective.offset = -1.0; + negative.objective.sense = O::ObjectiveSense::Maximize; + report = O::validate(negative, {-3.0}); + require(report.valid && report.objective, "negative integer/ranged row rejected"); + near(*report.objective, -10.0, "maximize objective was wrongly negated"); + report = O::validate(negative, {-5.0}); + require(!report.valid, "upper ranged-row violation accepted"); + near(report.max_row_violation, 2.0, "upper ranged-row residual incorrect"); + + auto empty = empty_model(); + empty.objective.offset = 9.0; + report = O::validate(empty, {}); + require(report.valid && report.objective, "empty feasible model rejected"); + near(*report.objective, 9.0, "empty objective offset incorrect"); + add_row(empty, {}, 1.0, infinity); + O::validate_structure(empty); + report = O::validate(empty, {}); + require(!report.valid && report.model_valid, "constant infeasible row accepted"); + near(report.max_row_violation, 1.0, "constant infeasible row residual incorrect"); + + auto no_integer = empty_model(); + add_variable(no_integer, O::VariableType::Integer, 0.2, 0.8); + O::validate_structure(no_integer); + report = O::validate(no_integer, {0.5}); + require(report.model_valid && !report.valid, "empty integer interval is not treated as infeasible"); +} + +void semi_checks() { + auto model = empty_model(); + add_variable(model, O::VariableType::SemiContinuous, 2.0, 5.0); + add_variable(model, O::VariableType::SemiInteger, 3.0, 7.0); + require(O::validate(model, {0.0, 0.0}).valid, "zero semi-variable alternatives rejected"); + require(O::validate(model, {2.25, 4.0}).valid, "nonzero semi-variable alternatives rejected"); + auto report = O::validate(model, {1.0, 2.0}); + require(!report.valid, "semi-variable holes accepted"); + near(report.max_bound_violation, 1.0, "semi-variable distance incorrect"); + report = O::validate(model, {2.25, 3.5}); + require(!report.valid, "fractional semi-integer accepted"); + near(report.max_integrality_violation, 0.5, "semi-integer residual incorrect"); + report = O::validate(model, {-0.25, 0.0}); + require(!report.valid, "negative semi-continuous accepted"); + near(report.max_bound_violation, 0.25, "semi-variable zero distance incorrect"); + require(O::validate(model, {-5e-8, 0.0}, 1e-7, 0.0).valid, + "semi-continuous zero tolerance rejected"); + require(!O::validate(model, {0.0, 5e-8}, 1e-7, 0.0).valid, + "semi-integer zero bypassed integrality tolerance"); + model.variables[0].upper = infinity; + require(O::validate(model, {1e100, 0.0}).valid, + "abstract semi-variable infinite upper bound rejected"); + + auto semi_empty = empty_model(); + add_variable(semi_empty, O::VariableType::SemiInteger, 0.2, 0.8); + require(O::validate(semi_empty, {0.0}).valid, + "semi-integer with empty positive interval lost zero alternative"); +} + +void malformed_checks() { + using Mutation = std::function; + const std::vector> cases = { + {"zero model ID", [](auto& m) { m.model_id = 0; }}, + {"variable wrong owner", [](auto& m) { m.variables[0].variable.model_id = 999; }}, + {"variable wrong slot", [](auto& m) { m.variables[0].variable.id = 1; }}, + {"row wrong owner", [](auto& m) { m.rows[0].constraint.model_id = 999; }}, + {"row wrong slot", [](auto& m) { m.rows[0].constraint.id = 1; }}, + {"variable NaN lower", [](auto& m) { m.variables[1].lower = nan; }}, + {"variable NaN upper", [](auto& m) { m.variables[1].upper = nan; }}, + {"variable positive infinite lower", [](auto& m) { m.variables[1].lower = infinity; }}, + {"variable negative infinite upper", [](auto& m) { m.variables[1].upper = -infinity; }}, + {"reversed variable bounds", [](auto& m) { m.variables[1].lower = 101.0; }}, + {"binary lower below zero", [](auto& m) { m.variables[0].lower = -1.0; }}, + {"binary upper above one", [](auto& m) { m.variables[0].upper = 2.0; }}, + {"row NaN lower", [](auto& m) { m.rows[0].lower = nan; }}, + {"row NaN upper", [](auto& m) { m.rows[0].upper = nan; }}, + {"row positive infinite lower", [](auto& m) { m.rows[0].lower = infinity; }}, + {"row negative infinite upper", [](auto& m) { m.rows[0].upper = -infinity; }}, + {"reversed row bounds", [](auto& m) { m.rows[0].lower = 1.0; }}, + {"negative variable type", [](auto& m) { m.variables[0].type = static_cast(-1); }}, + {"unknown variable type", [](auto& m) { m.variables[0].type = static_cast(99); }}, + {"negative objective sense", [](auto& m) { m.objective.sense = static_cast(-1); }}, + {"unknown objective sense", [](auto& m) { m.objective.sense = static_cast(99); }}, + {"NaN offset", [](auto& m) { m.objective.offset = nan; }}, + {"infinite offset", [](auto& m) { m.objective.offset = infinity; }}, + {"row wrong term owner", [](auto& m) { m.rows[0].terms[0].variable.model_id = 999; }}, + {"row dangling term", [](auto& m) { m.rows[0].terms[1].variable.id = 99; }}, + {"objective wrong term owner", [](auto& m) { m.objective.terms[0].variable.model_id = 999; }}, + {"objective dangling term", [](auto& m) { m.objective.terms[1].variable.id = 99; }}, + {"row NaN coefficient", [](auto& m) { m.rows[0].terms[0].coefficient = nan; }}, + {"row infinite coefficient", [](auto& m) { m.rows[0].terms[0].coefficient = infinity; }}, + {"objective NaN coefficient", [](auto& m) { m.objective.terms[0].coefficient = nan; }}, + {"objective infinite coefficient", [](auto& m) { m.objective.terms[0].coefficient = infinity; }}, + {"row zero coefficient", [](auto& m) { m.rows[0].terms[0].coefficient = 0.0; }}, + {"objective zero coefficient", [](auto& m) { m.objective.terms[0].coefficient = -0.0; }}, + {"duplicate row terms", [](auto& m) { m.rows[0].terms.push_back(m.rows[0].terms.back()); }}, + {"duplicate objective terms", [](auto& m) { m.objective.terms.push_back(m.objective.terms.back()); }}, + {"unsorted row terms", [](auto& m) { std::swap(m.rows[0].terms[0], m.rows[0].terms[1]); }}, + {"unsorted objective terms", [](auto& m) { std::swap(m.objective.terms[0], m.objective.terms[1]); }}, + {"active deleted-variable reference", [](auto& m) { m.variables[0].active = false; }} + }; + for (const auto& test : cases) { + auto model = production(); + test.second(model); + malformed(model, test.first); + } + for (const double lower : {0.0, -1.0, -infinity, infinity}) { + auto model = empty_model(); + add_variable(model, O::VariableType::SemiContinuous, lower, infinity); + malformed(model, "unsupported semi-variable lower bound"); + } +} + +void assignment_and_tolerance_checks() { + const auto model = production(); + for (const auto& values : std::vector>{ + {}, {1.0}, {1.0, 40.0, 0.0}, {nan, 40.0}, {1.0, infinity}, {1.0, -infinity}}) { + const auto report = O::validate(model, values); + require(!report.valid && report.model_valid && !report.objective, + "invalid assignment accepted or mislabeled as malformed model"); + } + for (const double tolerance : {-1.0, infinity, -infinity, nan}) { + auto report = O::validate(model, {1.0, 40.0}, tolerance, 0.0); + require(!report.valid && report.model_valid, "invalid feasibility tolerance accepted"); + report = O::validate(model, {1.0, 40.0}, 0.0, tolerance); + require(!report.valid && report.model_valid, "invalid integrality tolerance accepted"); + } + require(O::validate(model, {1.0 + 5e-8, 40.0}, 1e-7, 1e-6).valid, + "absolute bound/integrality tolerance rejected"); + require(!O::validate(model, {1.0 + 5e-8, 40.0}, 0.0, 1e-6).valid, + "zero feasibility tolerance ignored"); + auto scaled = empty_model(); + const auto x = add_variable(scaled, O::VariableType::Continuous, -infinity, infinity); + add_row(scaled, {{x, 1e9}}, -infinity, 1e9); + const auto report = O::validate(scaled, {1.0 + 1e-8}, 1e-7, 1e-6); + require(!report.valid && report.max_row_violation > 9.0, + "row tolerance was silently normalized by scale"); +} + +void deleted_slot_checks() { + auto model = production(); + const auto removed = add_variable(model, O::VariableType::Continuous, -infinity, infinity, false); + add_row(model, {{removed, 1.0}}, 99.0, 99.0, false); + auto report = O::validate(model, {1.0, 40.0, nan}); + require(report.valid && report.objective, "unreferenced deleted NaN slot was evaluated"); + near(*report.objective, 39.0, "deleted row changed objective"); + require(!O::validate(model, {1.0, 40.0}).valid, "deleted slot omitted from assignment dimension"); + auto altered = model; + altered.rows.back().active = true; + malformed(altered, "reactivated row referencing deleted slot"); + altered = model; + altered.objective.terms.push_back({removed, 1.0}); + malformed(altered, "objective references deleted slot"); + altered = model; + altered.variables.back().variable.model_id = 999; + malformed(altered, "deleted slot wrong owner"); + altered = model; + altered.rows.back().constraint.id = 999; + malformed(altered, "deleted row wrong slot"); +} + +void accumulation_checks() { + auto model = empty_model(); + const auto x = add_variable(model, O::VariableType::Continuous, -infinity, infinity); + model.objective.terms = {{x, std::numeric_limits::max()}}; + model.objective.offset = std::numeric_limits::max(); + auto report = O::validate(model, {1.0}); + require(!report.valid && report.model_valid && !report.objective, + "objective offset accumulation overflow accepted"); + model.objective.offset = 0.0; + report = O::validate(model, {std::numeric_limits::max()}); + require(!report.valid && !report.objective, "objective product overflow accepted"); + model.objective.terms.clear(); + add_row(model, {{x, std::numeric_limits::max()}}, -infinity, 1.0); + report = O::validate(model, {std::numeric_limits::max()}); + require(!report.valid, "large row activity accepted"); + + // Many large positive terms, a small term, then many large negative terms + // exercise cancellation independently of sparse coalescing in Model. + auto cancellation = empty_model(); + std::vector terms; + constexpr std::size_t side = 1024; + for (std::size_t i = 0; i < 2 * side + 1; ++i) { + const auto variable = add_variable(cancellation, O::VariableType::Continuous, 1.0, 1.0); + terms.push_back({variable, i < side ? 1e16 : (i == side ? 1.0 : -1e16)}); + } + add_row(cancellation, terms, 1.0, 1.0); + cancellation.objective.terms = terms; + cancellation.objective.offset = 11.0; + report = O::validate(cancellation, std::vector(terms.size(), 1.0), 0.0, 0.0); + require(report.valid && report.objective, "compensated large-term cancellation failed"); + near(*report.objective, 12.0, "compensated objective incorrect"); +} + +} + +int main() { + try { + analytic_checks(); + semi_checks(); + malformed_checks(); + assignment_and_tolerance_checks(); + deleted_slot_checks(); + accumulation_checks(); + std::cout << "PASS independent original-model validation\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "FAIL: " << error.what() << '\n'; + return 1; + } +} diff --git a/test/optimize/workflow.cpp b/test/optimize/workflow.cpp new file mode 100644 index 0000000000..8871dbbe22 --- /dev/null +++ b/test/optimize/workflow.cpp @@ -0,0 +1,434 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace Gecode::Optimize; + +#ifdef GECODE_WORKFLOW_TEST_FAKE_SOLVER + +namespace { +enum class Scenario { Incomplete, CancelDuringSolve, BadLock, MissingBound, FalseInfeasible, + CancellationResidual, ForgedCancellationObjective, ForgedCancellationGap }; +Scenario scenario = Scenario::Incomplete; +unsigned calls = 0; +double previous_remaining = std::numeric_limits::infinity(); +} + +// Compile this test mode with model/result/validate/workflow.cpp, without the +// real solve.cpp. Deterministic stops exercise the coordinator without sleeping +// or relying on solver/runtime speed. It also distrusts a malformed candidate. +namespace Gecode { namespace Optimize { +SolveResult solve(const ModelSnapshot& model, const SolveOptions& options) { + assert(options.relative_gap == 0 && options.absolute_gap == 0); + assert(options.time_limit_seconds <= previous_remaining); + previous_remaining = options.time_limit_seconds; + if (calls != 0) { + assert(options.primal_start.empty()); + assert(model.rows.size() == calls); // one independently checked prior lock + } + if (scenario == Scenario::CancellationResidual || + scenario == Scenario::ForgedCancellationObjective || + scenario == Scenario::ForgedCancellationGap) { + assert(calls == 0 && model.variables.size() == 2); + SolveResult result; + result.model_id = model.model_id; result.revision = model.revision; + result.values = {1, 1}; result.active_variables = {true, true}; + const auto checked = validate(model, result.values); + // This fixture's exact objective is 1 + big - big = 1. Its + // coefficient/offset scale exceeds even an x87 long-double significand. + assert(checked.valid && checked.objective == 1); + result.solution_validated = true; + result.objective = 1; result.best_bound = 1; + result.update_gaps(model.objective.sense); + result.termination = Termination::Optimal; + if (scenario == Scenario::ForgedCancellationObjective) result.objective = 0; + if (scenario != Scenario::CancellationResidual) result.best_bound = 0; + // The forged cases deliberately retain the prior cached zero gap. + ++calls; + return result; + } + SolveResult result; + result.model_id = model.model_id; + result.revision = model.revision; + result.backend = "deterministic test backend"; + result.values = {calls == 0 ? 0.0 : 1.0}; + if (scenario != Scenario::BadLock) result.values[0] = 0.0; + for (const auto& variable : model.variables) result.active_variables.push_back(variable.active); + const auto report = validate(model, result.values); + result.solution_validated = scenario == Scenario::BadLock || report.valid; + result.objective = calls == 0 ? 0.0 : result.values[0]; + result.best_bound = result.objective; + result.update_gaps(model.objective.sense); + result.termination = Termination::Optimal; + if (scenario == Scenario::MissingBound) { + result.best_bound.reset(); + result.absolute_gap.reset(); + result.relative_gap.reset(); + } + if (scenario == Scenario::CancelDuringSolve) options.cancellation->cancel(); + if (calls == 1 && scenario == Scenario::Incomplete) + result.termination = Termination::TimeLimit; + if (calls == 1 && scenario == Scenario::FalseInfeasible) { + result.termination = Termination::Infeasible; + result.solution_validated = false; + result.objective.reset(); + result.values.clear(); + } + ++calls; + return result; +} +}} + +int main() { + Model model; + auto x = model.add_binary(); + LexicographicObjective first; + first.objective.terms = {{x, 1}}; + auto second = first; + second.objective.sense = ObjectiveSense::Maximize; + const std::vector objectives{first, second, first}; + for (Scenario selected : {Scenario::Incomplete, Scenario::CancelDuringSolve, + Scenario::BadLock, Scenario::MissingBound, Scenario::FalseInfeasible}) { + scenario = selected; + calls = 0; + previous_remaining = std::numeric_limits::infinity(); + SolveOptions options; + options.time_limit_seconds = 60; + options.primal_start = {{x, 1}}; + auto result = solve_lexicographic(model, objectives, options); + assert(!result.completed_numerically()); + if (selected == Scenario::CancelDuringSolve) { + assert(calls == 1 && result.termination == Termination::Cancelled); + assert(!result.has_solution() && result.completed_stages == 0); + assert(!result.stages[0].result.has_solution()); + } else if (selected == Scenario::MissingBound) { + assert(calls == 1 && result.termination == Termination::NumericalFailure); + assert(result.has_solution() && result.completed_stages == 0); + } else { + assert(calls == 2 && result.completed_stages == 1); + assert(result.stages.size() == 2 && !result.stages.back().completed); + assert(result.has_solution() && result.final_solution.value(x) == 0); + assert(result.termination == (selected == Scenario::Incomplete + ? Termination::TimeLimit : Termination::NumericalFailure)); + } + } + for (auto selected : {Scenario::CancellationResidual, + Scenario::ForgedCancellationObjective, Scenario::ForgedCancellationGap}) { + scenario = selected; calls = 0; + previous_remaining = std::numeric_limits::infinity(); + Model cancellation; + auto residual = cancellation.add_continuous(1, 1); + auto fixed = cancellation.add_continuous(1, 1); + const double big = std::ldexp(1.0, std::numeric_limits::digits + 2); + assert(std::isfinite(big)); + cancellation.minimize({{residual, 1}, {fixed, big}}, -big); + LexicographicObjective objective; + objective.objective = cancellation.snapshot().objective; + auto result = solve_lexicographic(cancellation, {objective}); + assert(calls == 1 && result.has_solution()); + assert(result.final_solution.objective == 1); + assert(result.objective_values == std::vector{1}); + if (selected == Scenario::CancellationResidual) { + assert(result.completed_numerically() && result.completed_stages == 1); + } else { + assert(result.termination == Termination::NumericalFailure); + assert(!result.completed_numerically() && result.completed_stages == 0); + } + } +} + +#else + +namespace { +const double infinity = std::numeric_limits::infinity(); + +LexicographicObjective objective(Variable variable, ObjectiveSense sense, + double offset = 0.0) { + LexicographicObjective objective; + objective.objective = {{{variable, 1.0}}, offset, sense}; + return objective; +} + +double value(const ObjectiveData& objective, const std::vector& point) { + double total = objective.offset; + for (const auto& term : objective.terms) + total += term.coefficient * point[term.variable.id]; + return total; +} + +struct Oracle { + std::vector stage_optima; + std::vector> final_points; +}; + +// Independent exhaustive arithmetic oracle: input points are generated by +// explicit test-case inequalities, not by the solver or its model validator. +Oracle enumerate(std::vector> points, + const std::vector& objectives) { + Oracle oracle; + for (const auto& descriptor : objectives) { + const auto& objective = descriptor.objective; + double best = objective.sense == ObjectiveSense::Minimize ? infinity : -infinity; + for (const auto& point : points) { + const auto candidate = value(objective, point); + best = objective.sense == ObjectiveSense::Minimize + ? std::min(best, candidate) : std::max(best, candidate); + } + assert(std::isfinite(best)); + oracle.stage_optima.push_back(best); + const auto degradation = descriptor.absolute_degradation + + descriptor.relative_degradation * std::fabs(best); + points.erase(std::remove_if(points.begin(), points.end(), [&](const auto& point) { + const auto candidate = value(objective, point); + return objective.sense == ObjectiveSense::Minimize + ? candidate > best + degradation : candidate < best - degradation; + }), points.end()); + } + oracle.final_points = std::move(points); + return oracle; +} + +void verify(const LexicographicResult& result, const Oracle& oracle, + const std::vector& objectives) { + assert(result.completed_numerically()); + assert(result.termination == Termination::Optimal); + assert(result.guarantee == Guarantee::Numerical); + assert(result.has_solution()); + assert(result.completed_stages == objectives.size()); + assert(result.stages.size() == objectives.size()); + for (std::size_t i = 0; i < objectives.size(); ++i) { + assert(result.stages[i].completed); + assert(result.stages[i].index == i); + assert(result.stages[i].result.termination == Termination::Optimal); + assert(result.stages[i].result.objective == oracle.stage_optima[i]); + assert(result.objective_values[i] == value(objectives[i].objective, + result.final_solution.values)); + } + assert(std::find(oracle.final_points.begin(), oracle.final_points.end(), + result.final_solution.values) != oracle.final_points.end()); + assert(result.final_solution.termination == Termination::Unknown); + assert(!result.final_solution.best_bound && !result.final_solution.absolute_gap && + !result.final_solution.relative_gap && !result.final_solution.native_backend_gap); +} + +void discrete_oracle_tests() { + Model model; + auto x = model.add_integer(0, 4, "x"); + auto y = model.add_integer(0, 4, "y"); + auto demand = model.add_row({{x, 1}, {y, 1}}, 2, infinity, "demand"); + model.maximize({{x, 3}, {y, 2}}, 7); + const auto before = model.snapshot(); + std::vector> points; + for (int a = 0; a <= 4; ++a) + for (int b = 0; b <= 4; ++b) + if (a + b >= 2) points.push_back({static_cast(a), static_cast(b)}); + std::vector objectives{ + objective(x, ObjectiveSense::Minimize), objective(y, ObjectiveSense::Minimize)}; + objectives[0].name = "highest priority"; + auto result = solve_lexicographic(model, objectives); + verify(result, enumerate(points, objectives), objectives); + assert(result.final_solution.value(x) == 0 && result.final_solution.value(y) == 2); + assert(result.final_solution.objective == 11); // ORIGINAL scalar objective + assert(result.stages[0].name == "highest priority"); + assert(result.stages[0].retention_bound == 0); + assert(!result.stages[1].retention_bound); + assert(result.model_id == before.model_id && result.revision == before.revision); + assert(model.revision() == before.revision); + assert(model.snapshot().rows.size() == before.rows.size()); + assert(model.row(demand).lower == 2 && model.row(demand).upper == infinity); + assert(model.snapshot().objective.offset == 7); + assert(model.snapshot().objective.sense == ObjectiveSense::Maximize); + assert(model.snapshot().objective.terms[0].coefficient == 3); + assert(model.snapshot().objective.terms[1].coefficient == 2); + + SolveOptions started; + started.primal_start = {{x, 4}, {y, 4}}; // feasible initially, violates first x<=0 lock + auto with_start = solve_lexicographic(model, objectives, started); + verify(with_start, enumerate(points, objectives), objectives); + assert(with_start.stages[0].result.start_submitted); + assert(!with_start.stages[1].result.start_submitted); + + std::reverse(objectives.begin(), objectives.end()); // priority is input order + result = solve_lexicographic(before, objectives); + verify(result, enumerate(points, objectives), objectives); + assert(result.final_solution.value(x) == 2 && result.final_solution.value(y) == 0); + + objectives = {objective(x, ObjectiveSense::Minimize), objective(y, ObjectiveSense::Minimize)}; + objectives[0].absolute_degradation = 1; + result = solve_lexicographic(model, objectives); + verify(result, enumerate(points, objectives), objectives); + assert(result.final_solution.value(x) == 1 && result.final_solution.value(y) == 1); + + objectives[0] = objective(x, ObjectiveSense::Minimize, 2); + objectives[0].relative_degradation = 0.5; // f*=2, allowance=1, hence x<=1 + result = solve_lexicographic(model, objectives); + verify(result, enumerate(points, objectives), objectives); + assert(result.stages[0].retention_bound == 3); + assert(result.final_solution.value(x) == 1 && result.final_solution.value(y) == 1); + + model.set_bounds(demand, -infinity, 5); + points.clear(); + for (int a = 0; a <= 4; ++a) + for (int b = 0; b <= 4; ++b) + if (a + b <= 5) points.push_back({static_cast(a), static_cast(b)}); + objectives = {objective(x, ObjectiveSense::Maximize, -10), + objective(y, ObjectiveSense::Maximize, 3)}; + objectives[0].absolute_degradation = 1; + result = solve_lexicographic(model, objectives); + verify(result, enumerate(points, objectives), objectives); + assert(result.stages[0].retention_bound == -7); + assert(result.final_solution.value(x) == 3 && result.final_solution.value(y) == 2); + + objectives[1].objective.sense = ObjectiveSense::Minimize; + result = solve_lexicographic(model, objectives); + verify(result, enumerate(points, objectives), objectives); +} + +void numerical_and_empty_tests() { + Model offset_model; + auto x = offset_model.add_integer(1, 4); + auto result = solve_lexicographic(offset_model, + {objective(x, ObjectiveSense::Minimize, 1e16), objective(x, ObjectiveSense::Maximize)}); + assert(result.completed_numerically()); + assert(result.final_solution.value(x) == 1); // do not lock x <= round(1e16+1)-1e16 + + // A large linear subtotal and offset cancel to an exact small residual. + // An extreme-scale backend bound can remain unresolved; the workflow must + // still report the independently computed objective vector faithfully. + Model cancellation; + auto residual = cancellation.add_continuous(1, 1); + auto fixed = cancellation.add_continuous(1e6, 1e6); + cancellation.minimize({{residual, 1}, {fixed, 1e10}}, -1e16); + LexicographicObjective cancellation_objective; + cancellation_objective.objective = cancellation.snapshot().objective; + result = solve_lexicographic(cancellation, {cancellation_objective}); + assert(result.has_solution() && result.final_solution.objective == 1); + assert(result.objective_values == std::vector{1}); + assert(result.termination == Termination::Optimal || + result.termination == Termination::NumericalFailure); + if (result.completed_numerically()) { + assert(result.stages[0].result.objective == 1); + assert(result.stages[0].result.best_bound && + std::fabs(*result.stages[0].result.best_bound - 1) <= 1e-7); + } + + Model continuous; + auto a = continuous.add_continuous(0, 4); + auto b = continuous.add_continuous(0, 4); + continuous.add_row({{a, 1}, {b, 1}}, 2, infinity); + result = solve_lexicographic(continuous, + {objective(a, ObjectiveSense::Minimize), objective(b, ObjectiveSense::Maximize)}); + assert(result.completed_numerically()); + assert(result.final_solution.value(a) == 0 && result.final_solution.value(b) == 4); + + Model empty; + LexicographicObjective constant; + constant.objective.offset = 3; + LexicographicObjective other; + other.objective.offset = -2; + result = solve_lexicographic(empty, {constant, other}); + assert(result.completed_numerically() && result.has_solution()); + assert(result.objective_values == std::vector({3, -2})); + assert(result.final_solution.values.empty()); + + Model tombstones; + auto removed = tombstones.add_binary(); + tombstones.remove(removed); + result = solve_lexicographic(tombstones, {constant, other}); + assert(result.completed_numerically() && result.has_solution()); + assert(result.final_solution.active_variables == std::vector({false})); +} + +void status_tests() { + Model model; + auto x = model.add_integer(0, 4); + std::vector objectives{ + objective(x, ObjectiveSense::Minimize), objective(x, ObjectiveSense::Maximize)}; + SolveOptions options; + options.time_limit_seconds = 0; + auto result = solve_lexicographic(model, objectives, options); + assert(result.termination == Termination::TimeLimit && !result.has_solution()); + assert(result.stages.empty() && result.completed_stages == 0); + options = {}; + options.cancellation = std::make_shared(); + options.cancellation->cancel(); + result = solve_lexicographic(model, objectives, options); + assert(result.termination == Termination::Cancelled && !result.completed_numerically()); + options = {}; + options.node_limit = 0; + result = solve_lexicographic(model, objectives, options); + assert(result.termination == Termination::Unsupported && result.stages.empty()); + result = solve_lexicographic(model, {objectives[0]}, options); + assert(result.termination == Termination::NodeLimit && result.completed_stages == 0); + options = {}; + options.guarantee = Guarantee::Exact; + assert(solve_lexicographic(model, objectives, options).termination == Termination::Unsupported); + options.guarantee = Guarantee::Certified; + assert(solve_lexicographic(model, objectives, options).termination == Termination::Unsupported); + + options = {}; + options.relative_gap = -1; + assert(solve_lexicographic(model, objectives, options).termination == Termination::InvalidModel); + assert(solve_lexicographic(model, {}).termination == Termination::InvalidModel); + auto invalid = objectives; + invalid[1].absolute_degradation = -1; + result = solve_lexicographic(model, invalid); + assert(result.termination == Termination::InvalidModel && result.stages.empty()); + invalid = objectives; + invalid[1].relative_degradation = infinity; + assert(solve_lexicographic(model, invalid).termination == Termination::InvalidModel); + invalid = objectives; + invalid[0].objective.terms[0].variable.model_id += 100; + assert(solve_lexicographic(model, invalid).termination == Termination::InvalidModel); + + options = {}; + options.relative_gap = 1.0; + options.absolute_gap = 100.0; // workflow requests zero gaps for each phase + if (capabilities().available) + assert(solve_lexicographic(model, objectives, options).completed_numerically()); + + if (!capabilities().available) { + assert(solve_lexicographic(model, objectives).termination == Termination::Unsupported); + return; + } + Model infeasible; + auto impossible = infeasible.add_binary(); + infeasible.add_row({{impossible, 1}}, 2, infinity); + result = solve_lexicographic(infeasible, + {objective(impossible, ObjectiveSense::Minimize), objective(impossible, ObjectiveSense::Maximize)}); + assert(result.termination == Termination::Infeasible); + assert(result.stages.size() == 1 && result.completed_stages == 0 && !result.has_solution()); + + Model unbounded; + auto bounded = unbounded.add_continuous(0, 1); + auto free = unbounded.add_continuous(-infinity, infinity); + result = solve_lexicographic(unbounded, + {objective(bounded, ObjectiveSense::Minimize), objective(free, ObjectiveSense::Maximize)}); + assert(result.termination == Termination::Unbounded); + assert(result.stages.size() == 2 && result.completed_stages == 1); + assert(!result.completed_numerically()); + + auto overflow = objectives; + overflow[0].objective.offset = 2; + overflow[0].relative_degradation = std::numeric_limits::max(); + result = solve_lexicographic(model, overflow); + assert(result.termination == Termination::Unsupported && result.stages.size() == 1); + assert(!result.completed_numerically()); +} +} + +int main() { + status_tests(); + if (capabilities().available) { + discrete_oracle_tests(); + numerical_and_empty_tests(); + } +} + +#endif diff --git a/tools/flatzinc/configure-optimize-msc.cmake b/tools/flatzinc/configure-optimize-msc.cmake new file mode 100644 index 0000000000..ace412f286 --- /dev/null +++ b/tools/flatzinc/configure-optimize-msc.cmake @@ -0,0 +1,44 @@ +# Pure post-build/install JSON configuration, after target-file expansion. +# cmake -DTEMPLATE=... -DVERSION=... -DDRIVER=... -DMZNLIB=... -DOUTPUT=... -P this-file +cmake_minimum_required(VERSION 3.16) + +foreach(required TEMPLATE VERSION DRIVER MZNLIB OUTPUT) + if(NOT DEFINED ${required} OR "${${required}}" STREQUAL "") + message(FATAL_ERROR "Required input ${required} is missing or empty") + endif() +endforeach() +if(NOT EXISTS "${TEMPLATE}" OR IS_DIRECTORY "${TEMPLATE}") + message(FATAL_ERROR "TEMPLATE is not an existing file") +endif() + +# JSON strings require escaping all U+0001..U+001F controls, not just newlines. +# NUL cannot be supplied in an OS command-line argument/CMake string. UTF-8 +# characters otherwise remain intact. Do not normalize relative path spelling. +function(json_string input output) + string(REPLACE "\\" "\\\\" encoded "${input}") + string(REPLACE "\"" "\\\"" encoded "${encoded}") + set(hex "0123456789abcdef") + foreach(code RANGE 1 31) + string(ASCII ${code} control) + math(EXPR high "${code} / 16") + math(EXPR low "${code} % 16") + string(SUBSTRING "${hex}" ${high} 1 high_hex) + string(SUBSTRING "${hex}" ${low} 1 low_hex) + string(REPLACE "${control}" "\\u00${high_hex}${low_hex}" encoded "${encoded}") + endforeach() + set(${output} "${encoded}" PARENT_SCOPE) +endfunction() + +json_string("${VERSION}" GECODE_VERSION) +json_string("${DRIVER}" GECODE_OPTIMIZE_MSC_EXECUTABLE) +json_string("${MZNLIB}" GECODE_OPTIMIZE_MSC_MZNLIB) +get_filename_component(output_directory "${OUTPUT}" DIRECTORY) +if(NOT "${output_directory}" STREQUAL "") + file(MAKE_DIRECTORY "${output_directory}") +endif() +# configure_file substitutes the original template once; placeholder-like text +# inside values stays literal, unlike a sequence of string(REPLACE) operations. +string(RANDOM LENGTH 16 ALPHABET 0123456789abcdef nonce) +set(temporary "${OUTPUT}.${nonce}.tmp") +configure_file("${TEMPLATE}" "${temporary}" @ONLY) +file(RENAME "${temporary}" "${OUTPUT}") diff --git a/tools/flatzinc/fzn-gecode-optimize.cpp b/tools/flatzinc/fzn-gecode-optimize.cpp new file mode 100644 index 0000000000..23b8a6ecc5 --- /dev/null +++ b/tools/flatzinc/fzn-gecode-optimize.cpp @@ -0,0 +1,422 @@ +/* Explicit, bounded FlatZinc capture/compiler frontend. Legacy driver is separate. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace FznOptimizeDriver { +namespace O=Gecode::Optimize; +namespace F=Gecode::FlatZinc::Capture; +using Clock=std::chrono::steady_clock; +struct Options { + std::string filename; + bool minizinc=false; + F::Options capture; + O::SolveOptions solve; + std::string native_mode="auto",lp="off",search="dfs",branching="default",neighborhood="off"; + O::NativeAutoSettings automatic; + O::NativeRaceOptions race; + O::NativeLpSettings relaxation; + O::NativeBranchingSettings branching_settings; + O::NativeNeighborhoodSettings neighborhood_settings; + std::size_t max_open_nodes=100000; + bool root_cuts=false,diagnostics=false; + Options() { + solve.backend=O::Backend::Native;solve.guarantee=O::Guarantee::Exact; + solve.relative_gap=0;solve.absolute_gap=0; + } +}; +const char* usage() { + return "Usage: fzn-gecode-optimize MODEL.fzn|- [--backend native|highs] " + "[--time-limit SECONDS] [--node-limit N] [--max-input-bytes N]\n" + "One satisfaction solution or final optimization; bounded integer/Boolean subset.\n" + "Native uses exact integer search; HiGHS is explicitly numerical.\n" + "MiniZinc protocol: --minizinc [-t MILLISECONDS] [NATIVE OPTIONS] MODEL.fzn|-\n" + "Native options (all take a value):\n" + " --native-mode auto|race|plain|configured (default auto)\n" + " --native-auto-presolve/--native-auto-components/--native-auto-symmetry/--native-auto-knapsack on|off\n" + " --native-race-seconds SECONDS --native-race-nodes N (race only)\n" + " --native-lp off|root|updated --native-root-cuts on|off\n" + " --native-bound-tightening on|off --native-lp-interval N\n" + " --native-search bab|dfs|best-bound --native-max-open-nodes N\n" + " --native-branching default|reliability --native-branching-probes N\n" + " --native-neighborhood off|hamming --native-neighborhood-radius N\n" + " --native-neighborhood-nodes N --native-neighborhood-seconds SECONDS\n" + " LP/search/neighborhood controls require configured mode.\n" + " --native-node-limit N --native-diagnostics on|off\n" + "Racing uses sequential probes and restarts: it can increase total CPU and solve time.\n"; +} +std::uint64_t count(const std::string& text) { + if(text.empty())throw std::invalid_argument("Empty count"); + std::uint64_t value=0; + for(unsigned char c:text) { + if(c<'0'||c>'9'||value>(std::numeric_limits::max()-(c-'0'))/10) + throw std::invalid_argument("Count must be an unsigned decimal integer in range"); + value=value*10+(c-'0'); + } + return value; +} +double seconds(const std::string& text) { + std::istringstream input(text);input.imbue(std::locale::classic());double value; + input>>std::noskipws>>value; + if(input.fail()||!input.eof()||!std::isfinite(value)||value<0) + throw std::invalid_argument("Time limit must be finite and nonnegative"); + return value; +} +bool on_off(const std::string& value) { + if(value=="on")return true;if(value=="off")return false; + throw std::invalid_argument("Boolean native options must be on or off"); +} +std::size_t size_count(const std::string& value) { + const auto n=count(value); + if(n>std::numeric_limits::max())throw std::invalid_argument("Count exceeds size_t range"); + return static_cast(n); +} +std::string choice(const std::string& key,const std::string& value,std::initializer_list choices) { + for(const auto* accepted:choices)if(value==accepted)return value; + throw std::invalid_argument("Invalid value for "+key+": "+value); +} +bool native_argument(Options& o,const std::string& key,const std::string& value) { + if(key=="--native-mode")o.native_mode=choice(key,value,{"auto","race","plain","configured"}); + else if(key=="--native-auto-presolve")o.automatic.presolve=on_off(value); + else if(key=="--native-auto-components")o.automatic.components=on_off(value); + else if(key=="--native-auto-symmetry")o.automatic.symmetry=on_off(value); + else if(key=="--native-auto-knapsack")o.automatic.knapsack=on_off(value); + else if(key=="--native-race-seconds")o.race.exploration_seconds=seconds(value); + else if(key=="--native-race-nodes") { + o.race.probe_node_limit=count(value); + if(!o.race.probe_node_limit)throw std::invalid_argument("Native race node budget must be positive"); + } else if(key=="--native-lp")o.lp=choice(key,value,{"off","root","updated"}); + else if(key=="--native-root-cuts")o.root_cuts=on_off(value); + else if(key=="--native-bound-tightening")o.relaxation.bound_tightening=on_off(value); + else if(key=="--native-lp-interval") { + const auto n=count(value); + if(!n||n>std::numeric_limits::max())throw std::invalid_argument("LP interval must be a positive unsigned integer in range"); + o.relaxation.bound_change_interval=static_cast(n); + } else if(key=="--native-search")o.search=choice(key,value,{"bab","dfs","best-bound"}); + else if(key=="--native-branching")o.branching=choice(key,value,{"default","reliability"}); + else if(key=="--native-branching-probes")o.branching_settings.max_probe_status_calls=count(value); + else if(key=="--native-max-open-nodes")o.max_open_nodes=size_count(value); + else if(key=="--native-neighborhood")o.neighborhood=choice(key,value,{"off","hamming"}); + else if(key=="--native-neighborhood-radius")o.neighborhood_settings.radius=size_count(value); + else if(key=="--native-neighborhood-nodes")o.neighborhood_settings.max_status_calls=count(value); + else if(key=="--native-neighborhood-seconds")o.neighborhood_settings.time_limit_seconds=seconds(value); + else if(key=="--native-node-limit")o.solve.node_limit=count(value); + else if(key=="--native-diagnostics")o.diagnostics=on_off(value); + else return false; + return true; +} +O::NativeSearchOptions search_options(const Options& o,const O::SolveOptions& solve) { + O::NativeSearchOptions result;result.solve=solve; + result.order=o.search=="best-bound"?O::NativeSearchOrder::BestBound:O::NativeSearchOrder::DepthFirst; + result.max_open_nodes=o.max_open_nodes; + if(o.lp!="off")result.relaxation=o.relaxation; + if(o.branching=="reliability")result.branching=o.branching_settings; + return result; +} +void validate_arguments(Options& o,const std::set& seen) { + for(const auto& key:seen) { + if(key.compare(0,9,"--native-")!=0)continue; + if(o.solve.backend!=O::Backend::Native)throw std::invalid_argument("Native controls require the native backend"); + if(key=="--native-mode"||key=="--native-node-limit"||key=="--native-diagnostics")continue; + if(key.compare(0,14,"--native-auto-")==0) { + if(o.native_mode!="auto"&&o.native_mode!="race")throw std::invalid_argument(key+" requires auto or race mode"); + } else if(key.compare(0,14,"--native-race-")==0) { + if(o.native_mode!="race")throw std::invalid_argument(key+" requires race mode"); + } else if(o.native_mode!="configured")throw std::invalid_argument(key+" requires configured mode"); + } + const auto requires=[&](const std::string& key,bool valid,const std::string& reason) { + if(seen.count(key)&&!valid)throw std::invalid_argument(key+" requires "+reason); + }; + requires("--native-root-cuts",o.lp!="off","LP enabled"); + requires("--native-bound-tightening",o.lp!="off","LP enabled"); + requires("--native-lp-interval",o.lp=="updated","updated LP frequency"); + requires("--native-branching",o.search!="bab","frontier search (dfs or best-bound)"); + requires("--native-max-open-nodes",o.search!="bab","frontier search (dfs or best-bound)"); + requires("--native-neighborhood",o.search!="bab","frontier search (dfs or best-bound)"); + requires("--native-branching-probes",o.branching=="reliability","reliability branching"); + for(const auto* key:{"--native-neighborhood-radius","--native-neighborhood-nodes","--native-neighborhood-seconds"}) + requires(key,o.neighborhood=="hamming","Hamming neighborhoods"); + o.relaxation.frequency=o.lp=="updated"?O::NativeLpFrequency::AfterBoundChanges:O::NativeLpFrequency::Root; + if(o.root_cuts)o.relaxation.root_cover_cuts=O::NativeRootCoverSettings{}; + o.solve.validate(); + if(o.native_mode=="race") {o.race.solve=o.solve;o.race.automatic=o.automatic;o.race.validate();} + if(o.native_mode=="configured") { + if(o.lp!="off")o.relaxation.validate(); + if(o.search!="bab")search_options(o,o.solve).validate(); + if(o.neighborhood=="hamming")o.neighborhood_settings.validate(); + } +} +Options arguments(const std::vector& args) { + if(!args.empty()&&args[0]=="--minizinc") { + Options options;options.minizinc=true;bool timed=false,filename_only=false;std::set seen; + for(std::size_t i=1;i(milliseconds)/1000.0; + } else if(!filename_only&&arg.compare(0,9,"--native-")==0) { + if(i+1==args.size()||!seen.insert(arg).second)throw std::invalid_argument("Missing or repeated option: "+arg); + if(!native_argument(options,arg,args[++i]))throw std::invalid_argument("Unsupported MiniZinc protocol option: "+arg); + } else if(arg.empty()||(!filename_only&&arg[0]=='-'&&arg!="-")) + throw std::invalid_argument("Unsupported MiniZinc protocol option: "+arg); + else { + if(!options.filename.empty())throw std::invalid_argument("MiniZinc protocol requires exactly one model"); + options.filename=arg; + } + } + if(options.filename.empty())throw std::invalid_argument("MiniZinc protocol requires exactly one model"); + options.capture.source=options.filename;validate_arguments(options,seen);return options; + } + if(args.empty()||args[0].empty()||(args[0][0]=='-'&&args[0]!="-")) + throw std::invalid_argument(usage()); + Options options;options.filename=args[0];options.capture.source=args[0];std::set seen; + for(std::size_t i=1;istatic_cast(std::numeric_limits::max()))throw std::invalid_argument("Input limit exceeds parser index range"); + options.capture.max_input_bytes=static_cast(n); + } else if(!native_argument(options,key,value))throw std::invalid_argument("Unsupported option: "+key); + } + // The historical unprefixed node limit is also valid with HiGHS. + if(seen.count("--native-node-limit")&&std::find(args.begin(),args.end(),"--native-node-limit")==args.end())seen.erase("--native-node-limit"); + validate_arguments(options,seen);return options; +} +struct SolveOutput {O::SolveResult result;std::string work;}; +std::string lp_work(const O::NativeLpStatistics& s) { + return "lp-calls="+std::to_string(s.lp_calls)+" checked-bounds="+std::to_string(s.valid_bounds)+ + " root-cuts="+std::to_string(s.root_cover.cuts)+" variable-fixings="+std::to_string(s.variable_fixings)+ + " bound-tightenings="+std::to_string(s.variable_bound_tightenings); +} +SolveOutput search_output(O::NativeSearchResult result) { + auto work=lp_work(result.relaxation)+" frontier-admitted="+std::to_string(result.frontier.admitted_nodes)+ + " branching-probes="+std::to_string(result.branching.probe_status_calls)+ + " budget-nodes="+std::to_string(result.branching.budget_nodes); + return {std::move(result.result),std::move(work)}; +} +const char* neighborhood_completion(O::NativeNeighborhoodCompletion value) { + using C=O::NativeNeighborhoodCompletion; + switch(value) { + case C::NotStarted:return "not-started"; + case C::NoIncumbent:return "no-incumbent"; + case C::ProofCompletedBeforeAttempt:return "proof-completed-before-attempt"; + case C::NoEligibleBinary:return "no-eligible-binary"; + case C::NonrestrictingRadius:return "nonrestricting-radius"; + case C::FormulationLimit:return "formulation-limit"; + case C::SourceLimit:return "source-limit"; + case C::WorkLimit:return "work-limit"; + case C::StatusLimit:return "status-limit"; + case C::SharedNodeReserve:return "shared-node-reserve"; + case C::LocalStorageLimit:return "local-storage-limit"; + case C::LocalTimeLimit:return "local-time-limit"; + case C::NoImprovement:return "no-improvement"; + case C::Improved:return "improved"; + case C::GlobalStop:return "global-stop"; + case C::Error:return "error"; + } + return "unknown"; +} +SolveOutput dispatch(const O::ModelSnapshot& model,const Options& o,const O::SolveOptions& solve) { + if(solve.backend!=O::Backend::Native)return {O::solve(model,solve),{}}; + if(o.native_mode=="plain")return {O::solve_native(model,solve),{}}; + if(o.native_mode=="auto") { + O::NativeAutoOptions automatic;automatic.solve=solve;automatic.settings=o.automatic; + return {O::solve_native_auto_configured(model,automatic),{}}; + } + if(o.native_mode=="race") { + auto race=o.race;race.solve=solve;race.automatic=o.automatic; + return {O::solve_native_race(model,race),{}}; + } + if(o.search=="bab") { + if(o.lp=="off")return {O::solve_native(model,solve),{}}; + O::NativeLpOptions lp;static_cast(lp)=o.relaxation;lp.solve=solve; + auto result=O::solve_native_lp(model,lp);auto work=lp_work(result.relaxation); + return {std::move(result.result),std::move(work)}; + } + auto search=search_options(o,solve); + if(o.neighborhood=="off")return search_output(O::solve_native_search(model,search)); + O::NativeNeighborhoodOptions neighborhood;neighborhood.search=search;neighborhood.neighborhood=o.neighborhood_settings; + auto result=O::solve_native_neighborhoods(model,neighborhood);auto output=search_output(std::move(result.search)); + output.work+=" neighborhood-attempts="+std::to_string(result.neighborhood.attempts)+ + " neighborhood-improvements="+std::to_string(result.neighborhood.accepted_improvements)+ + " neighborhood-status-attempts="+std::to_string(result.neighborhood.status_attempts)+ + " neighborhood-eligible="+std::to_string(result.neighborhood.eligible_variables)+ + " neighborhood-completion="+neighborhood_completion(result.neighborhood.completion); + return output; +} +#ifdef GECODE_FLATZINC_DRIVER_TEST +SolveOutput test_solve(const O::ModelSnapshot&,const Options&,const O::SolveOptions&); +#endif +std::string location(const F::Location& at) { + return at.source+(at.line?":"+std::to_string(at.line):""); +} +struct Output {std::string text;int code=1;}; +std::string comment_line(std::string value) { + for(auto& c:value)if(static_cast(c)<32||static_cast(c)==127)c=' '; + return value; +} +std::string configuration(const Options& o) { + std::ostringstream text;text.imbue(std::locale::classic()); + const auto on=[](bool value){return value?"on":"off";}; + if(o.native_mode=="auto"||o.native_mode=="race") { + text<<"auto-presolve="<options.solve.integrality_tolerance) {complete=false;break;} + x=rounded; + } + if(complete) { + const auto checked=O::validate(compiled.model(),candidate.values,0,0); + if(checked.valid&&checked.objective) { + candidate.objective=checked.objective;candidate.solution_validated=true; + if(O::validate_flatzinc(compiled,candidate,0).valid) + throw std::runtime_error("Infeasible status contradicts the backend's original feasible assignment"); + } + } + } + for(const auto& gap:{result.absolute_gap,result.relative_gap,result.native_backend_gap}) + if(gap&&(!std::isfinite(*gap)||*gap<0))throw std::runtime_error("Backend returned an invalid gap"); + if(result.best_bound&&!std::isfinite(*result.best_bound))throw std::runtime_error("Backend returned a nonfinite bound for the bounded model"); + if(result.objective&&result.best_bound) { + auto checked=result;checked.update_gaps(compiled.model().objective.sense); + if((result.absolute_gap&&result.absolute_gap!=checked.absolute_gap)|| + (result.relative_gap&&result.relative_gap!=checked.relative_gap)) + throw std::runtime_error("Backend returned inconsistent gap fields"); + } else if(result.absolute_gap||result.relative_gap)throw std::runtime_error("Backend returned gaps without an objective and bound"); + const bool point=result.has_solution(); + if((result.termination==O::Termination::Optimal&&!point)|| + (result.termination==O::Termination::Infeasible&&point)) + throw std::runtime_error("Backend returned inconsistent status and assignment"); + if(result.termination==O::Termination::Unbounded||result.termination==O::Termination::InfeasibleOrUnbounded) + throw std::runtime_error("Unexpected unbounded status for the finite discrete model"); + // Do not silently hide a malformed claimed incumbent behind UNKNOWN or UNSAT. + if(result.solution_validated&&!point)throw std::runtime_error("Backend returned a malformed incumbent"); + if(result.termination==O::Termination::Optimal&&compiled.source().solve.method!=F::Method::Satisfy) { + if(!result.best_bound)throw std::runtime_error("Completed optimization has no bound"); + if(options.solve.guarantee==O::Guarantee::Exact&&result.best_bound!=result.objective) + throw std::runtime_error("Exact optimal status has an open objective gap"); + } + Output output; + output.text=options.solve.backend==O::Backend::Native ? "% guarantee: exact integer search\n" : "% guarantee: numerical; requested MIP gaps: 0\n"; + if(options.diagnostics) { + output.text+="% native-mode: "+comment_line(options.native_mode)+"\n% native-backend: "+comment_line(result.backend)+ + "\n% native-policy: "+comment_line(result.message)+"\n% native-configuration: "+comment_line(configuration(options))+"\n"; + if(!work.empty())output.text+="% native-work: "+comment_line(work)+"\n"; + } + if(point) { + output.text+=O::format_flatzinc_solution(compiled,result,options.solve.integrality_tolerance); + if(result.termination==O::Termination::Optimal) { + if(compiled.source().solve.method!=F::Method::Satisfy)output.text+="==========\n"; + output.code=0; + } + } else if(result.termination==O::Termination::Infeasible) { + output.text+="=====UNSATISFIABLE=====\n";output.code=0; + } else output.text+="=====UNKNOWN=====\n"; + return output; +} +int execute(const Options& options,std::istream& input,std::ostream& out,std::ostream& errors) { + const auto begin=Clock::now(); + const auto remaining=[&] {return std::max(0.0,options.solve.time_limit_seconds- + std::chrono::duration(Clock::now()-begin).count());}; + const auto expired=[&] {return remaining()==0;}; + const auto unknown=[&] {out<<"% time limit reached\n=====UNKNOWN=====\n";return options.minizinc?0:1;}; + try { + if(expired())return unknown(); + auto captured=F::parse(input,options.capture); + if(expired())return unknown(); + if(captured.status!=F::Status::Complete||!captured.records) { + std::string message="FlatZinc capture failed"; + if(!captured.diagnostics.empty())message=location(captured.diagnostics[0].location)+": "+captured.diagnostics[0].message; + throw std::runtime_error(message); + } + O::FlatZincCompileOptions compiler;compiler.time_limit_seconds=remaining(); + auto compiled=O::compile_flatzinc(*captured.records,compiler); + if(expired()||compiled.status==O::FlatZincCompileStatus::TimeLimit)return unknown(); + if(compiled.status!=O::FlatZincCompileStatus::Complete||!compiled.compiled) + throw std::runtime_error(location(compiled.location)+": "+compiled.message); + auto solve_options=options.solve;solve_options.time_limit_seconds=remaining(); +#ifdef GECODE_FLATZINC_DRIVER_TEST + auto solved=test_solve(compiled.compiled->model(),options,solve_options); +#else + auto solved=dispatch(compiled.compiled->model(),options,solve_options); +#endif + // The parser has cooperative stage boundaries, not token-level interruption. + // Never publish a newly returned point after the overall frontend deadline. + if(expired())return unknown(); + const auto& result=solved.result; + auto output=render(*compiled.compiled,result,options,solved.work); + if(expired())return unknown(); + out<(argv+1,argv+argc)); + if(options.filename=="-")return execute(options,std::cin,std::cout,std::cerr); + std::ifstream input(options.filename,std::ios::binary); + if(!input)throw std::runtime_error("Cannot open input: "+options.filename); + return execute(options,input,std::cout,std::cerr); + } catch(const std::exception& e) {std::cerr<<"FlatZinc optimization: "< 0, "Gecode Optimize: empty circuits are unsupported", + if min(index_set(x)) >= 0 then + gecode_circuit(min(index_set(x)), x) + else + gecode_circuit(0, [x[i] - min(index_set(x)) | i in index_set(x)]) + endif); diff --git a/tools/flatzinc/mznlib-optimize/fzn_cumulative.mzn b/tools/flatzinc/mznlib-optimize/fzn_cumulative.mzn new file mode 100644 index 0000000000..0b4bb51a93 --- /dev/null +++ b/tools/flatzinc/mznlib-optimize/fzn_cumulative.mzn @@ -0,0 +1,7 @@ +% The frontend accepts parameter values or original singleton domains only. +% Do not replace data by lb/ub inferred through propagation. +predicate gecode_cumulatives(array[int] of var int: s, array[int] of var int: d, + array[int] of var int: r, var int: b); +predicate fzn_cumulative(array[int] of var int: s, array[int] of var int: d, + array[int] of var int: r, var int: b) = + gecode_cumulatives(s, d, r, b); diff --git a/tools/flatzinc/mznlib-optimize/fzn_regular.mzn b/tools/flatzinc/mznlib-optimize/fzn_regular.mzn new file mode 100644 index 0000000000..a56685eed9 --- /dev/null +++ b/tools/flatzinc/mznlib-optimize/fzn_regular.mzn @@ -0,0 +1,5 @@ +predicate gecode_regular(array[int] of var int: x, int: Q, int: S, + array[int] of int: d, int: q0, set of int: F); +predicate fzn_regular(array[int] of var int: x, int: Q, int: S, + array[int,int] of int: d, int: q0, set of int: F) = + gecode_regular(x, Q, S, array1d(d), q0, F); diff --git a/tools/flatzinc/mznlib-optimize/fzn_table_int.mzn b/tools/flatzinc/mznlib-optimize/fzn_table_int.mzn new file mode 100644 index 0000000000..5311f9ad6b --- /dev/null +++ b/tools/flatzinc/mznlib-optimize/fzn_table_int.mzn @@ -0,0 +1,4 @@ +predicate gecode_table_int(array[int] of var int: x, array[int] of int: tuples); +predicate fzn_table_int(array[int] of var int: x, array[int,int] of int: t) = + assert(length(x) > 0, "Gecode Optimize: zero-arity flat tables are unsupported", + gecode_table_int(x, array1d(t))); diff --git a/tools/flatzinc/mznlib-optimize/redefinitions.mzn b/tools/flatzinc/mznlib-optimize/redefinitions.mzn new file mode 100644 index 0000000000..2af583f65b --- /dev/null +++ b/tools/flatzinc/mznlib-optimize/redefinitions.mzn @@ -0,0 +1,16 @@ +% This library intentionally does not inherit Gecode's broad native registry. +% Remaining standard decompositions must pass the frontend's whole-model check. +predicate int_eq_imp(var int: x, var int: y, var bool: b); +predicate int_le_imp(var int: x, var int: y, var bool: b); +predicate int_lin_le_imp(array[int] of int: a, array[int] of var int: x, int: b, var bool: r); +% Fail early for selected primitive families that cannot be compiled faithfully. +predicate int_times(var int: x, var int: y, var int: z) = + abort("Gecode Optimize: variable multiplication is unsupported"); +predicate int_div(var int: x, var int: y, var int: z) = + abort("Gecode Optimize: variable division is unsupported"); +predicate int_mod(var int: x, var int: y, var int: z) = + abort("Gecode Optimize: variable modulo is unsupported"); +predicate int_eq_reif(var int: x, var int: y, var bool: b) = + abort("Gecode Optimize: reified integer equality is unsupported"); +predicate int_lin_eq_reif(array[int] of int: a, array[int] of var int: x, int: b, var bool: r) = + abort("Gecode Optimize: reified linear equality is unsupported");