[solver] Modified the way to solve some equations

The solutions of equations that need numerical approximations to be
solved are now computed base on the undeveloped equation (instead of
fully the expended one used to identify polynomials)

This allow (x-10)^7=0 to yield x=10 as result (9.95 before)
Change-Id: Ia8acbe57a9cfebf0b5016e9c896d21c8ddac7a64
This commit is contained in:
Arthur Camouseigt
2020-06-25 15:15:26 +02:00
committed by Émilie Feral
parent 6e9a5a010f
commit 913c81a0d3
12 changed files with 97 additions and 94 deletions

View File

@@ -19,58 +19,42 @@ bool Equation::containsIComplex(Context * context) const {
return expressionClone().recursivelyMatches([](const Expression e, Context * context) { return e.type() == ExpressionNode::Type::Constant && static_cast<const Constant &>(e).isIComplex(); }, context);
}
Expression Equation::Model::standardForm(const Storage::Record * record, Context * context, bool replaceFunctionsButNotSymbols) const {
Expression * returnedExpression = replaceFunctionsButNotSymbols ? &m_standardFormWithReplacedFunctionsButNotSymbols : &m_standardFormWithReplacedFunctionsAndSymbols;
if (returnedExpression->isUninitialized()) {
Expression expressionInputWithoutFunctions = Expression::ExpressionWithoutSymbols(expressionClone(record), context, replaceFunctionsButNotSymbols);
if (expressionInputWithoutFunctions.isUninitialized()) {
// The expression is circularly-defined
expressionInputWithoutFunctions = Undefined::Builder();
}
EmptyContext emptyContext;
Context * contextToUse = replaceFunctionsButNotSymbols ? &emptyContext : context;
// Reduce the expression
Expression expressionRed = expressionInputWithoutFunctions.clone();
PoincareHelpers::Simplify(&expressionRed, contextToUse, ExpressionNode::ReductionTarget::SystemForApproximation);
// simplify might return an uninitialized Expression if interrupted
if (expressionRed.isUninitialized()) {
expressionRed = expressionInputWithoutFunctions;
}
if (expressionRed.type() == ExpressionNode::Type::Unreal) {
*returnedExpression = Unreal::Builder();
} else if (expressionRed.recursivelyMatches(
[](const Expression e, Context * context) {
return e.type() == ExpressionNode::Type::Undefined || e.type() == ExpressionNode::Type::Infinity || Expression::IsMatrix(e, context);
},
contextToUse))
{
*returnedExpression = Undefined::Builder();
} else if (expressionRed.type() == ExpressionNode::Type::Equal) {
Preferences * preferences = Preferences::sharedPreferences();
*returnedExpression = static_cast<const Equal&>(expressionRed).standardEquation(contextToUse, Expression::UpdatedComplexFormatWithExpressionInput(preferences->complexFormat(), expressionInputWithoutFunctions, contextToUse), preferences->angleUnit(), GlobalPreferences::sharedGlobalPreferences()->unitFormat());
} else {
assert(expressionRed.type() == ExpressionNode::Type::Rational && static_cast<const Rational&>(expressionRed).isOne());
// The equality was reduced which means the equality was always true.
*returnedExpression = Rational::Builder(0);
}
if (!m_standardFormWithReplacedFunctionsButNotSymbols.isUninitialized() && !m_standardFormWithReplacedFunctionsAndSymbols.isUninitialized()) {
// Do not keep two equal expressions
if (m_standardFormWithReplacedFunctionsButNotSymbols.isIdenticalTo(m_standardFormWithReplacedFunctionsAndSymbols)) {
m_standardFormWithReplacedFunctionsButNotSymbols = m_standardFormWithReplacedFunctionsAndSymbols;
}
}
Expression Equation::Model::standardForm(const Storage::Record * record, Context * context, bool replaceFunctionsButNotSymbols, ExpressionNode::ReductionTarget reductionTarget) const {
Expression returnedExpression = Expression();
Expression expressionInputWithoutFunctions = Expression::ExpressionWithoutSymbols(expressionClone(record), context, replaceFunctionsButNotSymbols);
if (expressionInputWithoutFunctions.isUninitialized()) {
// The expression is circularly-defined
expressionInputWithoutFunctions = Undefined::Builder();
}
return *returnedExpression;
}
EmptyContext emptyContext;
Context * contextToUse = replaceFunctionsButNotSymbols ? &emptyContext : context;
void Equation::Model::tidy() const {
ExpressionModel::tidy();
// Free the pool of the m_standardForm
m_standardFormWithReplacedFunctionsAndSymbols = Expression();
m_standardFormWithReplacedFunctionsButNotSymbols = Expression();
// Reduce the expression
Expression expressionRed = expressionInputWithoutFunctions.clone();
PoincareHelpers::Simplify(&expressionRed, contextToUse, reductionTarget);
// simplify might return an uninitialized Expression if interrupted
if (expressionRed.isUninitialized()) {
expressionRed = expressionInputWithoutFunctions;
}
if (expressionRed.type() == ExpressionNode::Type::Unreal) {
returnedExpression = Unreal::Builder();
} else if (expressionRed.recursivelyMatches(
[](const Expression e, Context * context) {
return e.type() == ExpressionNode::Type::Undefined || e.type() == ExpressionNode::Type::Infinity || Expression::IsMatrix(e, context);
},
contextToUse))
{
returnedExpression = Undefined::Builder();
} else if (expressionRed.type() == ExpressionNode::Type::Equal) {
Preferences * preferences = Preferences::sharedPreferences();
returnedExpression = static_cast<const Equal&>(expressionRed).standardEquation(contextToUse, Expression::UpdatedComplexFormatWithExpressionInput(preferences->complexFormat(), expressionInputWithoutFunctions, contextToUse), preferences->angleUnit(), GlobalPreferences::sharedGlobalPreferences()->unitFormat(), reductionTarget);
} else {
assert(expressionRed.type() == ExpressionNode::Type::Rational && static_cast<const Rational&>(expressionRed).isOne());
// The equality was reduced which means the equality was always true.
returnedExpression = Rational::Builder(0);
}
return returnedExpression;
}
void * Equation::Model::expressionAddress(const Ion::Storage::Record * record) const {

View File

@@ -11,19 +11,16 @@ public:
bool shouldBeClearedBeforeRemove() override {
return false;
}
Poincare::Expression standardForm(Poincare::Context * context, bool replaceFunctionsButNotSymbols) const { return m_model.standardForm(this, context, replaceFunctionsButNotSymbols); }
Poincare::Expression standardForm(Poincare::Context * context, bool replaceFunctionsButNotSymbols, Poincare::ExpressionNode::ReductionTarget reductionTarget) const { return m_model.standardForm(this, context, replaceFunctionsButNotSymbols, reductionTarget); }
bool containsIComplex(Poincare::Context * context) const;
private:
class Model : public Shared::ExpressionModel {
public:
Poincare::Expression standardForm(const Ion::Storage::Record * record, Poincare::Context * context, bool replaceFunctionsButNotSymbols) const;
void tidy() const override;
Poincare::Expression standardForm(const Ion::Storage::Record * record, Poincare::Context * context, bool replaceFunctionsButNotSymbols, Poincare::ExpressionNode::ReductionTarget reductionTarget) const;
private:
void * expressionAddress(const Ion::Storage::Record * record) const override;
size_t expressionSize(const Ion::Storage::Record * record) const override;
mutable Poincare::Expression m_standardFormWithReplacedFunctionsAndSymbols;
mutable Poincare::Expression m_standardFormWithReplacedFunctionsButNotSymbols;
};
size_t metaDataSize() const override { return 0; }
const Shared::ExpressionModel * model() const override { return &m_model; }

View File

@@ -89,28 +89,23 @@ void EquationStore::setIntervalBound(int index, double value) {
}
}
double EquationStore::approximateSolutionAtIndex(int i) {
assert(m_type == Type::Monovariable && i >= 0 && i < m_numberOfSolutions);
return m_approximateSolutions[i];
}
bool EquationStore::haveMoreApproximationSolutions(Context * context, bool solveWithoutContext) {
if (m_numberOfSolutions < k_maxNumberOfEquations) {
return false;
}
double step = (m_intervalApproximateSolutions[1]-m_intervalApproximateSolutions[0])*k_precision;
return !std::isnan(PoincareHelpers::NextRoot(modelForRecord(definedRecordAtIndex(0))->standardForm(context, solveWithoutContext), m_variables[0], m_approximateSolutions[m_numberOfSolutions-1], step, m_intervalApproximateSolutions[1], context));
}
void EquationStore::approximateSolve(Poincare::Context * context, bool shouldReplaceFunctionsButNotSymbols) {
m_hasMoreThanMaxNumberOfApproximateSolution = false;
Expression undevelopedExpression = modelForRecord(definedRecordAtIndex(0))->standardForm(context, shouldReplaceFunctionsButNotSymbols, ExpressionNode::ReductionTarget::SystemForApproximation);
m_userVariablesUsed = !shouldReplaceFunctionsButNotSymbols;
assert(m_variables[0][0] != 0 && m_variables[1][0] == 0);
assert(m_type == Type::Monovariable);
m_numberOfSolutions = 0;
double start = m_intervalApproximateSolutions[0];
double step = (m_intervalApproximateSolutions[1]-m_intervalApproximateSolutions[0])*k_precision;
for (int i = 0; i < k_maxNumberOfApproximateSolutions; i++) {
m_approximateSolutions[i] = PoincareHelpers::NextRoot(modelForRecord(definedRecordAtIndex(0))->standardForm(context, shouldReplaceFunctionsButNotSymbols), m_variables[0], start, step, m_intervalApproximateSolutions[1], context);
double root;
for (int i = 0; i <= k_maxNumberOfApproximateSolutions; i++) {
root = PoincareHelpers::NextRoot(undevelopedExpression, m_variables[0], start, step, m_intervalApproximateSolutions[1], context);
if (i == k_maxNumberOfApproximateSolutions) {
m_hasMoreThanMaxNumberOfApproximateSolution = !isnan(root);
break;
}
m_approximateSolutions[i] = root;
if (std::isnan(m_approximateSolutions[i])) {
break;
} else {
@@ -131,6 +126,20 @@ EquationStore::Error EquationStore::exactSolve(Poincare::Context * context, bool
return e;
}
/* Equations are solved according to the following procedure :
* 1) We develop the equations using the reduction target "SystemForAnalysis".
* This expands structures like Newton multinoms and allows us to detect
* polynoms afterwards. ("(x+2)^2" in this form is not detected but is if
* expanded).
* 2) We look for classic forms of equations for which we have algorithms
* that output the exact answer. If one is recognized in the input equation,
* the exact answer is given to the user.
* 3) If no classic form has been found in the developped form, we need to use
* numerical approximation. Therefore, to prevent precision losses, we work
* with the undevelopped form of the equation. Therefore we set reductionTarget
* to SystemForApproximation. Solutions are then numericaly approximated
* between the bounds provided by the user. */
EquationStore::Error EquationStore::privateExactSolve(Poincare::Context * context, bool replaceFunctionsButNotSymbols) {
tidySolution();
@@ -143,6 +152,7 @@ EquationStore::Error EquationStore::privateExactSolve(Poincare::Context * contex
// TODO we look twice for variables but not the same, is there a way to not do the same work twice?
m_userVariables[0][0] = 0;
m_numberOfUserVariables = 0;
Expression simplifiedExpressions[k_maxNumberOfEquations];
for (int i = 0; i < numberOfDefinedModels(); i++) {
Shared::ExpiringPointer<Equation> eq = modelForRecord(definedRecordAtIndex(i));
@@ -150,12 +160,21 @@ EquationStore::Error EquationStore::privateExactSolve(Poincare::Context * contex
/* Start by looking for user variables, so that if we escape afterwards, we
* know if it might be due to a user variable. */
if (m_numberOfUserVariables < Expression::k_maxNumberOfVariables) {
const Expression eWithSymbols = eq->standardForm(context, true);
const Expression eWithSymbols = eq->standardForm(context, true, ExpressionNode::ReductionTarget::SystemForAnalysis);
/* if replaceFunctionsButNotSymbols is true we can memoize the expressions
* for the rest of the function. Otherwise, we will memoize them at the
* next call to standardForm*/
if (replaceFunctionsButNotSymbols == true) {
simplifiedExpressions[i] = eWithSymbols;
}
int varCount = eWithSymbols.getVariables(context, [](const char * symbol, Poincare::Context * context) { return context->expressionTypeForIdentifier(symbol, strlen(symbol)) == Poincare::Context::SymbolAbstractType::Symbol; }, (char *)m_userVariables, Poincare::SymbolAbstract::k_maxNameSize, m_numberOfUserVariables);
m_numberOfUserVariables = varCount < 0 ? Expression::k_maxNumberOfVariables : varCount;
}
const Expression e = eq->standardForm(context, replaceFunctionsButNotSymbols); // The standard form is memoized so there is no double computation even if replaceFunctionsButNotSymbols is true.
if (simplifiedExpressions[i].isUninitialized()) {
// The expression was not memoized before.
simplifiedExpressions[i] = eq->standardForm(context, replaceFunctionsButNotSymbols, ExpressionNode::ReductionTarget::SystemForAnalysis);
}
const Expression e = simplifiedExpressions[i];
if (e.isUninitialized() || e.type() == ExpressionNode::Type::Undefined || e.recursivelyMatches(Expression::IsMatrix, context, replaceFunctionsButNotSymbols ? ExpressionNode::SymbolicComputation::ReplaceDefinedFunctionsWithDefinitions : ExpressionNode::SymbolicComputation::ReplaceAllDefinedSymbolsWithDefinition)) {
return Error::EquationUndefined;
}
@@ -180,7 +199,7 @@ EquationStore::Error EquationStore::privateExactSolve(Poincare::Context * contex
bool isLinear = true; // Invalid the linear system if one equation is non-linear
Preferences * preferences = Preferences::sharedPreferences();
for (int i = 0; i < numberOfDefinedModels(); i++) {
isLinear = isLinear && modelForRecord(definedRecordAtIndex(i))->standardForm(context, replaceFunctionsButNotSymbols).getLinearCoefficients((char *)m_variables, Poincare::SymbolAbstract::k_maxNameSize, coefficients[i], &constants[i], context, updatedComplexFormat(context), preferences->angleUnit(), GlobalPreferences::sharedGlobalPreferences()->unitFormat(), replaceFunctionsButNotSymbols ? ExpressionNode::SymbolicComputation::ReplaceDefinedFunctionsWithDefinitions : ExpressionNode::SymbolicComputation::ReplaceAllDefinedSymbolsWithDefinition);
isLinear = isLinear && simplifiedExpressions[i].getLinearCoefficients((char *)m_variables, Poincare::SymbolAbstract::k_maxNameSize, coefficients[i], &constants[i], context, updatedComplexFormat(context), preferences->angleUnit(), GlobalPreferences::sharedGlobalPreferences()->unitFormat(), replaceFunctionsButNotSymbols ? ExpressionNode::SymbolicComputation::ReplaceDefinedFunctionsWithDefinitions : ExpressionNode::SymbolicComputation::ReplaceAllDefinedSymbolsWithDefinition);
if (!isLinear) {
// TODO: should we clean pool allocated memory if the system is not linear
#if 0
@@ -211,7 +230,7 @@ EquationStore::Error EquationStore::privateExactSolve(Poincare::Context * contex
// Step 3. Polynomial & Monovariable?
assert(numberOfVariables == 1 && numberOfDefinedModels() == 1);
Expression polynomialCoefficients[Expression::k_maxNumberOfPolynomialCoefficients];
int degree = modelForRecord(definedRecordAtIndex(0))->standardForm(context, replaceFunctionsButNotSymbols)
int degree = simplifiedExpressions[0]
.getPolynomialReducedCoefficients(
m_variables[0],
polynomialCoefficients,

View File

@@ -74,14 +74,18 @@ public:
/* Approximate resolution */
double intervalBound(int index) const;
void setIntervalBound(int index, double value);
double approximateSolutionAtIndex(int i);
double approximateSolutionAtIndex(int i) {
assert(m_type == Type::Monovariable && i >= 0 && i < m_numberOfSolutions);
return m_approximateSolutions[i];
}
void approximateSolve(Poincare::Context * context, bool shouldReplaceFuncionsButNotSymbols);
bool haveMoreApproximationSolutions(Poincare::Context * context, bool solveWithoutContext);
bool haveMoreApproximationSolutions() { return m_hasMoreThanMaxNumberOfApproximateSolution; }
void tidy() override;
static constexpr int k_maxNumberOfExactSolutions = Poincare::Expression::k_maxNumberOfVariables > Poincare::Expression::k_maxPolynomialDegree + 1? Poincare::Expression::k_maxNumberOfVariables : Poincare::Expression::k_maxPolynomialDegree + 1;
static constexpr int k_maxNumberOfApproximateSolutions = 10;
bool m_hasMoreThanMaxNumberOfApproximateSolution;
static constexpr int k_maxNumberOfSolutions = k_maxNumberOfExactSolutions > k_maxNumberOfApproximateSolutions ? k_maxNumberOfExactSolutions : k_maxNumberOfApproximateSolutions;
private:
static constexpr double k_precision = 0.01;

View File

@@ -45,7 +45,8 @@ IntervalController::IntervalController(Responder * parentResponder, InputEventHa
FloatParameterController<double>(parentResponder),
m_contentView(&m_selectableTableView),
m_intervalCell{},
m_equationStore(equationStore)
m_equationStore(equationStore),
m_shouldReplaceFunctionsButNotSymbols(false)
{
m_selectableTableView.setTopMargin(0);
m_okButton.setMessage(I18n::Message::ResolveEquation);
@@ -102,7 +103,7 @@ bool IntervalController::textFieldDidFinishEditing(TextField * textField, const
void IntervalController::buttonAction() {
StackViewController * stack = stackController();
m_equationStore->approximateSolve(textFieldDelegateApp()->localContext(), App::app()->solutionsController()->shouldReplaceFuncionsButNotSymbols());
m_equationStore->approximateSolve(textFieldDelegateApp()->localContext(), m_shouldReplaceFunctionsButNotSymbols);
stack->push(App::app()->solutionsControllerStack(), KDColorWhite, Palette::SubTab, Palette::SubTab);
}

View File

@@ -15,6 +15,7 @@ public:
TELEMETRY_ID("Interval");
int numberOfRows() const override;
void willDisplayCellForIndex(HighlightCell * cell, int index) override;
void setShouldReplaceFuncionsButNotSymbols(bool shouldReplaceFunctionsButNotSymbols) { m_shouldReplaceFunctionsButNotSymbols = shouldReplaceFunctionsButNotSymbols; }
private:
HighlightCell * reusableParameterCell(int index, int type) override;
int reusableParameterCellCount(int type) override;
@@ -39,6 +40,7 @@ private:
constexpr static int k_maxNumberOfCells = 2;
MessageTableCellWithEditableText m_intervalCell[k_maxNumberOfCells];
EquationStore * m_equationStore;
bool m_shouldReplaceFunctionsButNotSymbols;
};
}

View File

@@ -190,7 +190,7 @@ void ListController::resolveEquations() {
return;
case EquationStore::Error::RequireApproximateSolution:
{
App::app()->solutionsController()->setShouldReplaceFuncionsButNotSymbols(resultWithoutUserDefinedSymbols);
reinterpret_cast<IntervalController *>(App::app()->intervalController())->setShouldReplaceFuncionsButNotSymbols(resultWithoutUserDefinedSymbols);
stackController()->push(App::app()->intervalController(), KDColorWhite, Palette::PurpleBright, Palette::PurpleBright);
return;
}
@@ -198,7 +198,7 @@ void ListController::resolveEquations() {
{
assert(e == EquationStore::Error::NoError);
StackViewController * stack = stackController();
App::app()->solutionsController()->setShouldReplaceFuncionsButNotSymbols(resultWithoutUserDefinedSymbols);
reinterpret_cast<IntervalController *>(App::app()->intervalController())->setShouldReplaceFuncionsButNotSymbols(resultWithoutUserDefinedSymbols);
stack->push(App::app()->solutionsControllerStack(), KDColorWhite, Palette::PurpleBright, Palette::PurpleBright);
}
}

View File

@@ -81,8 +81,7 @@ SolutionsController::SolutionsController(Responder * parentResponder, EquationSt
m_equationStore(equationStore),
m_deltaCell(0.5f, 0.5f),
m_delta2Layout(),
m_contentView(this),
m_shouldReplaceFunctionsButNotSymbols(false)
m_contentView(this)
{
m_delta2Layout = HorizontalLayout::Builder(VerticalOffsetLayout::Builder(CodePointLayout::Builder('2', KDFont::SmallFont), VerticalOffsetLayoutNode::Position::Superscript), LayoutHelper::String("-4ac", 4, KDFont::SmallFont));
const char * deltaB = "Δ=b";
@@ -112,7 +111,7 @@ void SolutionsController::viewWillAppear() {
bool requireWarning = false;
if (m_equationStore->type() == EquationStore::Type::Monovariable) {
m_contentView.setWarningMessages(I18n::Message::OnlyFirstSolutionsDisplayed0, I18n::Message::OnlyFirstSolutionsDisplayed1);
requireWarning = m_equationStore->haveMoreApproximationSolutions(App::app()->localContext(), m_shouldReplaceFunctionsButNotSymbols);
requireWarning = m_equationStore->haveMoreApproximationSolutions();
} else if (m_equationStore->type() == EquationStore::Type::PolynomialMonovariable && m_equationStore->numberOfSolutions() == 1) {
assert(Preferences::sharedPreferences()->complexFormat() == Preferences::ComplexFormat::Real);
m_contentView.setWarningMessages(I18n::Message::PolynomeHasNoRealSolution0, I18n::Message::PolynomeHasNoRealSolution1);

View File

@@ -11,8 +11,6 @@ namespace Solver {
class SolutionsController : public ViewController, public AlternateEmptyViewDefaultDelegate, public SelectableTableViewDataSource, public TableViewDataSource, public SelectableTableViewDelegate {
public:
SolutionsController(Responder * parentResponder, EquationStore * equationStore);
void setShouldReplaceFuncionsButNotSymbols(bool shouldReplaceFuncionsButNotSymbols) { m_shouldReplaceFunctionsButNotSymbols = shouldReplaceFuncionsButNotSymbols; }
bool shouldReplaceFuncionsButNotSymbols() const { return m_shouldReplaceFunctionsButNotSymbols; }
/* ViewController */
const char * title() override;
View * view() override { return &m_contentView; }
@@ -111,7 +109,6 @@ private:
EvenOddBufferTextCell m_approximateValueCells[k_numberOfApproximateValueCells];
MessageCell m_messageCells[k_numberOfMessageCells];
ContentView m_contentView;
bool m_shouldReplaceFunctionsButNotSymbols;
};
}

View File

@@ -89,8 +89,12 @@ QUIZ_CASE(equation_solve) {
// conj(x)*x+1 = 0
assert_solves_to_error("conj(x)*x+1=0", RequireApproximateSolution);
assert_solves_numerically_to("conj(x)*x+1=0", -100, 100, {});
assert_solves_to_error("(x-10)^7=0", RequireApproximateSolution);
assert_solves_numerically_to("(x-10)^7=0", -100, 100, {10});
}
QUIZ_CASE(equation_solve_complex_real) {
set_complex_format(Real);
assert_solves_to("x+𝐢=0", "x=-𝐢"); // We still want complex solutions if the input has some complex value