mirror of
				https://github.com/Z3Prover/z3
				synced 2025-10-31 19:52:29 +00:00 
			
		
		
		
	improved dio handler
Signed-off-by: Lev Nachmanson <levnach@hotmail.com>
This commit is contained in:
		
							parent
							
								
									30021dd74f
								
							
						
					
					
						commit
						6f7b749ff9
					
				
					 11 changed files with 885 additions and 544 deletions
				
			
		|  | @ -1,24 +1,24 @@ | |||
| /*++
 | ||||
| Copyright (c) 2017 Microsoft Corporation | ||||
|   Copyright (c) 2017 Microsoft Corporation | ||||
| 
 | ||||
| Module Name: | ||||
|   Module Name: | ||||
| 
 | ||||
|     <name> | ||||
|   <name> | ||||
| 
 | ||||
| Abstract: | ||||
|   Abstract: | ||||
| 
 | ||||
|     We have an equality : sum by j of row[j]*x[j] = rs | ||||
|     We try to pin a var by pushing the total by using the variable bounds | ||||
|     on a loop we drive the partial sum down, denoting the variables of this process by _u. | ||||
|     In the same loop trying to pin variables by pushing the partial sum up, denoting the variable related to it by _l | ||||
|   We have an equality : sum by j of row[j]*x[j] = rs | ||||
|   We try to pin a var by pushing the total by using the variable bounds | ||||
|   on a loop we drive the partial sum down, denoting the variables of this process by _u. | ||||
|   In the same loop trying to pin variables by pushing the partial sum up, denoting the variable related to it by _l | ||||
| 
 | ||||
| Author: | ||||
|     Lev Nachmanson  (levnach) | ||||
|     Nikolaj Bjorner (nbjorner) | ||||
| Revision History: | ||||
|   Author: | ||||
|   Lev Nachmanson  (levnach) | ||||
|   Nikolaj Bjorner (nbjorner) | ||||
|   Revision History: | ||||
| 
 | ||||
| 
 | ||||
| --*/ | ||||
|   --*/ | ||||
| #pragma once | ||||
| 
 | ||||
