LCOV - code coverage report
Current view: top level - buildbot/coverage/build/src/theory/ff - multi_roots.cpp (source / functions) Hit Total Coverage
Test: coverage.info Lines: 140 169 82.8 %
Date: 2026-09-10 09:47:00 Functions: 17 21 81.0 %
Branches: 83 170 48.8 %

           Branch data     Line data    Source code
       1                 :            : /******************************************************************************
       2                 :            :  * This file is part of the cvc5 project.
       3                 :            :  *
       4                 :            :  * Copyright (c) 2009-2026 by the authors listed in the file AUTHORS
       5                 :            :  * in the top-level source directory and their institutional affiliations.
       6                 :            :  * All rights reserved.  See the file COPYING in the top-level source
       7                 :            :  * directory for licensing information.
       8                 :            :  * ****************************************************************************
       9                 :            :  *
      10                 :            :  * Multivariate root finding. Implements "FindZero" from [OKTB23].
      11                 :            :  *
      12                 :            :  * [OKTB23]: https://doi.org/10.1007/978-3-031-37703-7_8
      13                 :            :  */
      14                 :            : 
      15                 :            : #ifdef CVC5_USE_COCOA
      16                 :            : 
      17                 :            : #include "theory/ff/multi_roots.h"
      18                 :            : 
      19                 :            : #include <CoCoA/BigIntOps.H>
      20                 :            : #include <CoCoA/RingFp.H>
      21                 :            : #include <CoCoA/SparsePolyOps-MinPoly.H>
      22                 :            : #include <CoCoA/SparsePolyOps-RingElem.H>
      23                 :            : #include <CoCoA/SparsePolyOps-ideal.H>
      24                 :            : #include <CoCoA/ring.H>
      25                 :            : 
      26                 :            : #include <algorithm>
      27                 :            : #include <memory>
      28                 :            : #include <sstream>
      29                 :            : 
      30                 :            : #include "theory/ff/cocoa_util.h"
      31                 :            : #include "theory/ff/uni_roots.h"
      32                 :            : #include "theory/ff/util.h"
      33                 :            : #include "util/resource_manager.h"
      34                 :            : 
      35                 :            : namespace cvc5::internal {
      36                 :            : namespace theory {
      37                 :            : namespace ff {
      38                 :            : 
      39                 :        950 : AssignmentEnumerator::~AssignmentEnumerator() = default;
      40                 :            : 
      41                 :        602 : ListEnumerator::ListEnumerator(std::vector<CoCoA::RingElem>&& options)
      42                 :        602 :     : d_remainingOptions(std::move(options))
      43                 :            : {
      44                 :        602 :   std::reverse(d_remainingOptions.begin(), d_remainingOptions.end());
      45                 :        602 : }
      46                 :            : 
      47                 :       1204 : ListEnumerator::~ListEnumerator() {};
      48                 :            : 
      49                 :        614 : std::optional<CoCoA::RingElem> ListEnumerator::next()
      50                 :            : {
      51         [ +  + ]:        614 :   if (d_remainingOptions.empty())
      52                 :            :   {
      53                 :         15 :     return {};
      54                 :            :   }
      55                 :            :   else
      56                 :            :   {
      57                 :        599 :     CoCoA::RingElem v = d_remainingOptions.back();
      58                 :        599 :     d_remainingOptions.pop_back();
      59                 :        599 :     return v;
      60                 :        599 :   }
      61                 :            : }
      62                 :            : 
      63                 :          0 : std::string ListEnumerator::name() { return "list"; }
      64                 :            : 
      65                 :        601 : std::unique_ptr<ListEnumerator> factorEnumerator(CoCoA::RingElem univariatePoly)
      66                 :            : {
      67                 :        601 :   long varIdx = CoCoA::UnivariateIndetIndex(univariatePoly);
      68 [ -  + ][ -  + ]:        601 :   Assert(varIdx >= 0);
                 [ -  - ]
      69         [ +  - ]:        601 :   Trace("ff::model::factor") << "roots for: " << univariatePoly << std::endl;
      70                 :        601 :   std::vector<CoCoA::RingElem> theRoots = roots(univariatePoly);
      71                 :        601 :   std::vector<CoCoA::RingElem> linears{};
      72                 :        601 :   CoCoA::RingElem var = CoCoA::indet(CoCoA::owner(univariatePoly), varIdx);
      73         [ +  + ]:       1259 :   for (const auto& r : theRoots)
      74                 :            :   {
      75                 :        658 :     linears.push_back(var - r);
      76                 :            :   }
      77                 :       1202 :   return std::make_unique<ListEnumerator>(std::move(linears));
      78                 :        601 : }
      79                 :            : 
      80                 :        348 : RoundRobinEnumerator::RoundRobinEnumerator(
      81                 :        348 :     const std::vector<CoCoA::RingElem>& vars, const CoCoA::ring& ring)
      82                 :        348 :     : d_vars(vars),
      83                 :        348 :       d_ring(ring),
      84                 :        348 :       d_idx(),
      85                 :        696 :       d_maxIdx(
      86                 :        696 :           CoCoA::power(CoCoA::characteristic(ring), CoCoA::LogCardinality(ring))
      87                 :        348 :           * vars.size())
      88                 :            : {
      89                 :        348 : }
      90                 :            : 
      91                 :        696 : RoundRobinEnumerator::~RoundRobinEnumerator() {}
      92                 :            : 
      93                 :        591 : std::optional<CoCoA::RingElem> RoundRobinEnumerator::next()
      94                 :            : {
      95                 :        591 :   std::optional<CoCoA::RingElem> ret{};
      96         [ +  + ]:        591 :   if (d_idx != d_maxIdx)
      97                 :            :   {
      98                 :        589 :     size_t whichVar = d_idx % d_vars.size();
      99                 :        589 :     CoCoA::BigInt whichVal = d_idx / d_vars.size();
     100                 :        589 :     CoCoA::RingElem val = d_ring->myZero();
     101                 :        589 :     val += whichVal;
     102                 :        589 :     ret = d_vars[whichVar] - val;
     103                 :        589 :     ++d_idx;
     104                 :        589 :   }
     105                 :        591 :   return ret;
     106                 :          0 : }
     107                 :            : 
     108                 :          0 : std::string RoundRobinEnumerator::name() { return "round-robin"; }
     109                 :            : 
     110                 :       1123 : bool isUnsat(const CoCoA::ideal& ideal)
     111                 :            : {
     112                 :       1123 :   const auto& gens = CoCoA::GBasis(ideal);
     113         [ +  - ]:       1317 :   return gens.size() == 1 && !CoCoA::IsZero(gens[0])
     114 [ +  + ][ +  + ]:       1317 :          && CoCoA::deg(gens[0]) <= 0;
     115                 :            : }
     116                 :            : 
     117                 :            : // True if a Groebner basis for `ideal` is available without (re)computation.
     118                 :            : //
     119                 :            : // Since CoCoALib 0.99850, computing the GBasis of the zero ideal no longer
     120                 :            : // marks it as stored (CoCoA::GBasis returns the trivially empty basis but
     121                 :            : // CoCoA::HasGBasis stays false); 0.99800 did mark it. The empty basis is always
     122                 :            : // available for the zero ideal, so treat that case as "has GBasis" too.
     123                 :       2589 : bool hasGBasis(const CoCoA::ideal& ideal)
     124                 :            : {
     125 [ +  + ][ +  - ]:       2589 :   return CoCoA::HasGBasis(ideal) || CoCoA::IsZero(ideal);
     126                 :            : }
     127                 :            : 
     128                 :            : template <typename T>
     129                 :          0 : std::string ostring(const T& t)
     130                 :            : {
     131                 :          0 :   std::ostringstream o;
     132                 :          0 :   o << t;
     133                 :          0 :   return o.str();
     134                 :          0 : }
     135                 :            : 
     136                 :        943 : std::pair<size_t, CoCoA::RingElem> extractAssignment(
     137                 :            :     const CoCoA::RingElem& elem)
     138                 :            : {
     139 [ -  + ][ -  + ]:        943 :   Assert(CoCoA::deg(elem) == 1);
                 [ -  - ]
     140 [ -  + ][ -  + ]:        943 :   Assert(CoCoA::NumTerms(elem) <= 2);
                 [ -  - ]
     141                 :        943 :   const CoCoA::RingElem m = CoCoA::monic(elem);
     142                 :        943 :   long varNumber = CoCoA::UnivariateIndetIndex(elem);
     143 [ -  + ][ -  + ]:        943 :   Assert(varNumber >= 0);
                 [ -  - ]
     144                 :       2829 :   return {varNumber, -CoCoA::ConstantCoeff(m)};
     145                 :        943 : }
     146                 :            : 
     147                 :        964 : std::unordered_set<std::string> assignedVars(const CoCoA::ideal& ideal)
     148                 :            : {
     149                 :        964 :   std::unordered_set<std::string> ret{};
     150 [ -  + ][ -  + ]:        964 :   Assert(hasGBasis(ideal));
                 [ -  - ]
     151         [ +  + ]:       5024 :   for (const auto& g : CoCoA::GBasis(ideal))
     152                 :            :   {
     153         [ +  + ]:       4060 :     if (CoCoA::deg(g) == 1)
     154                 :            :     {
     155                 :       3432 :       long varNumber = CoCoA::UnivariateIndetIndex(g);
     156         [ +  + ]:       3432 :       if (varNumber >= 0)
     157                 :            :       {
     158                 :       3221 :         ret.insert(ostring(CoCoA::indet(ideal->myRing(), varNumber)));
     159                 :            :       }
     160                 :            :     }
     161                 :            :   }
     162                 :        964 :   return ret;
     163                 :          0 : }
     164                 :            : 
     165                 :        788 : bool allVarsAssigned(const CoCoA::ideal& ideal)
     166                 :            : {
     167                 :       1576 :   return assignedVars(ideal).size()
     168                 :       1576 :          == (size_t)CoCoA::NumIndets(ideal->myRing());
     169                 :            : }
     170                 :            : 
     171                 :        266 : std::unique_ptr<AssignmentEnumerator> applyRule(const CoCoA::ideal& ideal,
     172                 :            :                                                 FfStatistics* stats = nullptr)
     173                 :            : {
     174                 :        266 :   CoCoA::PolyRing polyRing(ideal->myRing());
     175 [ -  + ][ -  + ]:        266 :   Assert(!isUnsat(ideal));
                 [ -  - ]
     176                 :            :   // first, we look for super-linear univariate polynomials.
     177 [ -  + ][ -  + ]:        266 :   Assert(hasGBasis(ideal));
                 [ -  - ]
     178                 :        266 :   const auto& gens = CoCoA::GBasis(ideal);
     179         [ +  + ]:       1147 :   for (const auto& p : gens)
     180                 :            :   {
     181                 :        971 :     long varNumber = CoCoA::UnivariateIndetIndex(p);
     182 [ +  + ][ +  + ]:        971 :     if (varNumber >= 0 && CoCoA::deg(p) > 1)
                 [ +  + ]
     183                 :            :     {
     184                 :         90 :       return factorEnumerator(p);
     185                 :            :     }
     186                 :            :   }
     187                 :            :   // now, we check the dimension
     188         [ -  + ]:        176 :   if (CoCoA::IsZeroDim(ideal))
     189                 :            :   {
     190         [ -  - ]:          0 :     if (stats) ++stats->d_idealMinPoly;
     191                 :            :     // If zero-dimensional, we compute a minimal polynomial in some unset
     192                 :            :     // variable.
     193                 :          0 :     std::unordered_set<std::string> alreadySet = assignedVars(ideal);
     194         [ -  - ]:          0 :     for (const auto& var : CoCoA::indets(polyRing))
     195                 :            :     {
     196                 :          0 :       std::string varName = ostring(var);
     197         [ -  - ]:          0 :       if (!alreadySet.count(ostring(var)))
     198                 :            :       {
     199                 :          0 :         CoCoA::RingElem minPoly = CoCoA::MinPolyQuot(var, ideal, var);
     200                 :          0 :         return factorEnumerator(minPoly);
     201                 :          0 :       }
     202         [ -  - ]:          0 :     }
     203                 :          0 :     Unreachable()
     204                 :          0 :         << "There should be no unset variables in zero-dimensional ideal";
     205                 :          0 :   }
     206                 :            :   else
     207                 :            :   {
     208         [ +  + ]:        176 :     if (stats) ++stats->d_idealPosDim;
     209                 :            :     // If positive dimensional, we make a list of unset variables and
     210                 :            :     // round-robin guess.
     211                 :            :     //
     212                 :            :     // TODO(aozdemir): better model construction (cvc5-wishues/issues/138)
     213                 :        176 :     std::unordered_set<std::string> alreadySet = assignedVars(ideal);
     214                 :        176 :     std::vector<CoCoA::RingElem> toGuess{};
     215         [ +  + ]:       1172 :     for (const auto& var : CoCoA::indets(polyRing))
     216                 :            :     {
     217                 :        996 :       std::string varName = ostring(var);
     218         [ +  + ]:        996 :       if (!alreadySet.count(ostring(var)))
     219                 :            :       {
     220                 :        364 :         toGuess.push_back(var);
     221                 :            :       }
     222                 :        996 :     }
     223                 :        352 :     return std::make_unique<RoundRobinEnumerator>(toGuess,
     224                 :        352 :                                                   polyRing->myBaseRing());
     225                 :        176 :   }
     226                 :        266 : }
     227                 :            : 
     228                 :        211 : std::vector<CoCoA::RingElem> findZero(const CoCoA::ideal& initialIdeal,
     229                 :            :                                       const Env& env,
     230                 :            :                                       FfStatistics* stats)
     231                 :            : {
     232                 :        211 :   CoCoA::ring polyRing = initialIdeal->myRing();
     233                 :            :   // We maintain two stacks:
     234                 :            :   // * one of ideals
     235                 :            :   // * one of branchers
     236                 :            :   //
     237                 :            :   // If brancher B has the same index as ideal I, then B represents possible
     238                 :            :   // expansions of ideal I (equivalently, restrictions of I's variety).
     239                 :            :   //
     240                 :            :   // NB: FindZero of [OKTB23] also takes a partial map M as input. GB(I)
     241                 :            :   // implicitly represents M: GB(I) contains a univariate linear polynomial
     242                 :            :   // Xi - k, if and only iff M[Xi] = k.
     243                 :            :   //
     244                 :            :   // NB: FindZero of [OKTB23] is recursive. That recursion is flattened here
     245                 :            :   // using the two stacks. The stack of ideals represents the input to
     246                 :            :   // recursive FindZero: GB(I). The stack of branchers represents the
     247                 :            :   // continuation context (which iteration of the for loop to return to).
     248                 :            : 
     249                 :            :   // goal: find a zero for any ideal in the stack.
     250                 :        633 :   std::vector<CoCoA::ideal> ideals{initialIdeal};
     251         [ -  + ]:        211 :   if (TraceIsOn("ff::model::branch"))
     252                 :            :   {
     253         [ -  - ]:          0 :     Trace("ff::model::branch") << "init polys: " << std::endl;
     254         [ -  - ]:          0 :     for (const auto& p : CoCoA::gens(initialIdeal))
     255                 :            :     {
     256         [ -  - ]:          0 :       Trace("ff::model::branch") << " * " << p << std::endl;
     257                 :            :     }
     258                 :            :   }
     259                 :            : 
     260                 :        211 :   std::vector<std::unique_ptr<AssignmentEnumerator>> branchers{};
     261                 :            :   // while some ideal might have a zero.
     262         [ +  + ]:        863 :   while (!ideals.empty())
     263                 :            :   {
     264                 :            :     // check for timeout
     265         [ -  + ]:        849 :     if (env.getResourceManager()->outOfTime())
     266                 :            :     {
     267                 :          0 :       throw FfTimeoutException("findZero");
     268                 :            :     }
     269                 :            :     // choose one ideal
     270                 :        849 :     const auto& ideal = ideals.back();
     271                 :            :     // make sure we have a GBasis:
     272                 :        849 :     GBasisTimeout(ideal, env.getResourceManager());
     273 [ -  + ][ -  + ]:        849 :     Assert(hasGBasis(ideal));
                 [ -  - ]
     274                 :            :     // If the ideal is UNSAT, drop it.
     275         [ +  + ]:        849 :     if (isUnsat(ideal))
     276                 :            :     {
     277                 :         61 :       ideals.pop_back();
     278                 :            :     }
     279                 :            :     // If the ideal has a linear polynomial in each variable, we've found a
     280                 :            :     // variety element (a model).
     281         [ +  + ]:        788 :     else if (allVarsAssigned(ideal))
     282                 :            :     {
     283                 :        197 :       std::unordered_map<size_t, CoCoA::RingElem> varNumToValue{};
     284 [ -  + ][ -  + ]:        197 :       Assert(hasGBasis(ideal));
                 [ -  - ]
     285                 :        197 :       const auto& gens = CoCoA::GBasis(ideal);
     286                 :        197 :       size_t numIndets = CoCoA::NumIndets(polyRing);
     287 [ -  + ][ -  + ]:        197 :       Assert(gens.size() == numIndets);
                 [ -  - ]
     288         [ +  + ]:       1136 :       for (const auto& g : gens)
     289                 :            :       {
     290                 :        939 :         varNumToValue.insert(extractAssignment(g));
     291                 :            :       }
     292                 :        197 :       std::vector<CoCoA::RingElem> values{};
     293         [ +  + ]:       1136 :       for (size_t i = 0; i < numIndets; ++i)
     294                 :            :       {
     295                 :        939 :         values.push_back(varNumToValue[i]);
     296                 :            :       }
     297                 :        197 :       return values;
     298                 :        197 :     }
     299                 :            :     // If there are more ideals than branchers, branch
     300         [ +  + ]:        591 :     else if (ideals.size() > branchers.size())
     301                 :            :     {
     302 [ -  + ][ -  + ]:        266 :       Assert(ideals.size() == branchers.size() + 1);
                 [ -  - ]
     303                 :        266 :       branchers.push_back(applyRule(ideal, stats));
     304         [ +  - ]:        532 :       Trace("ff::model::branch")
     305 [ -  + ][ -  - ]:        266 :           << "brancher: " << branchers.back()->name() << std::endl;
     306         [ -  + ]:        266 :       if (TraceIsOn("ff::model::branch"))
     307                 :            :       {
     308         [ -  - ]:          0 :         Trace("ff::model::branch") << "ideal polys: " << std::endl;
     309         [ -  - ]:          0 :         for (const auto& p : CoCoA::gens(ideal))
     310                 :            :         {
     311         [ -  - ]:          0 :           Trace("ff::model::branch") << " * " << p << std::endl;
     312                 :            :         }
     313                 :            :       }
     314                 :            :     }
     315                 :            :     // Otherwise, this ideal should have a brancher; get the next branch
     316                 :            :     else
     317                 :            :     {
     318 [ -  + ][ -  + ]:        325 :       Assert(ideals.size() == branchers.size());
                 [ -  - ]
     319                 :        325 :       std::optional<CoCoA::RingElem> choicePoly = branchers.back()->next();
     320                 :            :       // construct a new ideal from the branch
     321         [ +  + ]:        325 :       if (choicePoly.has_value())
     322                 :            :       {
     323         [ +  - ]:        626 :         Trace("ff::model::branch")
     324                 :          0 :             << "level: " << branchers.size()
     325 [ -  + ][ -  - ]:        313 :             << ", brancher: " << branchers.back()->name()
     326                 :        313 :             << ", branch: " << choicePoly.value() << std::endl;
     327 [ -  + ][ -  + ]:        313 :         Assert(hasGBasis(ideal));
                 [ -  - ]
     328                 :        313 :         std::vector<CoCoA::RingElem> newGens = CoCoA::GBasis(ideal);
     329                 :        313 :         newGens.push_back(choicePoly.value());
     330                 :        313 :         ideals.push_back(CoCoA::ideal(newGens));
     331                 :        313 :       }
     332                 :            :       // or drop this ideal & brancher if we're out of branches.
     333                 :            :       else
     334                 :            :       {
     335                 :         12 :         branchers.pop_back();
     336                 :         12 :         ideals.pop_back();
     337                 :            :       }
     338                 :        325 :     }
     339                 :            :   }
     340                 :            :   // Could not find any solution; return empty.
     341                 :         14 :   return {};
     342                 :        211 : }
     343                 :            : 
     344                 :            : }  // namespace ff
     345                 :            : }  // namespace theory
     346                 :            : }  // namespace cvc5::internal
     347                 :            : 
     348                 :            : #endif /* CVC5_USE_COCOA */

Generated by: LCOV version 1.14