dlinear  0.0.1
Delta-complete SMT solver for linear programming
Loading...
Searching...
No Matches
NNSoplexTheorySolver.cpp
1
6#include "NNSoplexTheorySolver.h"
7
8#include "dlinear/util/exception.h"
9#include "dlinear/util/logging.h"
10
11namespace dlinear {
12
13using soplex::Rational;
14using SoplexStatus = soplex::SPxSolver::Status;
15
16NNSoplexTheorySolver::NNSoplexTheorySolver(PredicateAbstractor &predicate_abstractor, const std::string &class_name)
17 : SoplexTheorySolver(predicate_abstractor, class_name),
18 pl_theory_lit_{},
19 enabled_pl_constraints_{},
20 pl_explanations_{},
21 soi_{0} {
22 spx_.setIntParam(soplex::SoPlex::OBJSENSE, soplex::SoPlex::OBJSENSE_MINIMIZE);
23}
24
25void NNSoplexTheorySolver::SetPiecewiseLinearConstraints(
26 const std::vector<std::unique_ptr<PiecewiseLinearConstraint>> &relu_constraints) {
27 for (const auto &relu_constraint : relu_constraints) {
28 pl_theory_lit_.emplace(relu_constraint->active_var(), relu_constraint.get());
29 pl_theory_lit_.emplace(relu_constraint->inactive_var(), relu_constraint.get());
30 // fmt::println("Adding relu constraint: {} - {}", relu_constraint->active_var(),
31 // relu_constraint->inactive_var());
32 }
33}
34
35void NNSoplexTheorySolver::AddLiteral(const Variable &formula_var, const Formula &formula) {
36 DLINEAR_ASSERT(!is_consolidated_, "Cannot add literals after consolidation");
37 // Literal is already present
38 if (lit_to_theory_row_.contains(formula_var.get_id())) return;
39 // Literal will be handled by the ReLU SOI
40 // if (relu_theory_lit_.contains(formula_var)) return;
41 DLINEAR_TRACE_FMT("NNSoplexTheorySolver::AddLiteral({})", formula);
42
43 // Create the LP solver variables
44 for (const Variable &var : formula.GetFreeVariables()) AddVariable(var);
45
46 spx_sense_.emplace_back(~parseLpSense(formula));
47
48 const int spx_row{spx_rows_.num()};
49
50 const bool is_simple_bound = BoundPreprocessor::IsSimpleBound(formula);
51 soplex::DSVectorRational coeffs{is_simple_bound ? soplex::DSVectorRational{} : ParseRowCoeff(formula)};
52 if (is_simple_bound) spx_rhs_.emplace_back(0);
53 spx_rows_.add(soplex::LPRowRational(-soplex::infinity, coeffs, soplex::infinity));
54 if (2 == config_.simplex_sat_phase()) CreateArtificials(spx_row);
55
56 // Update indexes
57 lit_to_theory_row_.emplace(formula_var.get_id(), spx_row);
58 theory_row_to_lit_.emplace_back(formula_var, true);
59
60 DLINEAR_ASSERT(static_cast<std::size_t>(spx_row) == theory_row_to_lit_.size() - 1,
61 "incorrect theory_row_to_lit_.size()");
62 DLINEAR_ASSERT(static_cast<std::size_t>(spx_row) == spx_sense_.size() - 1, "incorrect spx_sense_.size()");
63 DLINEAR_ASSERT(static_cast<std::size_t>(spx_row) == spx_rhs_.size() - 1, "incorrect spx_rhs_.size()");
64 DLINEAR_DEBUG_FMT("NNSoplexTheorySolver::AddLiteral: {} ↦ {}", formula, spx_row);
65}
66
67TheorySolver::Explanations NNSoplexTheorySolver::EnableLiteral(const Literal &lit) {
68 DLINEAR_ASSERT(is_consolidated_, "The solver must be consolidate before enabling a literal");
69 DLINEAR_ASSERT(predicate_abstractor_.var_to_formula_map().contains(lit.var), "var must map to a theory literal");
70
71 const auto &[var, truth] = lit;
72 DLINEAR_DEV_FMT("Enabled Literal {}", lit);
73
74 // Literal will be handled by the ReLU SOI
75 if (pl_theory_lit_.contains(var)) {
76 const PiecewiseLinearConstraint &constraint = *pl_theory_lit_.at(var);
77 // No point in adding both sides of the piecewise constraint. Just consider the true one
78 if (!truth) return {};
79 // fmt::println("Enabled ReLU literal: ({}) {} -> {}", constraint.fixed(), lit, constraint);
80 // If the constraint is fixed, add it as a proper LP constraint.
81 // Otherwise, use it to update the Sum of Infeasibility
82 if (!constraint.fixed()) {
83 const bool is_active = constraint.active_var().equal_to(var);
84 enabled_pl_constraints_.emplace_back(&constraint, is_active, false);
85 DLINEAR_DEV_FMT("ENABLED {} = fixed? {}\nIs active?: {}\n{}", lit, constraint.fixed(),
86 enabled_pl_constraints_.back().active, constraint);
87 soi_ += is_active ? constraint.active_soi() : constraint.inactive_soi();
88 return {};
89 }
90 }
91
92 Explanations explanations{preprocessor_.EnableLiteral(lit)};
93 if (!explanations.empty()) return explanations;
94
95 const auto it = lit_to_theory_row_.find(var.get_id());
96 // If the literal was not already among the constraints (rows) of the LP, it must be a learned literal.
97 if (it == lit_to_theory_row_.end()) {
98 DLINEAR_TRACE_FMT("NNSoplexTheorySolver::EnableLinearLiteral: ignoring ({}{})", truth ? "" : "¬", var);
99 return {};
100 }
101
102 // A non-trivial linear literal from the input problem
103 const int spx_row = it->second;
104 // Update the truth value for the current iteration with the last SAT solver assignment
105 theory_row_to_lit_[spx_row].truth = truth;
106
107 DLINEAR_TRACE_FMT("NNSoplexTheorySolver::EnableLinearLiteral({})", lit);
108
109 EnableSpxRow(spx_row, truth);
110 // fmt::println("Enabled literal: {}", lit);
111 return explanations;
112}
113
114SatResult NNSoplexTheorySolver::CheckSatCore(mpq_class *actual_precision, std::set<LiteralSet> &explanations) {
115 // fmt::println("NN Soplex Theory Solver: SOI = {}", soi_);
116 // Use the Sum of Infeasibility expression built so far to create the objective function of the optimization
117 // with the goal of minimizing it
118 SoiToObjFunction();
119 // Set the bounds for the variables
120 EnableSpxVarBound();
121 // Remove all the disabled rows from the LP solver
122 DisableSpxRows();
123
124 // Now we call the solver
125 DLINEAR_DEBUG_FMT("NNSoplexTheorySolver::CheckSat: calling SoPlex (phase {})", config_.simplex_sat_phase());
126
127 // While the LP problem is not solved (either feasible with objective value <= precision or infeasible)
128 while (true) {
129 const std::size_t prev_size = explanations.size();
130
131 // Try to solve the LP problem.
132 switch (SpxCheckSat(actual_precision)) {
133 case SpxCheckSatResult::SAT: // A solution with the desired precision has been found
134 UpdateModelSolution();
135 DLINEAR_DEBUG("NNSoplexTheorySolver::CheckSat: returning sat");
136 return SatResult::SAT_DELTA_SATISFIABLE;
137 case SpxCheckSatResult::INFEASIBLE: // The constraints are infeasible
138 UpdateExplanations(explanations);
139 DLINEAR_DEBUG("NNSoplexTheorySolver::CheckSat: returning unsat");
140 return SatResult::SAT_UNSATISFIABLE;
141 case SpxCheckSatResult::SOI_VIOLATION: // The SOI constraint has been violated, invert a piecewise constraint
142 // TODO(tend): temporary solution to avoid looping. Check if the explanation was added
143 UpdateExplanationsWithCurrentPiecewiseLinearLiterals(explanations);
144 // Tried all sub-problems, just return UNSAT and start over
145 if (explanations.size() == prev_size || !InvertGreatestViolation()) {
146 DLINEAR_DEBUG("NNSoplexTheorySolver::CheckSat: returning unsat");
147 return SatResult::SAT_UNSATISFIABLE;
148 }
149 break;
150 default:
151 DLINEAR_UNREACHABLE();
152 }
153 }
154}
155
156NNSoplexTheorySolver::SpxCheckSatResult NNSoplexTheorySolver::SpxCheckSat(mpq_class *actual_precision) {
157#if 0
158 spx_.writeFile("/home/campus.ncl.ac.uk/c3054737/Programming/phd/dlinear/dump.lp");
159#endif
160 SoplexStatus status = spx_.optimize();
161 Rational max_violation, sum_violation;
162
163 // The status must be OPTIMAL, UNBOUNDED, or INFEASIBLE. Anything else is an error
164 if (status != SoplexStatus::OPTIMAL && status != SoplexStatus::UNBOUNDED && status != SoplexStatus::INFEASIBLE) {
165 DLINEAR_RUNTIME_ERROR_FMT("SoPlex returned {}. That's not allowed here", status);
166 } else if (spx_.getRowViolationRational(max_violation, sum_violation)) {
167 DLINEAR_DEBUG_FMT("DeltaSoplexTheorySolver::CheckSat: SoPlex returned {}, precision = {}", status, max_violation);
168 } else {
169 DLINEAR_DEBUG_FMT("NNSoplexTheorySolver::CheckSat: SoPlex has returned {}, but no precision", status);
170 }
171
172 // Add the precision (delta) to the expected violation
173 const mpq_class violation_rhs{is_addition(soi_) ? -get_constant_in_addition(soi_) + *actual_precision
174 : *actual_precision};
175 switch (status) {
176 case SoplexStatus::OPTIMAL:
177 fmt::println("Objective value: {}", gmp::ToMpqClass(spx_.objValueRational().backend().data()).get_d());
178 return spx_.objValueRational() <= violation_rhs.get_mpq_t() ? SpxCheckSatResult::SAT
179 : SpxCheckSatResult::SOI_VIOLATION;
180 case SoplexStatus::INFEASIBLE:
181 return SpxCheckSatResult::INFEASIBLE;
182 default:
183 DLINEAR_UNREACHABLE();
184 }
185}
186
187bool NNSoplexTheorySolver::InvertGreatestViolation() {
188 const int colcount = spx_.numColsRational();
189 soplex::VectorRational x;
190 x.reDim(colcount);
191 [[maybe_unused]] const bool has_sol = spx_.getPrimalRational(x);
192 DLINEAR_ASSERT(has_sol, "has_sol must be true");
193 DLINEAR_ASSERT(x.dim() >= colcount, "x.dim() must be >= colcount");
194 Environment env;
195 for (int theory_col = 0; theory_col < static_cast<int>(theory_col_to_var_.size()); theory_col++) {
196 const Variable &var{theory_col_to_var_[theory_col]};
197 env[var] = gmp::ToMpqClass(x[theory_col].backend().data());
198 }
199
200 // fmt::println("x: {}", x);
201 mpq_class max_cost{0};
202 std::size_t max_cost_index{0};
203 for (std::size_t i = 0; i < enabled_pl_constraints_.size(); i++) {
204 const mpq_class cost{enabled_pl_constraints_[i].constraint->Cost(env, enabled_pl_constraints_[i].active)};
205 // fmt::println("Cost of {}: {}", *enabled_pl_constraints_[i], cost.get_d());
206 if (cost > max_cost) {
207 max_cost = cost;
208 max_cost_index = i;
209 }
210 }
211
212 // fmt::println("Max cost {} belongs to constraint {}", max_cost,
213 // *enabled_pl_constraints_[max_cost_index].constraint);
214 DLINEAR_ASSERT(max_cost > 0, "max_cost must be > 0");
215
216 const auto [constraint_ptr, active, fixed] = enabled_pl_constraints_[max_cost_index];
217
218 if (fixed) {
219 DLINEAR_ERROR("NNSoplexTheorySolver::InvertGreatestViolation: max cost belongs to a fixed constraint");
220 return false;
221 }
222 enabled_pl_constraints_[max_cost_index].fixed = true;
223
224 const PiecewiseLinearConstraint &constraint{*enabled_pl_constraints_[max_cost_index].constraint};
225 // Update the Sum of Infeasibilities by removing the old soi cost and adding the new one
226 soi_ += active ? -constraint.active_soi() + constraint.inactive_soi()
227 : -constraint.inactive_soi() + constraint.active_soi();
228
229 enabled_pl_constraints_[max_cost_index].active = !active;
230
231 // TODO(tend): this could be made more efficient by selectively changing the objective function coefficients
232 SoiToObjFunction();
233 // Related to issue
234 spx_.clearBasis();
235 return true;
236}
237
238void NNSoplexTheorySolver::UpdateExplanationsWithCurrentPiecewiseLinearLiterals(std::set<LiteralSet> &explanations) {
239 LiteralSet explanation;
240 for (const PlConstraint &enabled_pl_constraint : enabled_pl_constraints_) {
241 explanation.emplace(enabled_pl_constraint.active ? enabled_pl_constraint.constraint->active_var()
242 : enabled_pl_constraint.constraint->inactive_var(),
243 true);
244 DLINEAR_DEV_FMT("addidg {} to explanation", enabled_pl_constraint.active
245 ? enabled_pl_constraint.constraint->active_var()
246 : enabled_pl_constraint.constraint->inactive_var());
247 }
248 if (explanations.contains(explanation)) return;
249 pl_explanations_.emplace(explanation);
250 explanations.emplace(explanation);
251}
252
253void NNSoplexTheorySolver::SoiToObjFunction() {
254 DLINEAR_DEBUG_FMT("NNSoplexTheorySolver::SoiToObjFunction: soi_ = {}", soi_);
255 if (is_variable(soi_)) {
256 for (std::size_t i = 0; i < theory_col_to_var_.size(); i++) {
257 const Variable &var{theory_col_to_var_[i]};
258 spx_.changeObjRational(static_cast<int>(i), var.equal_to(get_variable(soi_)) ? Rational(1) : Rational(0));
259 }
260 } else if (is_multiplication(soi_)) {
261 for (std::size_t i = 0; i < theory_col_to_var_.size(); i++) {
262 const Variable &var{theory_col_to_var_[i]};
263 const auto it = get_base_to_exponent_map_in_multiplication(soi_).find(var);
264 spx_.changeObjRational(static_cast<int>(i), it == get_base_to_exponent_map_in_multiplication(soi_).end()
265 ? Rational(0)
266 : Rational(get_constant_in_multiplication(soi_).get_mpq_t()));
267 }
268 } else if (is_addition(soi_)) {
269 for (std::size_t i = 0; i < theory_col_to_var_.size(); i++) {
270 const Variable &var{theory_col_to_var_[i]};
271 const auto it = get_expr_to_coeff_map_in_addition(soi_).find(var);
272 spx_.changeObjRational(static_cast<int>(i), it == get_expr_to_coeff_map_in_addition(soi_).end()
273 ? Rational(0)
274 : Rational(it->second.get_mpq_t()));
275 }
276 }
277}
278
279void NNSoplexTheorySolver::Reset() {
280 SoplexTheorySolver::Reset();
281 soi_ = 0;
282 enabled_pl_constraints_.clear();
283}
284
285void NNSoplexTheorySolver::Consolidate(const Box &box) {
286 if (is_consolidated_) return;
287 enabled_pl_constraints_.reserve(pl_theory_lit_.size() / 2);
288 SoplexTheorySolver::Consolidate(box);
289}
290
291void NNSoplexTheorySolver::EnableSpxRow(int spx_row, bool truth) {
292 // Get the correct sense for the row, inverting it if the literals is false
293 const LpRowSense sense = truth ? spx_sense_[spx_row] : -spx_sense_[spx_row];
294 if (sense == LpRowSense::NQ) return;
295
296 const mpq_class &rhs{spx_rhs_[spx_row]};
297 spx_.changeRangeRational(
298 spx_row,
299 sense == LpRowSense::GE || sense == LpRowSense::EQ ? Rational(gmp::ToMpq(rhs)) : Rational(-soplex::infinity),
300 sense == LpRowSense::LE || sense == LpRowSense::EQ ? Rational(gmp::ToMpq(rhs)) : Rational(soplex::infinity));
301 theory_rows_state_.at(spx_row) = true;
302 DLINEAR_ASSERT(truth == theory_row_to_lit_[spx_row].truth, "truth must be equal to stored lit truth");
303 DLINEAR_TRACE_FMT("NNSoplexTheorySolver::EnableLinearLiteral({} {} {})", theory_row_to_lit_[spx_row], sense, rhs);
304}
305} // namespace dlinear
Collection of variables with associated intervals.
Definition Box.h:31
A piecewise linear constraint is a constraint that assumes different linear functions depending on th...
bool fixed() const
Check whether the constraint is fixed.
const Variable & active_var() const
Get read-only access to the boolean variable associated with the theory literal of the constraint.
const Expression & active_soi() const
Get read-only access to the expression of the constraint.
const Expression & inactive_soi() const
Get read-only access to the expression of the constraint.
std::set< LiteralSet > Explanations
Set of explanations of the conflict.
Represents a symbolic environment (mapping from a variable to a value).
Represents a symbolic form of a first-order logic formula.
const Variables & GetFreeVariables() const
Gets free variables (unquantified variables).
Represents a symbolic variable.
bool equal_to(const Variable &v) const
Checks the equality of two variables based on their ID values.
Global namespace for the dlinear library.
SatResult
Result of running the theory solver over the literals decided by the SAT solver.
Definition SatResult.h:17
std::set< Literal > LiteralSet
A set of literals.
Definition literal.h:28
LpRowSense parseLpSense(const Formula &f)
Parse the sense from a formula.
LpRowSense
Sense of a linear programming row describing a constraint.
Definition LpRowSense.h:23
A literal is a variable with an associated truth value, indicating whether it is true or false.
Definition literal.h:24
Variable var
Variable.
Definition literal.h:25