| #include "util/vector.h" | ||||
|  | @ -28,312 +28,343 @@ Revision History: | |||
| namespace lp { | ||||
|    | ||||
|   | ||||
| template <typename C, typename B> // C plays a role of a container, B - lp_bound_propagator
 | ||||
| class bound_analyzer_on_row { | ||||
|     const C&                           m_row; | ||||
|     B &                                m_bp; | ||||
|     int                                m_column_of_u; // index of an unlimited from above monoid
 | ||||
|     // -1 means that such a value is not found, -2 means that at least two of such monoids were found
 | ||||
|     int                                m_column_of_l; // index of an unlimited from below monoid
 | ||||
|     impq                               m_rs; | ||||
|     template <typename C, typename B> // C plays a role of a container, B - lp_bound_propagator
 | ||||
|     class bound_analyzer_on_row { | ||||
|         const C&                           m_row; | ||||
|         B &                                m_bp; | ||||
|         int                                m_column_of_u; // index of an unlimited from above monoid
 | ||||
|         // -1 means that such a value is not found, -2 means that at least two of such monoids were found
 | ||||
|         int                                m_column_of_l; // index of an unlimited from below monoid
 | ||||
|         impq                               m_rs; | ||||
| 
 | ||||
| public : | ||||
|     // constructor
 | ||||
|     bound_analyzer_on_row( | ||||
|         const C & it, | ||||
|         const numeric_pair<mpq>& rs, | ||||
|         B & bp) | ||||
|         : | ||||
|         m_row(it), | ||||
|         m_bp(bp), | ||||
|         m_column_of_u(-1), | ||||
|         m_column_of_l(-1), | ||||
|         m_rs(rs) | ||||
|     {} | ||||
|     public : | ||||
|         // constructor
 | ||||
|         bound_analyzer_on_row( | ||||
|             const C & it, | ||||
|             const numeric_pair<mpq>& rs, | ||||
|             B & bp) | ||||
|             : | ||||
|             m_row(it), | ||||
|             m_bp(bp), | ||||
|             m_column_of_u(-1), | ||||
|             m_column_of_l(-1), | ||||
|             m_rs(rs) | ||||
|             {} | ||||
| 
 | ||||
|      | ||||
|     static unsigned analyze_row(const C & row, | ||||
|                             const numeric_pair<mpq>& rs, | ||||
|                             B & bp) { | ||||
|         bound_analyzer_on_row a(row, rs, bp); | ||||
|         return a.analyze(); | ||||
|     } | ||||
| 
 | ||||
| private: | ||||
| 
 | ||||
|     unsigned analyze() { | ||||
|         unsigned num_prop = 0; | ||||
|         for (const auto & c : m_row) { | ||||
|             if ((m_column_of_l == -2) && (m_column_of_u == -2)) | ||||
|                 return 0; | ||||
|             analyze_bound_on_var_on_coeff(c.var(), c.coeff()); | ||||
|         static unsigned analyze_row(const C & row, | ||||
|                                     const numeric_pair<mpq>& rs, | ||||
|                                     B & bp) { | ||||
|             bound_analyzer_on_row a(row, rs, bp); | ||||
|             return a.analyze(); | ||||
|         } | ||||
|         ++num_prop; | ||||
|         if (m_column_of_u >= 0) | ||||
|             limit_monoid_u_from_below(); | ||||
|         else if (m_column_of_u == -1) | ||||
|             limit_all_monoids_from_below(); | ||||
|         else | ||||
|             --num_prop; | ||||
| 
 | ||||
|         ++num_prop; | ||||
|         if (m_column_of_l >= 0) | ||||
|             limit_monoid_l_from_above(); | ||||
|         else if (m_column_of_l == -1) | ||||
|             limit_all_monoids_from_above(); | ||||
|         else | ||||
|             --num_prop; | ||||
|         return num_prop; | ||||
|     } | ||||
|     private: | ||||
| 
 | ||||
|     bool bound_is_available(unsigned j, bool lower_bound) { | ||||
|         return (lower_bound && m_bp.lower_bound_is_available(j)) || | ||||
|             (!lower_bound && m_bp.upper_bound_is_available(j)); | ||||
|     } | ||||
| 
 | ||||
|     const impq & ub(unsigned j) const { | ||||
|         lp_assert(m_bp.upper_bound_is_available(j)); | ||||
|         return m_bp.get_upper_bound(j); | ||||
|     } | ||||
| 
 | ||||
|     const impq & lb(unsigned j) const { | ||||
|         lp_assert(m_bp.lower_bound_is_available(j)); | ||||
|         return m_bp.get_lower_bound(j); | ||||
|     } | ||||
| 
 | ||||
|     const mpq & monoid_max_no_mult(bool a_is_pos, unsigned j, bool & strict) const { | ||||
|         if (a_is_pos) { | ||||
|             strict = !is_zero(ub(j).y); | ||||
|             return ub(j).x; | ||||
|         } | ||||
|         strict = !is_zero(lb(j).y); | ||||
|         return lb(j).x; | ||||
|     } | ||||
| 
 | ||||
|     mpq monoid_max(const mpq & a, unsigned j) const { | ||||
|         return a * (is_pos(a) ? ub(j).x : lb(j).x); | ||||
|     } | ||||
| 
 | ||||
|     mpq monoid_max(const mpq & a, unsigned j, bool & strict) const { | ||||
|         if (is_pos(a)) { | ||||
|             strict = !is_zero(ub(j).y); | ||||
|             return a * ub(j).x; | ||||
|         } | ||||
|         strict = !is_zero(lb(j).y); | ||||
|         return a * lb(j).x; | ||||
|     } | ||||
| 
 | ||||
|     const mpq & monoid_min_no_mult(bool a_is_pos, unsigned j, bool & strict) const { | ||||
|         if (!a_is_pos) { | ||||
|             strict = !is_zero(ub(j).y); | ||||
|             return ub(j).x; | ||||
|         } | ||||
|         strict = !is_zero(lb(j).y); | ||||
|         return lb(j).x; | ||||
|     } | ||||
| 
 | ||||
|     mpq monoid_min(const mpq & a, unsigned j, bool& strict) const { | ||||
|         if (is_neg(a)) { | ||||
|             strict = !is_zero(ub(j).y); | ||||
|             return a * ub(j).x; | ||||
|         }         | ||||
|         strict = !is_zero(lb(j).y); | ||||
|         return a * lb(j).x; | ||||
|     } | ||||
| 
 | ||||
|     mpq monoid_min(const mpq & a, unsigned j) const { | ||||
|         return a * (is_neg(a) ? ub(j).x : lb(j).x); | ||||
|     } | ||||
|      | ||||
|     mpq m_total, m_bound; | ||||
|     void limit_all_monoids_from_above() { | ||||
|         int strict = 0; | ||||
|         m_total.reset(); | ||||
|         lp_assert(is_zero(m_total)); | ||||
|         for (const auto& p : m_row) { | ||||
|             bool str; | ||||
|             m_total -= monoid_min(p.coeff(), p.var(), str); | ||||
|             if (str) | ||||
|                 strict++; | ||||
|         } | ||||
|          | ||||
|         for (const auto &p : m_row) { | ||||
|             bool str; | ||||
|             bool a_is_pos = is_pos(p.coeff()); | ||||
|             m_bound = m_total; | ||||
|             m_bound /= p.coeff(); | ||||
|             m_bound += monoid_min_no_mult(a_is_pos, p.var(), str); | ||||
|             if (a_is_pos) { | ||||
|                 limit_j(p.var(), m_bound, true, false, strict - static_cast<int>(str) > 0); | ||||
|         unsigned analyze() { | ||||
|             unsigned num_prop = 0; | ||||
|             for (const auto & c : m_row) { | ||||
|                 if ((m_column_of_l == -2) && (m_column_of_u == -2)) | ||||
|                     return 0; | ||||
|                 analyze_bound_on_var_on_coeff(c.var(), c.coeff()); | ||||
|             } | ||||
|             else { | ||||
|                 limit_j(p.var(), m_bound, false, true, strict - static_cast<int>(str) > 0); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     void limit_all_monoids_from_below() { | ||||
|         int strict = 0; | ||||
|         m_total.reset(); | ||||
|         lp_assert(is_zero(m_total)); | ||||
|         for (const auto &p : m_row) { | ||||
|             bool str; | ||||
|             m_total -= monoid_max(p.coeff(), p.var(), str); | ||||
|             if (str) | ||||
|                 strict++; | ||||
|         } | ||||
| 
 | ||||
|         for (const auto& p : m_row) { | ||||
|             bool str; | ||||
|             bool a_is_pos = is_pos(p.coeff()); | ||||
|             m_bound = m_total; | ||||
|             m_bound /= p.coeff(); | ||||
|             m_bound += monoid_max_no_mult(a_is_pos, p.var(), str); | ||||
|             bool astrict = strict - static_cast<int>(str) > 0;  | ||||
|             if (a_is_pos) { | ||||
|                 limit_j(p.var(), m_bound, true, true, astrict); | ||||
|             } | ||||
|             else { | ||||
|                 limit_j(p.var(), m_bound, false, false, astrict); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|      | ||||
|     void limit_monoid_u_from_below() { | ||||
|         // we are going to limit from below the monoid m_column_of_u,
 | ||||
|         // every other monoid is impossible to limit from below
 | ||||
|         mpq u_coeff; | ||||
|         unsigned j; | ||||
|         m_bound = -m_rs.x; | ||||
|         bool strict = false; | ||||
|         for (const auto& p : m_row) { | ||||
|             j = p.var(); | ||||
|             if (j == static_cast<unsigned>(m_column_of_u)) { | ||||
|                 u_coeff = p.coeff(); | ||||
|                 continue; | ||||
|             } | ||||
|             bool str; | ||||
|             m_bound -= monoid_max(p.coeff(), j, str); | ||||
|             if (str) | ||||
|                 strict = true; | ||||
|         } | ||||
| 
 | ||||
|         m_bound /= u_coeff; | ||||
|          | ||||
|         if (u_coeff.is_pos()) { | ||||
|             limit_j(m_column_of_u, m_bound, true, true, strict); | ||||
|         } else { | ||||
|             limit_j(m_column_of_u, m_bound, false, false, strict); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     void limit_monoid_l_from_above() { | ||||
|         // we are going to limit from above the monoid m_column_of_l,
 | ||||
|         // every other monoid is impossible to limit from above
 | ||||
|         mpq l_coeff; | ||||
|         unsigned j; | ||||
|         m_bound = -m_rs.x; | ||||
|         bool strict = false; | ||||
|         for (const auto &p : m_row) { | ||||
|             j = p.var(); | ||||
|             if (j == static_cast<unsigned>(m_column_of_l)) { | ||||
|                 l_coeff = p.coeff(); | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             bool str; | ||||
|             m_bound -= monoid_min(p.coeff(), j, str); | ||||
|             if (str) | ||||
|                 strict = true; | ||||
|         } | ||||
|         m_bound /= l_coeff; | ||||
|         if (is_pos(l_coeff)) { | ||||
|             limit_j(m_column_of_l, m_bound, true, false, strict); | ||||
|         } else { | ||||
|             limit_j(m_column_of_l, m_bound, false, true, strict); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // // it is the coefficient before the bounded column
 | ||||
|     // void provide_evidence(bool coeff_is_pos) {
 | ||||
|     //     /*
 | ||||
|     //     auto & be = m_ibounds.back();
 | ||||
|     //     bool lower_bound = be.m_lower_bound;
 | ||||
|     //     if (!coeff_is_pos)
 | ||||
|     //         lower_bound = !lower_bound;
 | ||||
|     //     auto it = m_row.clone();
 | ||||
|     //     mpq a; unsigned j;
 | ||||
|     //     while (it->next(a, j)) {
 | ||||
|     //         if (be.m_j == j) continue;
 | ||||
|     //         lp_assert(bound_is_available(j, is_neg(a) ? lower_bound : !lower_bound));
 | ||||
|     //         be.m_vector_of_bound_signatures.emplace_back(a, j, numeric_traits<impq>::
 | ||||
|     //                                                      is_neg(a)? lower_bound: !lower_bound);
 | ||||
|     //     }
 | ||||
|     //     delete it;
 | ||||
|     //     */
 | ||||
|     // }
 | ||||
| 
 | ||||
|     void limit_j(unsigned bound_j, const mpq& u, bool coeff_before_j_is_pos, bool is_lower_bound, bool strict) | ||||
|     { | ||||
|         auto* lar = &m_bp.lp(); | ||||
|         const auto& row = this->m_row; | ||||
|         auto explain = [row, bound_j, coeff_before_j_is_pos, is_lower_bound, strict, lar]() { | ||||
|             (void) strict; | ||||
|             TRACE("bound_analyzer", tout << "explain_bound_on_var_on_coeff, bound_j = " << bound_j << ", coeff_before_j_is_pos = " << coeff_before_j_is_pos << ", is_lower_bound = " << is_lower_bound << ", strict = " << strict << "\n";); | ||||
|             int bound_sign = (is_lower_bound ? 1 : -1); | ||||
|             int j_sign = (coeff_before_j_is_pos ? 1 : -1) * bound_sign; | ||||
| 
 | ||||
|             u_dependency* ret = nullptr; | ||||
|             for (auto const& r : row) { | ||||
|                 unsigned j = r.var(); | ||||
|                 if (j == bound_j) | ||||
|                     continue; | ||||
|                 mpq const& a = r.coeff(); | ||||
|                 int a_sign = is_pos(a) ? 1 : -1; | ||||
|                 int sign = j_sign * a_sign; | ||||
|                 u_dependency* witness = sign > 0 ? lar->get_column_upper_bound_witness(j) : lar->get_column_lower_bound_witness(j); | ||||
|                 ret = lar->join_deps(ret, witness); | ||||
|             } | ||||
|             return ret; | ||||
|         }; | ||||
|         m_bp.add_bound(u, bound_j, is_lower_bound, strict, explain); | ||||
|     } | ||||
| 
 | ||||
|     void advance_u(unsigned j) { | ||||
|         m_column_of_u = (m_column_of_u == -1) ? j : -2; | ||||
|     } | ||||
|      | ||||
|     void advance_l(unsigned j) { | ||||
|         m_column_of_l = (m_column_of_l == -1) ? j : -2; | ||||
|     } | ||||
|      | ||||
|     void analyze_bound_on_var_on_coeff(int j, const mpq &a) { | ||||
|         switch (m_bp.get_column_type(j)) { | ||||
|         case column_type::lower_bound: | ||||
|             if (numeric_traits<mpq>::is_pos(a)) | ||||
|                 advance_u(j); | ||||
|             else  | ||||
|                 advance_l(j); | ||||
|             break; | ||||
|         case column_type::upper_bound: | ||||
|             if (numeric_traits<mpq>::is_neg(a)) | ||||
|                 advance_u(j); | ||||
|             ++num_prop; | ||||
|             if (m_column_of_u >= 0) | ||||
|                 limit_monoid_u_from_below(); | ||||
|             else if (m_column_of_u == -1) | ||||
|                 limit_all_monoids_from_below(); | ||||
|             else | ||||
|                 advance_l(j); | ||||
|             break; | ||||
|         case column_type::free_column: | ||||
|             advance_u(j); | ||||
|             advance_l(j); | ||||
|             break; | ||||
|         default: | ||||
|             break; | ||||
|         } | ||||
|     }    | ||||
|      | ||||
| }; | ||||
|     | ||||
|                 --num_prop; | ||||
| 
 | ||||
|             ++num_prop; | ||||
|             if (m_column_of_l >= 0) | ||||
|                 limit_monoid_l_from_above(); | ||||
|             else if (m_column_of_l == -1) | ||||
|                 limit_all_monoids_from_above(); | ||||
|             else | ||||
|                 --num_prop; | ||||
|             return num_prop; | ||||
|         } | ||||
| 
 | ||||
|         bool bound_is_available(unsigned j, bool lower_bound) { | ||||
|             return (lower_bound && m_bp.lower_bound_is_available(j)) || | ||||
|                 (!lower_bound && m_bp.upper_bound_is_available(j)); | ||||
|         } | ||||
| 
 | ||||
|         const impq & ub(unsigned j) const { | ||||
|             lp_assert(upper_bound_is_available(j)); | ||||
|             return get_upper_bound(j); | ||||
|         } | ||||
| 
 | ||||
|         const impq & lb(unsigned j) const { | ||||
|             lp_assert(lower_bound_is_available(j)); | ||||
|             return get_lower_bound(j); | ||||
|         } | ||||
| 
 | ||||
|         const mpq & monoid_max_no_mult(bool a_is_pos, unsigned j, bool & strict) const { | ||||
|             if (a_is_pos) { | ||||
|                 strict = !is_zero(ub(j).y); | ||||
|                 return ub(j).x; | ||||
|             } | ||||
|             strict = !is_zero(lb(j).y); | ||||
|             return lb(j).x; | ||||
|         } | ||||
| 
 | ||||
|         mpq monoid_max(const mpq & a, unsigned j) const { | ||||
|             return a * (is_pos(a) ? ub(j).x : lb(j).x); | ||||
|         } | ||||
| 
 | ||||
|         mpq monoid_max(const mpq & a, unsigned j, bool & strict) const { | ||||
|             if (is_pos(a)) { | ||||
|                 strict = !is_zero(ub(j).y); | ||||
|                 return a * ub(j).x; | ||||
|             } | ||||
|             strict = !is_zero(lb(j).y); | ||||
|             return a * lb(j).x; | ||||
|         } | ||||
| 
 | ||||
|         const mpq & monoid_min_no_mult(bool a_is_pos, unsigned j, bool & strict) const { | ||||
|             if (!a_is_pos) { | ||||
|                 strict = !is_zero(ub(j).y); | ||||
|                 return ub(j).x; | ||||
|             } | ||||
|             strict = !is_zero(lb(j).y); | ||||
|             return lb(j).x; | ||||
|         } | ||||
| 
 | ||||
|         mpq monoid_min(const mpq & a, unsigned j, bool& strict) const { | ||||
|             if (is_neg(a)) { | ||||
|                 strict = !is_zero(ub(j).y); | ||||
|                 return a * ub(j).x; | ||||
|             }         | ||||
|             strict = !is_zero(lb(j).y); | ||||
|             return a * lb(j).x; | ||||
|         } | ||||
| 
 | ||||
|         mpq monoid_min(const mpq & a, unsigned j) const { | ||||
|             return a * (is_neg(a) ? ub(j).x : lb(j).x); | ||||
|         } | ||||
|      | ||||
|         mpq m_total, m_bound; | ||||
|         void limit_all_monoids_from_above() { | ||||
|             int strict = 0; | ||||
|             m_total = m_rs.x; | ||||
|             for (const auto& p : m_row) { | ||||
|                 bool str; | ||||
|                 m_total -= monoid_min(p.coeff(), p.var(), str); | ||||
|                 if (str) | ||||
|                     strict++; | ||||
|             } | ||||
|          | ||||
|             for (const auto &p : m_row) { | ||||
|                 bool str; | ||||
|                 bool a_is_pos = is_pos(p.coeff()); | ||||
|                 m_bound = m_total; | ||||
|                 m_bound /= p.coeff(); | ||||
|                 m_bound += monoid_min_no_mult(a_is_pos, p.var(), str); | ||||
|                 if (a_is_pos) { | ||||
|                     limit_j(p.var(), m_bound, true, false, strict - static_cast<int>(str) > 0); | ||||
|                 } | ||||
|                 else { | ||||
|                     limit_j(p.var(), m_bound, false, true, strict - static_cast<int>(str) > 0); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         void limit_all_monoids_from_below() { | ||||
|             int strict = 0; | ||||
|             m_total = m_rs.x; | ||||
|             for (const auto &p : m_row) { | ||||
|                 bool str; | ||||
|                 m_total -= monoid_max(p.coeff(), p.var(), str); | ||||
|                 if (str) | ||||
|                     strict++; | ||||
|             } | ||||
| 
 | ||||
|             for (const auto& p : m_row) { | ||||
|                 bool str; | ||||
|                 bool a_is_pos = is_pos(p.coeff()); | ||||
|                 m_bound = m_total; | ||||
|                 m_bound /= p.coeff(); | ||||
|                 m_bound += monoid_max_no_mult(a_is_pos, p.var(), str); | ||||
|                 bool astrict = strict - static_cast<int>(str) > 0;  | ||||
|                 if (a_is_pos) { | ||||
|                     limit_j(p.var(), m_bound, true, true, astrict); | ||||
|                 } | ||||
|                 else { | ||||
|                     limit_j(p.var(), m_bound, false, false, astrict); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|      | ||||
|         void limit_monoid_u_from_below() { | ||||
|             // we are going to limit from below the monoid m_column_of_u,
 | ||||
|             // every other monoid is impossible to limit from below
 | ||||
|             mpq u_coeff; | ||||
|             unsigned j; | ||||
|             m_bound = m_rs.x; | ||||
|             bool strict = false; | ||||
|             for (const auto& p : m_row) { | ||||
|                 j = p.var(); | ||||
|                 if (j == static_cast<unsigned>(m_column_of_u)) { | ||||
|                     u_coeff = p.coeff(); | ||||
|                     continue; | ||||
|                 } | ||||
|                 bool str; | ||||
|                 m_bound -= monoid_max(p.coeff(), j, str); | ||||
|                 if (str) | ||||
|                     strict = true; | ||||
|             } | ||||
| 
 | ||||
|             m_bound /= u_coeff; | ||||
|          | ||||
|             if (u_coeff.is_pos()) { | ||||
|                 limit_j(m_column_of_u, m_bound, true, true, strict); | ||||
|             } else { | ||||
|                 limit_j(m_column_of_u, m_bound, false, false, strict); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         void limit_monoid_l_from_above() { | ||||
|             // we are going to limit from above the monoid m_column_of_l,
 | ||||
|             // every other monoid is impossible to limit from above
 | ||||
|             mpq l_coeff; | ||||
|             unsigned j; | ||||
|             m_bound = m_rs.x; | ||||
|             bool strict = false; | ||||
|             for (const auto &p : m_row) { | ||||
|                 j = p.var(); | ||||
|                 if (j == static_cast<unsigned>(m_column_of_l)) { | ||||
|                     l_coeff = p.coeff(); | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 bool str; | ||||
|                 m_bound -= monoid_min(p.coeff(), j, str); | ||||
|                 if (str) | ||||
|                     strict = true; | ||||
|             } | ||||
|             m_bound /= l_coeff; | ||||
|             if (is_pos(l_coeff)) { | ||||
|                 limit_j(m_column_of_l, m_bound, true, false, strict); | ||||
|             } else { | ||||
|                 limit_j(m_column_of_l, m_bound, false, true, strict); | ||||
|             } | ||||
|         } | ||||
|      | ||||
|         // // it is the coefficient before the bounded column
 | ||||
|         // void provide_evidence(bool coeff_is_pos) {
 | ||||
|         //     /*
 | ||||
|         //     auto & be = m_ibounds.back();
 | ||||
|         //     bool lower_bound = be.m_lower_bound;
 | ||||
|         //     if (!coeff_is_pos)
 | ||||
|         //         lower_bound = !lower_bound;
 | ||||
|         //     auto it = m_row.clone();
 | ||||
|         //     mpq a; unsigned j;
 | ||||
|         //     while (it->next(a, j)) {
 | ||||
|         //         if (be.m_j == j) continue;
 | ||||
|         //         lp_assert(bound_is_available(j, is_neg(a) ? lower_bound : !lower_bound));
 | ||||
|         //         be.m_vector_of_bound_signatures.emplace_back(a, j, numeric_traits<impq>::
 | ||||
|         //                                                      is_neg(a)? lower_bound: !lower_bound);
 | ||||
|         //     }
 | ||||
|         //     delete it;
 | ||||
|         //     */
 | ||||
|         // }
 | ||||
| 
 | ||||
|         void limit_j(unsigned bound_j, const mpq& u, bool coeff_before_j_is_pos, bool is_lower_bound, bool strict) { | ||||
|             auto* lar = &m_bp.lp(); | ||||
|             const auto& row = this->m_row; | ||||
|             auto explain = [row, bound_j, coeff_before_j_is_pos, is_lower_bound, strict, lar]() { | ||||
|                 (void) strict; | ||||
|                 TRACE("bound_analyzer", tout << "explain_bound_on_var_on_coeff, bound_j = " << bound_j << ", coeff_before_j_is_pos = " << coeff_before_j_is_pos << ", is_lower_bound = " << is_lower_bound << ", strict = " << strict << "\n";); | ||||
|                 int bound_sign = (is_lower_bound ? 1 : -1); | ||||
|                 int j_sign = (coeff_before_j_is_pos ? 1 : -1) * bound_sign; | ||||
| 
 | ||||
|                 u_dependency* ret = nullptr; | ||||
|                 for (auto const& r : row) { | ||||
|                     unsigned j = r.var(); | ||||
|                     if (j == bound_j) | ||||
|                         continue; | ||||
|                     mpq const& a = r.coeff(); | ||||
|                     int a_sign = is_pos(a) ? 1 : -1; | ||||
|                     int sign = j_sign * a_sign; | ||||
|                     u_dependency* witness = sign > 0 ? lar->get_column_upper_bound_witness(j) : lar->get_column_lower_bound_witness(j); | ||||
|                     ret = lar->join_deps(ret, witness); | ||||
|                 } | ||||
|                 return ret; | ||||
|             }; | ||||
|             m_bp.add_bound(u, bound_j, is_lower_bound, strict, explain); | ||||
|         } | ||||
| 
 | ||||
|         void advance_u(unsigned j) { | ||||
|             m_column_of_u = (m_column_of_u == -1) ? j : -2; | ||||
|         } | ||||
|      | ||||
|         void advance_l(unsigned j) { | ||||
|             m_column_of_l = (m_column_of_l == -1) ? j : -2; | ||||
|         } | ||||
|      | ||||
|         void analyze_bound_on_var_on_coeff(int j, const mpq &a) { | ||||
|             switch (get_column_type(j)) { | ||||
|             case column_type::lower_bound: | ||||
|                 if (numeric_traits<mpq>::is_pos(a)) | ||||
|                     advance_u(j); | ||||
|                 else  | ||||
|                     advance_l(j); | ||||
|                 break; | ||||
|             case column_type::upper_bound: | ||||
|                 if (numeric_traits<mpq>::is_neg(a)) | ||||
|                     advance_u(j); | ||||
|                 else | ||||
|                     advance_l(j); | ||||
|                 break; | ||||
|             case column_type::free_column: | ||||
|                 advance_u(j); | ||||
|                 advance_l(j); | ||||
|                 break; | ||||
|             default: | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         const impq& get_upper_bound(unsigned j) const { | ||||
|             return lp().get_upper_bound(j); | ||||
|         } | ||||
| 
 | ||||
|         const impq& get_lower_bound(unsigned j) const { | ||||
|             return lp().get_lower_bound(j); | ||||
|         } | ||||
| 
 | ||||
|         column_type get_column_type(unsigned j) const { | ||||
|             return (lp().get_column_types())[j]; | ||||
|         } | ||||
|          | ||||
|         const auto& lp() const { return m_bp.lp(); } | ||||
| 
 | ||||
|         auto& lp() { return m_bp.lp(); } | ||||
| 
 | ||||
|         bool upper_bound_is_available(unsigned j) const { | ||||
|             switch (get_column_type(j)) { | ||||
|             case column_type::fixed: | ||||
|             case column_type::boxed: | ||||
|             case column_type::upper_bound: | ||||
|                 return true; | ||||
|             default: | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|         bool lower_bound_is_available(unsigned j) const { | ||||
|             switch (get_column_type(j)) { | ||||
|             case column_type::fixed: | ||||
|             case column_type::boxed: | ||||
|             case column_type::lower_bound: | ||||
|                 return true; | ||||
|             default: | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -188,7 +188,7 @@ namespace lp { | |||
|         } | ||||
| 
 | ||||
|         bool should_gomory_cut() { | ||||
|             return (!settings().dio_eqs() || settings().dio_enable_gomory_cuts()) | ||||
|             return (!all_columns_are_integral() ||(!settings().dio_eqs() || settings().dio_enable_gomory_cuts())) | ||||
|                 && m_number_of_calls % settings().m_int_gomory_cut_period == 0; | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -2509,6 +2509,24 @@ namespace lp { | |||
|     // Otherwise the new asserted lower bound is is greater than the existing upper bound.
 | ||||
|     // dep is the reason for the new bound
 | ||||
| 
 | ||||
|     void lar_solver::write_bound_lemma_to_file(unsigned j, bool is_low, const std::string & file_name, const std::string& location) const { | ||||
|         std::ofstream file(file_name); | ||||
|         if (!file.is_open()) { | ||||
|             // Handle file open error
 | ||||
|             std::cerr << "Failed to open file: " << file_name << std::endl; | ||||
|             return; | ||||
|         } | ||||
|      | ||||
|         write_bound_lemma(j, is_low, location, file); | ||||
|         file.close(); | ||||
|      | ||||
|         if (file.fail()) { | ||||
|             std::cerr << "Error occurred while writing to file: " << file_name << std::endl; | ||||
|         } else { | ||||
|             std::cout << "Bound lemma written to " << file_name << std::endl; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     void lar_solver::set_crossed_bounds_column_and_deps(unsigned j, bool lower_bound, u_dependency* dep) { | ||||
|         if (m_crossed_bounds_column != null_lpvar) return; // already set
 | ||||
|         SASSERT(m_crossed_bounds_deps == nullptr); | ||||
|  | @ -2518,7 +2536,7 @@ namespace lp { | |||
|         u_dependency* bdep = lower_bound? ul.lower_bound_witness() : ul.upper_bound_witness(); | ||||
|         SASSERT(bdep != nullptr); | ||||
|         m_crossed_bounds_deps = m_dependencies.mk_join(bdep, dep); | ||||
|         insert_to_columns_with_changed_bounds(j); | ||||
|         TRACE("dio", tout << "crossed_bound_deps:\n";  print_explanation(tout, flatten(m_crossed_bounds_deps)) << "\n";); | ||||
|     } | ||||
| 
 | ||||
|     void lar_solver::collect_more_rows_for_lp_propagation(){ | ||||
|  | @ -2539,6 +2557,189 @@ namespace lp { | |||
|         return out; | ||||
|     } | ||||
| 
 | ||||
|      | ||||
| 
 | ||||
| 
 | ||||
|         // Helper function to format constants in SMT2 format
 | ||||
|     std::string format_smt2_constant(const mpq& val) { | ||||
|         if (val.is_neg()) { | ||||
|             // Negative constant - use unary minus operator
 | ||||
|             return std::string("(- ") + abs(val).to_string() + ")"; | ||||
|         } else { | ||||
|             // Positive or zero constant - write directly
 | ||||
|             return val.to_string(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     void lar_solver::write_bound_lemma(unsigned j, bool is_low, const std::string& location, std::ostream & out) const { | ||||
|         // Get the bound value and dependency
 | ||||
|         mpq bound_val; | ||||
|         bool is_strict = false; | ||||
|         u_dependency* bound_dep = nullptr; | ||||
|      | ||||
|         // Get the appropriate bound info
 | ||||
|         if (is_low) { | ||||
|             if (!has_lower_bound(j, bound_dep, bound_val, is_strict)) { | ||||
|                 out << "; Error: Variable " << j << " has no lower bound\n"; | ||||
|                 return; | ||||
|             } | ||||
|         } else { | ||||
|             if (!has_upper_bound(j, bound_dep, bound_val, is_strict)) { | ||||
|                 out << "; Error: Variable " << j << " has no upper bound\n"; | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|             // Start SMT2 file
 | ||||
|         out << "(set-info : \"generated at " << location; | ||||
|         out << " for " << (is_low ? "lower" : "upper") << " bound of variable " << j << ","; | ||||
|         out << " bound value: " << bound_val << (is_strict ? (is_low ? " < " : " > ") : (is_low ? " <= " : " >= ")) << "x" << j << "\")\n"; | ||||
|          | ||||
|         // Collect all variables used in dependencies
 | ||||
|         std::unordered_set<unsigned> vars_used; | ||||
|         vars_used.insert(j); | ||||
|         bool is_int = column_is_int(j); | ||||
|          | ||||
|         // Linearize the dependencies
 | ||||
|         svector<constraint_index> deps; | ||||
|         m_dependencies.linearize(bound_dep, deps); | ||||
|          | ||||
|         // Collect variables from constraints
 | ||||
|         for (auto ci : deps) { | ||||
|             const auto& c = m_constraints[ci]; | ||||
|             for (const auto& p : c.coeffs()) { | ||||
|                 vars_used.insert(p.second); | ||||
|                 if (!column_is_int(p.second)) | ||||
|                     is_int = false; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Collect variables from terms
 | ||||
|         std::unordered_set<unsigned> term_variables; | ||||
|         for (unsigned var : vars_used) { | ||||
|             if (column_has_term(var)) { | ||||
|                 const lar_term& term = get_term(var); | ||||
|                 for (const auto& p : term) { | ||||
|                     term_variables.insert(p.j()); | ||||
|                     if (!column_is_int(p.j())) | ||||
|                         is_int = false; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         // Add term variables to vars_used
 | ||||
|         vars_used.insert(term_variables.begin(), term_variables.end()); | ||||
|                  | ||||
|         if (is_int) { | ||||
|             out << "(set-logic QF_LIA)\n\n"; | ||||
|         } | ||||
|          | ||||
|         // Declare variables
 | ||||
|         out << "; Variable declarations\n"; | ||||
|         for (unsigned var : vars_used) { | ||||
|             out << "(declare-const x" << var << " " << (column_is_int(var) ? "Int" : "Real") << ")\n"; | ||||
|         } | ||||
|         out << "\n"; | ||||
|          | ||||
|         // Define term relationships
 | ||||
|         out << "; Term definitions\n"; | ||||
|         for (unsigned var : vars_used) { | ||||
|             if (column_has_term(var)) { | ||||
|                 const lar_term& term = get_term(var); | ||||
|                 out << "(assert (= x" << var << " "; | ||||
|                      | ||||
|                 if (term.size() == 0) { | ||||
|                     out << "0"; | ||||
|                 } else { | ||||
|                     if (term.size() > 1) out << "(+ "; | ||||
|                          | ||||
|                     bool first = true; | ||||
|                     for (const auto& p : term) { | ||||
|                         if (first) first = false; | ||||
|                         else out << " "; | ||||
|                              | ||||
|                         if (p.coeff().is_one()) { | ||||
|                             out << "x" << p.j(); | ||||
|                         } else { | ||||
|                             out << "(* " << format_smt2_constant(p.coeff()) << " x" << p.j() << ")"; | ||||
|                         } | ||||
|                     } | ||||
|                          | ||||
|                     if (term.size() > 1) out << ")"; | ||||
|                 } | ||||
|                      | ||||
|                 out << "))\n"; | ||||
|             } | ||||
|         } | ||||
|         out << "\n"; | ||||
|          | ||||
|         // Add assertions for the dependencies
 | ||||
|         out << "; Bound dependencies\n"; | ||||
|          | ||||
|         for (auto ci : deps) { | ||||
|             const auto& c = m_constraints[ci]; | ||||
|             out << "(assert "; | ||||
|              | ||||
|             // Handle the constraint type and expression
 | ||||
|             auto k = c.kind(); | ||||
|              | ||||
|             // Normal constraint with variables
 | ||||
|             switch (k) { | ||||
|             case LE: out << "(<= "; break; | ||||
|             case LT: out << "(< "; break; | ||||
|             case GE: out << "(>= "; break; | ||||
|             case GT: out << "(> "; break; | ||||
|             case EQ: out << "(= "; break; | ||||
|             default: out << "(unknown-constraint-type "; break; | ||||
|             } | ||||
|              | ||||
|             // Left-hand side (variables)
 | ||||
|             if (c.coeffs().size() == 1) { | ||||
|                 // Single variable
 | ||||
|                 auto p = *c.coeffs().begin(); | ||||
|                 if (p.first.is_one()) { | ||||
|                     out << "x" << p.second << " "; | ||||
|                 } else { | ||||
|                     out << "(* " << format_smt2_constant(p.first) << " x" << p.second << ") "; | ||||
|                 } | ||||
|             } else { | ||||
|                 // Multiple variables - create a sum
 | ||||
|                 out << "(+ "; | ||||
|                 for (auto const& p : c.coeffs()) { | ||||
|                     if (p.first.is_one()) { | ||||
|                         out << "x" << p.second << " "; | ||||
|                     } else { | ||||
|                         out << "(* " << format_smt2_constant(p.first) << " x" << p.second << ") "; | ||||
|                     } | ||||
|                 } | ||||
|                 out << ") "; | ||||
|             } | ||||
|              | ||||
|             // Right-hand side (constant)
 | ||||
|             out << format_smt2_constant(c.rhs()); | ||||
|             out << "))\n"; | ||||
|         } | ||||
|         out << "\n"; | ||||
|          | ||||
|         // Now add the assertion that contradicts the bound
 | ||||
|         out << "; Negation of the derived bound\n"; | ||||
|         if (is_low) { | ||||
|             if (is_strict) { | ||||
|                 out << "(assert (<= x" << j << " " << format_smt2_constant(bound_val) << "))\n"; | ||||
|             } else { | ||||
|                 out << "(assert (< x" << j << " " << format_smt2_constant(bound_val) << "))\n"; | ||||
|             } | ||||
|         } else { | ||||
|             if (is_strict) { | ||||
|                 out << "(assert (>= x" << j << " " << format_smt2_constant(bound_val) << "))\n"; | ||||
|             } else { | ||||
|                 out << "(assert (> x" << j << " " << format_smt2_constant(bound_val) << "))\n"; | ||||
|             } | ||||
|         } | ||||
|         out << "\n"; | ||||
|          | ||||
|         // Check sat and get model if available
 | ||||
|         out << "(check-sat)\n"; | ||||
|         out << "(exit)\n"; | ||||
|         } | ||||
| } // namespace lp
 | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -617,11 +617,12 @@ public: | |||
|     } | ||||
|     inline bool column_has_term(lpvar j) const { return m_columns[j].term() != nullptr; } | ||||
| 
 | ||||
|     std::ostream& print_column_info(unsigned j, std::ostream& out) const { | ||||
|     std::ostream& print_column_info(unsigned j, std::ostream& out, bool print_expl = false) const { | ||||
|         m_mpq_lar_core_solver.m_r_solver.print_column_info(j, out); | ||||
|         if (column_has_term(j))  | ||||
|             print_term_as_indices(get_term(j), out) << "\n";        | ||||
|         display_column_explanation(out, j); | ||||
|         if (print_expl) | ||||
|             display_column_explanation(out, j); | ||||
|         return out; | ||||
|     } | ||||
| 
 | ||||
|  | @ -630,10 +631,18 @@ public: | |||
|         svector<unsigned> vs1, vs2; | ||||
|         m_dependencies.linearize(ul.lower_bound_witness(), vs1); | ||||
|         m_dependencies.linearize(ul.upper_bound_witness(), vs2); | ||||
|         if (!vs1.empty()) | ||||
|             out << "lo: " << vs1; | ||||
|         if (!vs2.empty()) | ||||
|             out << "hi: " << vs2; | ||||
|         if (!vs1.empty()) { | ||||
|             out << " lo:\n"; | ||||
|             for (unsigned ci : vs1) { | ||||
|                 display_constraint(out, ci) << "\n"; | ||||
|             } | ||||
|         } | ||||
|         if (!vs2.empty()) { | ||||
|             out << " hi:\n"; | ||||
|             for (unsigned ci : vs2) { | ||||
|                 display_constraint(out, ci) << "\n"; | ||||
|             } | ||||
|         }      | ||||
|         if (!vs1.empty() || !vs2.empty()) | ||||
|             out << "\n"; | ||||
|         return out; | ||||
|  | @ -716,6 +725,11 @@ public: | |||
|             return 0; | ||||
|         return m_usage_in_terms[j]; | ||||
|     } | ||||
| 
 | ||||
|     void write_bound_lemma_to_file(unsigned j, bool is_low, const std::string & file_name, const std::string & location) const; | ||||
| 
 | ||||
|     void write_bound_lemma(unsigned j, bool is_low, const std::string & location, std::ostream & out) const; | ||||
| 
 | ||||
|     std::function<void (const indexed_uint_set& columns_with_changed_bound)> m_find_monics_with_changed_bounds_func = nullptr; | ||||
|     friend int_solver; | ||||
|     friend int_branch; | ||||
|  |  | |||
|  | @ -436,7 +436,7 @@ public: | |||
|         return out; | ||||
|     } | ||||
| 
 | ||||
|     std::ostream& print_column_info(unsigned j, std::ostream & out) const { | ||||
|     std::ostream& print_column_info(unsigned j, std::ostream & out, const std::string& var_prefix = "x") const { | ||||
|         if (j >= m_lower_bounds.size()) { | ||||
|             out << "[" << j << "] is not present\n"; | ||||
|             return out; | ||||
|  | @ -445,7 +445,7 @@ public: | |||
|         std::stringstream strm; | ||||
|         strm << m_x[j]; | ||||
|         std::string j_val = strm.str(); | ||||
|         out << "[" << j << "] " << std::setw(6) << " := " << j_val; | ||||
|         out << var_prefix << j << " = " << std::setw(6) << j_val; | ||||
|         if (m_basis_heading[j] >= 0) | ||||
|             out << " base "; | ||||
|         else  | ||||
|  |  | |||
|  | @ -35,4 +35,5 @@ void lp::lp_settings::updt_params(params_ref const& _p) { | |||
|     m_dio_eqs = p.arith_lp_dio_eqs(); | ||||
|     m_dio_enable_gomory_cuts = p.arith_lp_dio_cuts_enable_gomory(); | ||||
|     m_dio_branching_period = p.arith_lp_dio_branching_period(); | ||||
|     m_dump_bound_lemmas = p.arith_dump_bound_lemmas(); | ||||
| } | ||||
|  |  | |||
|  | @ -259,7 +259,7 @@ private: | |||
|     bool             m_dio_enable_hnf_cuts = true; | ||||
|     unsigned         m_dio_branching_period = 100; //  do branching rarely
 | ||||
|     unsigned         m_dio_report_branch_with_term_tigthening_period = 10000000; // period of reporting the branch with term tigthening
 | ||||
| 
 | ||||
|     bool             m_dump_bound_lemmas = false; | ||||
| public: | ||||
|     bool print_external_var_name() const { return m_print_external_var_name; } | ||||
|     bool propagate_eqs() const { return m_propagate_eqs;} | ||||
|  | @ -277,6 +277,8 @@ public: | |||
|         return m_bound_propagation; | ||||
|     } | ||||
| 
 | ||||
|     bool dump_bound_lemmas() { return m_dump_bound_lemmas; } | ||||
|      | ||||
|     bool& bound_propagation() { return m_bound_propagation; } | ||||
|      | ||||
|     lp_settings() : m_default_resource_limit(*this), | ||||
|  |  | |||
|  | @ -239,6 +239,8 @@ struct numeric_pair { | |||
|     void neg() { x.neg(); y.neg(); } | ||||
|      | ||||
|     std::string to_string() const { | ||||
|         if (y.is_zero()) | ||||
|             return T_to_string(x); | ||||
|         return std::string("(") + T_to_string(x) + ", "  + T_to_string(y) + ")"; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -93,16 +93,9 @@ public: | |||
|         operator T () const { return m_matrix.get_elem(m_row, m_col); } | ||||
|     }; | ||||
| 
 | ||||
|     class ref_row { | ||||
|         const static_matrix & m_matrix; | ||||
|         unsigned        m_row; | ||||
|     public: | ||||
|         ref_row(const static_matrix & m, unsigned row): m_matrix(m), m_row(row) {} | ||||
|         T operator[](unsigned col) const { return m_matrix.get_elem(m_row, col); } | ||||
|     }; | ||||
| 
 | ||||
| public: | ||||
| 
 | ||||
|     const auto & operator[](unsigned i) const { return m_rows[i]; } | ||||
|      | ||||
|     const T & get_val(const column_cell & c) const { | ||||
|         return m_rows[c.var()][c.offset()].coeff(); | ||||
|     } | ||||
|  | @ -145,6 +138,11 @@ public: | |||
|     void add_columns_up_to(unsigned j) { while (j >= column_count()) add_column(); } | ||||
| 
 | ||||
|     void remove_element(std_vector<row_cell<T>> & row, row_cell<T> & elem_to_remove); | ||||
| 
 | ||||
|     void remove_element(unsigned ei, row_cell<T> & elem_to_remove) { | ||||
|         remove_element(m_rows[ei], elem_to_remove); | ||||
|     } | ||||
|      | ||||
|      | ||||
|     void multiply_column(unsigned column, T const & alpha) { | ||||
|         for (auto & t : m_columns[column]) { | ||||
|  | @ -452,7 +450,6 @@ public: | |||
|         return column_container(j, *this); | ||||
|     } | ||||
| 
 | ||||
|     ref_row operator[](unsigned i) const { return ref_row(*this, i);} | ||||
|     typedef T coefftype; | ||||
|     typedef X argtype; | ||||
| }; | ||||
|  |  | |||
|  | @ -94,6 +94,7 @@ def_module_params(module_name='smt', | |||
|                           ('arith.int_eq_branch', BOOL, False, 'branching using derived integer equations'), | ||||
|                           ('arith.ignore_int', BOOL, False, 'treat integer variables as real'), | ||||
|                           ('arith.dump_lemmas', BOOL, False, 'dump arithmetic theory lemmas to files'), | ||||
|                           ('arith.dump_bound_lemmas', BOOL, False, 'dump linear solver bounds to files in smt2 format'),                           | ||||
|                           ('arith.greatest_error_pivot', BOOL, False, 'Pivoting strategy'), | ||||
|                           ('arith.eager_eq_axioms', BOOL, True, 'eager equality axioms'), | ||||
|                           ('arith.auto_config_simplex', BOOL, False, 'force simplex solver in auto_config'), | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue