2 * @file CSplitModule.cpp
3 * @author Ă–mer Sali <oemer.sali@rwth-aachen.de>
6 * Created on 2017-11-01.
9 #include "CSplitModule.h"
11 #include <carl-arith/interval/SetTheory.h>
15 template<class Settings>
16 CSplitModule<Settings>::CSplitModule(const ModuleInput* _formula, Conditionals& _conditionals, Manager* _manager)
17 : Module( _formula, _conditionals, _manager )
23 , mCheckedWithBackends(false)
26 template<class Settings>
27 bool CSplitModule<Settings>::addCore(ModuleInput::const_iterator _subformula)
29 addReceivedSubformulaToPassedFormula(_subformula);
30 const FormulaT& formula{_subformula->formula()};
31 if (formula.type() == carl::FormulaType::FALSE)
32 mInfeasibleSubsets.push_back({formula});
33 else if (formula.is_bound())
35 /// Update the variable domain with the asserted bound
36 mVariableBounds.addBound(formula, formula);
37 const carl::Variable& variable{*formula.variables().begin()};
38 auto expansionIter{mExpansions.firstFind(variable)};
39 if (expansionIter == mExpansions.end())
40 expansionIter = mExpansions.emplace(variable);
41 expansionIter->mChangedBounds = true;
42 if (mVariableBounds.isConflicting())
43 mInfeasibleSubsets.emplace_back(mVariableBounds.getConflict());
45 else if (formula.type() == carl::FormulaType::CONSTRAINT)
47 /// Normalize the left hand side of the constraint and turn the relation accordingly
48 const ConstraintT& constraint{formula.constraint()};
49 const Poly normalization{constraint.lhs().normalize()};
50 carl::Relation relation{constraint.relation()};
51 if (carl::is_negative(constraint.lhs().lcoeff()))
52 relation = carl::turn_around(relation);
54 /// Purifiy and discretize the normalized left hand side to construct the linearization
55 auto linearizationIter{mLinearizations.firstFind(normalization)};
56 if (linearizationIter == mLinearizations.end())
59 std::vector<Purification *> purifications;
60 bool hasRealVariables{false};
61 for (TermT term : normalization)
63 if (!term.is_constant())
65 size_t realVariables{0};
66 for (const auto& exponent : term.monomial()->exponents())
67 if (exponent.first.type() == carl::VariableType::VT_REAL)
68 realVariables += exponent.second;
71 term.coeff() /= carl::pow(Rational(Settings::discrDenom), realVariables);
72 hasRealVariables = true;
75 if (!term.is_linear())
77 Purification& purification{mPurifications[term.monomial()]};
78 purifications.emplace_back(&purification);
79 term = term.coeff()*purification.mSubstitutions[0];
81 else if (realVariables)
83 const carl::Variable variable{term.single_variable()};
84 auto expansionIter{mExpansions.firstFind(variable)};
85 if (expansionIter == mExpansions.end())
86 expansionIter = mExpansions.emplace(variable);
87 term = term.coeff()*expansionIter->mQuotients[0];
90 discretization += term;
92 linearizationIter = mLinearizations.emplace(normalization, std::move(discretization.normalize()), std::move(purifications), hasRealVariables);
94 Linearization& linearization{*linearizationIter};
95 propagateFormula(FormulaT(linearization.mLinearization, relation), true);
96 if (linearization.mRelations.empty())
97 for (Purification *purification : linearization.mPurifications)
98 ++purification->mUsage;
99 linearization.mRelations.emplace(relation);
101 /// Check if the asserted relation trivially conflicts with other asserted relations
104 case carl::Relation::EQ:
105 if (linearization.mRelations.count(carl::Relation::NEQ))
106 mInfeasibleSubsets.push_back({
107 FormulaT(normalization, carl::Relation::EQ),
108 FormulaT(normalization, carl::Relation::NEQ)
110 if (linearization.mRelations.count(carl::Relation::LESS))
111 mInfeasibleSubsets.push_back({
112 FormulaT(normalization, carl::Relation::EQ),
113 FormulaT(normalization, carl::Relation::LESS)
115 if (linearization.mRelations.count(carl::Relation::GREATER))
116 mInfeasibleSubsets.push_back({
117 FormulaT(normalization, carl::Relation::EQ),
118 FormulaT(normalization, carl::Relation::GREATER)
121 case carl::Relation::NEQ:
122 if (linearization.mRelations.count(carl::Relation::EQ))
123 mInfeasibleSubsets.push_back({
124 FormulaT(normalization, carl::Relation::NEQ),
125 FormulaT(normalization, carl::Relation::EQ)
128 case carl::Relation::LESS:
129 if (linearization.mRelations.count(carl::Relation::EQ))
130 mInfeasibleSubsets.push_back({
131 FormulaT(normalization, carl::Relation::LESS),
132 FormulaT(normalization, carl::Relation::EQ)
134 if (linearization.mRelations.count(carl::Relation::GEQ))
135 mInfeasibleSubsets.push_back({
136 FormulaT(normalization, carl::Relation::LESS),
137 FormulaT(normalization, carl::Relation::GEQ)
140 case carl::Relation::LEQ:
141 if (linearization.mRelations.count(carl::Relation::GREATER))
142 mInfeasibleSubsets.push_back({
143 FormulaT(normalization, relation),
144 FormulaT(normalization, carl::Relation::GREATER)
147 case carl::Relation::GREATER:
148 if (linearization.mRelations.count(carl::Relation::EQ))
149 mInfeasibleSubsets.push_back({
150 FormulaT(normalization, carl::Relation::GREATER),
151 FormulaT(normalization, carl::Relation::EQ)
153 if (linearization.mRelations.count(carl::Relation::LEQ))
154 mInfeasibleSubsets.push_back({
155 FormulaT(normalization, carl::Relation::GREATER),
156 FormulaT(normalization, carl::Relation::LEQ)
159 case carl::Relation::GEQ:
160 if (linearization.mRelations.count(carl::Relation::LESS))
161 mInfeasibleSubsets.push_back({
162 FormulaT(normalization, relation),
163 FormulaT(normalization, carl::Relation::LESS)
170 return mInfeasibleSubsets.empty();
173 template<class Settings>
174 void CSplitModule<Settings>::removeCore(ModuleInput::const_iterator _subformula)
176 const FormulaT& formula{_subformula->formula()};
177 if (formula.is_bound())
179 /// Update the variable domain with the removed bound
180 mVariableBounds.removeBound(formula, formula);
181 mExpansions.firstAt(*formula.variables().begin()).mChangedBounds = true;
183 else if (formula.type() == carl::FormulaType::CONSTRAINT)
185 /// Normalize the left hand side of the constraint and turn the relation accordingly
186 const ConstraintT& constraint{formula.constraint()};
187 const Poly normalization{constraint.lhs().normalize()};
188 carl::Relation relation{constraint.relation()};
189 if (carl::is_negative(constraint.lhs().lcoeff()))
190 relation = carl::turn_around(relation);
192 /// Retrieve the normalized constraint and mark the separator object as changed
193 Linearization& linearization{mLinearizations.firstAt(normalization)};
194 propagateFormula(FormulaT(linearization.mLinearization, relation), false);
195 linearization.mRelations.erase(relation);
196 if (linearization.mRelations.empty())
197 for (Purification *purification : linearization.mPurifications)
198 ++purification->mUsage;
202 template<class Settings>
203 void CSplitModule<Settings>::updateModel() const
208 if (mCheckedWithBackends)
211 excludeNotReceivedVariablesFromModel();
215 for (const Expansion& expansion : mExpansions)
216 if (receivedVariable(expansion.mRationalization))
218 Rational value{mLRAModel.at(expansion.mDiscretization).asRational()};
219 if (expansion.mRationalization.type() == carl::VariableType::VT_REAL)
220 value /= Settings::discrDenom;
221 mModel.emplace(expansion.mRationalization, value);
224 mModelComputed = true;
228 template<class Settings>
229 Answer CSplitModule<Settings>::checkCore()
231 /// Report unsatisfiability if the already found conflicts are still unresolved
232 if (!mInfeasibleSubsets.empty())
233 return Answer::UNSAT;
235 /// Apply the method only if the asserted formula is not trivially undecidable
236 if (rReceivedFormula().is_constraint_conjunction())
239 if (resetExpansions())
241 mCheckedWithBackends = false;
242 for (size_t i = 1; i <= Settings::maxIter; ++i)
243 if (mLRAModule.check(true) == Answer::SAT)
245 mLRAModel = mLRAModule.model();
251 FormulaSetT conflict{mLRAModule.infeasibleSubsets()[0]};
252 if (bloatDomains(conflict))
255 return analyzeConflict(conflict);
262 /// Check the asserted formula with the backends
263 mCheckedWithBackends = true;
264 Answer answer{runBackends()};
265 if (answer == Answer::UNSAT)
266 getInfeasibleSubsets();
271 template<class Settings>
272 bool CSplitModule<Settings>::resetExpansions()
274 /// Update the variable domains and watch out for discretization conflicts
275 for (Expansion& expansion : mExpansions)
277 RationalInterval& maximalDomain{expansion.mMaximalDomain};
278 if (expansion.mChangedBounds)
280 maximalDomain = mVariableBounds.getInterval(expansion.mRationalization);
281 if (expansion.mRationalization.type() == carl::VariableType::VT_REAL)
282 maximalDomain *= Rational(Settings::discrDenom);
283 maximalDomain.integralPart_assign();
284 if (expansion.mMaximalDomain.is_unbounded())
285 expansion.mMaximalDomainSize = DomainSize::UNBOUNDED;
286 else if (expansion.mMaximalDomain.diameter() > Settings::maxDomainSize)
287 expansion.mMaximalDomainSize = DomainSize::LARGE;
289 expansion.mMaximalDomainSize = DomainSize::SMALL;
290 expansion.mChangedBounds = false;
292 if (maximalDomain.is_empty())
294 expansion.mActiveDomain = RationalInterval::empty_interval();
295 expansion.mPurifications.clear();
298 /// Activate all used purifications bottom-up
299 for (auto purificationIter = mPurifications.begin(); purificationIter != mPurifications.end(); ++purificationIter)
301 Purification& purification{purificationIter->second};
302 if (purification.mUsage)
304 carl::Monomial::Arg monomial{purificationIter->first};
306 /// Find set of variables with maximal domain size
307 carl::Variables maxVariables;
308 DomainSize maxDomainSize{DomainSize::SMALL};
309 for (const auto& exponent : monomial->exponents())
311 const carl::Variable& variable{exponent.first};
312 auto expansionIter{mExpansions.firstFind(variable)};
313 if (expansionIter == mExpansions.end())
314 expansionIter = mExpansions.emplace(variable);
315 Expansion& expansion{*expansionIter};
317 if (maxDomainSize <= expansion.mMaximalDomainSize)
319 if (maxDomainSize < expansion.mMaximalDomainSize)
321 maxVariables.clear();
322 maxDomainSize = expansion.mMaximalDomainSize;
324 maxVariables.emplace(variable);
328 /// Find a locally optimal reduction for the monomial
329 const auto isReducible = [&](const auto& purificationsEntry) {
330 return purificationsEntry.second.mActive
331 && monomial->divisible(purificationsEntry.first)
333 maxVariables.begin(),
335 [&](const carl::Variable& variable) {
336 return purificationsEntry.first->has(variable);
340 auto reductionIter{std::find_if(std::make_reverse_iterator(purificationIter), mPurifications.rend(), isReducible)};
342 /// Activate the sequence of reductions top-down
343 carl::Monomial::Arg guidance;
344 if (reductionIter == mPurifications.rend())
345 monomial->divide(*maxVariables.begin(), guidance);
347 monomial->divide(reductionIter->first, guidance);
348 auto hintIter{purificationIter};
349 for (const auto& exponentPair : guidance->exponents())
351 const carl::Variable& variable{exponentPair.first};
352 Expansion& expansion{mExpansions.firstAt(variable)};
353 for (carl::exponent exponent = 1; exponent <= exponentPair.second; ++exponent)
355 hintIter->second.mActive = true;
356 expansion.mPurifications.emplace_back(&hintIter->second);
357 monomial->divide(variable, monomial);
358 if (monomial->isAtMostLinear())
359 hintIter->second.mReduction = mExpansions.firstAt(monomial->single_variable()).mQuotients[0];
362 auto temp{mPurifications.emplace_hint(hintIter, std::piecewise_construct, std::make_tuple(monomial), std::make_tuple())};
363 hintIter->second.mReduction = temp->second.mSubstitutions[0];
370 purification.mActive = false;
373 /// Activate expansions that are used for case splits and deactivate them otherwise
374 for (Expansion& expansion : mExpansions)
376 /// Calculate the center point where the initial domain is located
377 expansion.mNucleus = 0;
378 if (expansion.mMaximalDomain.lower_bound_type() != carl::BoundType::INFTY
379 && expansion.mNucleus < expansion.mMaximalDomain.lower())
380 expansion.mNucleus = expansion.mMaximalDomain.lower();
381 else if (expansion.mMaximalDomain.upper_bound_type() != carl::BoundType::INFTY
382 && expansion.mNucleus > expansion.mMaximalDomain.upper())
383 expansion.mNucleus = expansion.mMaximalDomain.upper();
385 /// Calculate and activate the corresponding domain
386 RationalInterval domain(0, 1);
387 domain.mul_assign(carl::Interval<Rational>(Rational(Settings::initialRadius)));
388 domain.add_assign(carl::Interval<Rational>(expansion.mNucleus));
389 domain = carl::set_intersection(domain, expansion.mMaximalDomain);
390 changeActiveDomain(expansion, std::move(domain));
396 template<class Settings>
397 bool CSplitModule<Settings>::bloatDomains(const FormulaSetT& LRAConflict)
399 /// Data structure for potential bloating candidates
402 Expansion& mExpansion;
403 const Rational mDirection;
404 const Rational mRadius;
406 Candidate(Expansion& expansion, Rational&& direction, Rational&& radius)
407 : mExpansion(expansion)
408 , mDirection(std::move(direction))
409 , mRadius(std::move(radius))
412 bool operator<(const Candidate& rhs) const
414 if (carl::is_one(mDirection*rhs.mDirection))
415 return mRadius < rhs.mRadius;
416 else if (carl::is_one(mDirection))
417 return mRadius < Rational(Settings::thresholdRadius);
419 return rhs.mRadius >= Rational(Settings::thresholdRadius);
422 std::set<Candidate> candidates;
424 /// Scan the infeasible subset of the LRA solver for potential candidates
425 for (const FormulaT& formula : LRAConflict)
426 if (formula.is_bound())
428 const ConstraintT& constraint{formula.constraint()};
429 const carl::Variable variable{constraint.variables().as_vector().front()};
430 auto expansionIter{mExpansions.secondFind(variable)};
431 if (expansionIter != mExpansions.end())
433 Expansion& expansion{*expansionIter};
435 if (carl::is_lower_bound(constraint)
436 && (expansion.mMaximalDomain.lower_bound_type() == carl::BoundType::INFTY
437 || expansion.mMaximalDomain.lower() < expansion.mActiveDomain.lower()))
439 else if (carl::is_upper_bound(constraint)
440 && (expansion.mMaximalDomain.upper_bound_type() == carl::BoundType::INFTY
441 || expansion.mMaximalDomain.upper() > expansion.mActiveDomain.upper()))
443 if (!carl::is_zero(direction))
445 Rational radius{(direction*(expansion.mActiveDomain-expansion.mNucleus)).upper()};
446 if (radius <= Settings::maximalRadius)
448 candidates.emplace(expansion, std::move(direction), std::move(radius));
449 if (candidates.size() > Settings::maxBloatedDomains)
450 candidates.erase(std::prev(candidates.end()));
456 /// Change the active domain of the candidates with highest priority
457 for (const Candidate& candidate : candidates)
459 RationalInterval domain;
460 if (candidate.mRadius <= Settings::thresholdRadius)
461 domain = RationalInterval(0, 1);
462 else if (candidate.mExpansion.mPurifications.empty())
463 domain = RationalInterval(0, carl::BoundType::WEAK, 0, carl::BoundType::INFTY);
465 domain = RationalInterval(0, candidate.mRadius);
466 domain.mul_assign(carl::Interval<Rational>(candidate.mDirection));
467 domain.add_assign(candidate.mExpansion.mActiveDomain);
468 domain = carl::set_intersection(domain, candidate.mExpansion.mMaximalDomain);
469 changeActiveDomain(candidate.mExpansion, std::move(domain));
472 /// Report if any variable domain was bloated
473 return candidates.empty();
476 template<class Settings>
477 Answer CSplitModule<Settings>::analyzeConflict(const FormulaSetT& LRAConflict)
479 FormulaSetT infeasibleSubset;
480 for (const FormulaT& formula : LRAConflict)
482 if (formula.is_bound())
484 auto expansionIter{mExpansions.secondFind(*formula.variables().begin())};
485 if (expansionIter != mExpansions.end())
487 const Expansion& expansion{*expansionIter};
488 if (expansion.mRationalization.type() == carl::VariableType::VT_REAL
489 || expansion.mMaximalDomain != expansion.mActiveDomain)
490 return Answer::UNKNOWN;
493 FormulaSetT boundOrigins{mVariableBounds.getOriginSetOfBounds(expansion.mRationalization)};
494 infeasibleSubset.insert(boundOrigins.begin(), boundOrigins.end());
498 else if (formula.type() == carl::FormulaType::CONSTRAINT)
500 const ConstraintT& constraint{formula.constraint()};
501 auto linearizationIter{mLinearizations.secondFind(constraint.lhs().normalize())};
502 if (linearizationIter != mLinearizations.end())
504 const Linearization& linearization{*linearizationIter};
505 if (linearization.mHasRealVariables)
506 return Answer::UNKNOWN;
509 carl::Relation relation{constraint.relation()};
510 if (carl::is_negative(constraint.lhs().lcoeff()))
511 relation = carl::turn_around(relation);
512 infeasibleSubset.emplace(linearization.mNormalization, relation);
517 mInfeasibleSubsets.emplace_back(std::move(infeasibleSubset));
518 return Answer::UNSAT;
521 template<class Settings>
522 void CSplitModule<Settings>::changeActiveDomain(Expansion& expansion, RationalInterval&& domain)
524 RationalInterval activeDomain{std::move(expansion.mActiveDomain)};
525 expansion.mActiveDomain = domain;
527 /// Update the variable bounds
528 if (!activeDomain.is_empty())
530 if (activeDomain.lower_bound_type() != carl::BoundType::INFTY
531 && (domain.lower_bound_type() == carl::BoundType::INFTY
532 || domain.lower() != activeDomain.lower()
533 || domain.is_empty()))
534 propagateFormula(FormulaT(expansion.mQuotients[0]-Poly(activeDomain.lower()), carl::Relation::GEQ), false);
535 if (activeDomain.upper_bound_type() != carl::BoundType::INFTY
536 && (domain.upper_bound_type() == carl::BoundType::INFTY
537 || domain.upper() != activeDomain.upper()
538 || domain.is_empty()))
539 propagateFormula(FormulaT(expansion.mQuotients[0]-Poly(activeDomain.upper()), carl::Relation::LEQ), false);
541 if (!domain.is_empty())
543 if (domain.lower_bound_type() != carl::BoundType::INFTY
544 && (activeDomain.lower_bound_type() == carl::BoundType::INFTY
545 || activeDomain.lower() != domain.lower()
546 || activeDomain.is_empty()))
547 propagateFormula(FormulaT(expansion.mQuotients[0]-Poly(domain.lower()), carl::Relation::GEQ), true);
548 if (domain.upper_bound_type() != carl::BoundType::INFTY
549 && (activeDomain.upper_bound_type() == carl::BoundType::INFTY
550 || activeDomain.upper() != domain.upper()
551 || activeDomain.is_empty()))
552 propagateFormula(FormulaT(expansion.mQuotients[0]-Poly(domain.upper()), carl::Relation::LEQ), true);
555 /// Check if the digits of the expansion need to be encoded
556 if (expansion.mPurifications.empty())
558 activeDomain = RationalInterval::empty_interval();
559 domain = RationalInterval::empty_interval();
562 /// Update the case splits of the corresponding digits
563 for (size_t i = 0; activeDomain != domain; ++i)
565 if (domain.diameter() <= Settings::maxDomainSize)
567 /// Update the currently active linear encoding
568 Rational lower{activeDomain.is_empty() ? domain.lower() : activeDomain.lower()};
569 Rational upper{activeDomain.is_empty() ? domain.lower() : activeDomain.upper()+1};
570 for (const Purification *purification : expansion.mPurifications)
572 for (Rational alpha = domain.lower(); alpha < lower; ++alpha)
575 carl::FormulaType::IMPLIES,
576 FormulaT(Poly(expansion.mQuotients[i])-Poly(alpha), carl::Relation::EQ),
577 FormulaT(Poly(purification->mSubstitutions[i])-Poly(alpha)*Poly(purification->mReduction), carl::Relation::EQ)
581 for (Rational alpha = upper; alpha <= domain.upper(); ++alpha)
584 carl::FormulaType::IMPLIES,
585 FormulaT(Poly(expansion.mQuotients[i])-Poly(alpha), carl::Relation::EQ),
586 FormulaT(Poly(purification->mSubstitutions[i])-Poly(alpha)*Poly(purification->mReduction), carl::Relation::EQ)
592 else if (activeDomain.diameter() <= Settings::maxDomainSize)
594 /// Switch from the linear to a logarithmic encoding
595 if (expansion.mQuotients.size() <= i+1)
597 expansion.mQuotients.emplace_back(carl::fresh_integer_variable());
598 expansion.mRemainders.emplace_back(carl::fresh_integer_variable());
600 for (Purification *purification : expansion.mPurifications)
602 if (purification->mSubstitutions.size() <= i+1)
603 purification->mSubstitutions.emplace_back(carl::fresh_integer_variable());
604 for (Rational alpha = activeDomain.lower(); alpha <= activeDomain.upper(); ++alpha)
607 carl::FormulaType::IMPLIES,
608 FormulaT(Poly(expansion.mQuotients[i])-Poly(alpha), carl::Relation::EQ),
609 FormulaT(Poly(purification->mSubstitutions[i])-Poly(alpha)*Poly(purification->mReduction), carl::Relation::EQ)
613 for (Rational alpha = 0; alpha < Settings::expansionBase; ++alpha)
616 carl::FormulaType::IMPLIES,
617 FormulaT(Poly(expansion.mRemainders[i])-Poly(alpha), carl::Relation::EQ),
618 FormulaT(Poly(purification->mSubstitutions[i])-Poly(Settings::expansionBase)*Poly(purification->mSubstitutions[i+1])-Poly(alpha)*Poly(purification->mReduction), carl::Relation::EQ)
623 propagateFormula(FormulaT(Poly(expansion.mQuotients[i])-Poly(Settings::expansionBase)*Poly(expansion.mQuotients[i+1])-Poly(expansion.mRemainders[i]), carl::Relation::EQ), true);
624 propagateFormula(FormulaT(Poly(expansion.mRemainders[i]), carl::Relation::GEQ), true);
625 propagateFormula(FormulaT(Poly(expansion.mRemainders[i])-Poly(Settings::expansionBase-1), carl::Relation::LEQ), true);
628 /// Calculate the domain of the next digit
629 if (!activeDomain.is_empty()) {
630 if (activeDomain.diameter() <= Settings::maxDomainSize)
631 activeDomain = RationalInterval::empty_interval();
633 activeDomain = carl::floor(activeDomain/Rational(Settings::expansionBase));
635 if (!domain.is_empty()) {
636 if (domain.diameter() <= Settings::maxDomainSize)
637 domain = RationalInterval::empty_interval();
639 domain = carl::floor(domain/Rational(Settings::expansionBase));
642 /// Update the variable bounds of the next digit
643 if (!activeDomain.is_empty())
645 if (domain.is_empty() || domain.lower() != activeDomain.lower())
646 propagateFormula(FormulaT(expansion.mQuotients[i+1]-Poly(activeDomain.lower()), carl::Relation::GEQ), false);
647 if (domain.is_empty() || domain.upper() != activeDomain.upper())
648 propagateFormula(FormulaT(expansion.mQuotients[i+1]-Poly(activeDomain.upper()), carl::Relation::LEQ), false);
650 if (!domain.is_empty())
652 if (activeDomain.is_empty() || activeDomain.lower() != domain.lower())
653 propagateFormula(FormulaT(expansion.mQuotients[i+1]-Poly(domain.lower()), carl::Relation::GEQ), true);
654 if (activeDomain.is_empty() || activeDomain.upper() != domain.upper())
655 propagateFormula(FormulaT(expansion.mQuotients[i+1]-Poly(domain.upper()), carl::Relation::LEQ), true);
660 template<class Settings>
661 inline void CSplitModule<Settings>::propagateFormula(const FormulaT& formula, bool assert)
664 mLRAModule.add(formula);
666 mLRAModule.remove(std::find(mLRAModule.formulaBegin(), mLRAModule.formulaEnd(), formula));