solver: Implement solving menu

The `SolvingMenu` makes it possible to quickly solve equations with
multiple variables. It is typically accessed from the `SolverMenu`
(currently under key F4).

The menu shows the variables in the current equation, as defined by
the `'EQ'` variable (aka `Equation`). This can be accessed with the
`StEQ` and `RcEQ` commands.

For each variable in the equation, the function keys work as follow:

* The unshifted key sets the variable value
* The shifted value solves for the given variable
* The second-shifted value recalls the variable value

The menu also shows the current value in menu label.

Fixes: #925

Signed-off-by: Christophe de Dinechin <christophe@dinechin.org>
This commit is contained in:
Christophe de Dinechin 2024-05-13 01:19:49 +02:00
parent 391901c66c
commit 45742061b6
9 changed files with 351 additions and 42 deletions

View file

@ -59,6 +59,8 @@ ERROR(directory_path, "Directory not in path")
ERROR(name_exists, "Name already exists")
ERROR(invalid_name, "Invalid name")
ERROR(undefined_name, "Undefined name")
ERROR(some_undefined_name, "Some variables are undefined")
ERROR(some_invalid_name, "Some variables are invalid")
ERROR(recursion, "Too many recursive calls")
ERROR(return_without_caller, "Return without a caller")
ERROR(invalid_local, "Invalid local")

View file

@ -39,6 +39,7 @@
#include "settings.h"
#include "unit.h"
#include "utf8.h"
#include "variables.h"
RECORDER(equation, 16, "Processing of equations and algebraic objects");
RECORDER(equation_error,16, "Errors with equations");
@ -2060,6 +2061,31 @@ GRAPH_BODY(expression)
}
expression_p expression::current_equation()
// ----------------------------------------------------------------------------
// Return content of EQ variable
// ----------------------------------------------------------------------------
{
object_p obj = directory::recall_all(static_object(ID_Equation), false);
if (!obj)
{
if (!rt.error())
rt.no_equation_error();
return nullptr;
}
id eqty = obj->type();
if (eqty != ID_expression && eqty != ID_polynomial)
{
rt.type_error();
return nullptr;
}
if (eqty == ID_expression)
obj = expression_p(obj)->as_difference_for_solve();
return expression_p(obj);
}
// ============================================================================
//

View file

@ -111,6 +111,8 @@ struct expression : program
return nullptr;
}
static expression_p current_equation();
// ========================================================================

View file

@ -1002,11 +1002,12 @@ SETTING(ErrorBeepDuration, 0, 500, 50)
//
// ============================================================================
OP(AlgebraConfiguration, "Ⓓ") ALIAS(AlgebraConfiguration, "CASDir")
OP(AlgebraVariable, "ⓧ") ALIAS(AlgebraVariable, "VX")
ALIAS(AlgebraVariable, "RclVX")
OP(StoreAlgebraVariable, "Storeⓧ") ALIAS(StoreAlgebraVariable, "StoVX")
OP(StEq, "▶Equation") ALIAS(StEq, "StoreEquation")
OP(RcEq, "Equation▶") ALIAS(RcEq, "RecallEquation")
@ -1026,6 +1027,9 @@ CMD(VariablesMenuExecute)
CMD(VariablesMenuRecall)
CMD(VariablesMenuStore)
CMD(SolvingMenuStore)
CMD(SolvingMenuSolve)
CMD(SolvingMenuRecall)
@ -1110,6 +1114,7 @@ MENU(TextMenu)
// Special menus
MENU(EditMenu)
CMD(VariablesMenu)
CMD(SolvingMenu)
CMD(ToolsMenu)
CMD(Catalog)

View file

@ -1179,20 +1179,21 @@ MENU(SolverMenu,
// ----------------------------------------------------------------------------
// The solver menu / application
// ----------------------------------------------------------------------------
"Eq", ID_Equation,
"Indep", ID_Unimplemented,
"Eq", ID_RcEq,
"", ID_AlgebraVariable,
"Root", ID_Root,
"MultiR", ID_Unimplemented,
"PolyR", ID_Unimplemented,
"Solve", ID_SolvingMenu,
"Num", ID_NumericalSolverMenu,
"Diff", ID_DifferentialSolverMenu,
"Symb", ID_SymbolicSolverMenu,
"▶Eq", ID_StEq,
"Stoⓧ", ID_StoreAlgebraVariable,
"Diff", ID_DifferentialSolverMenu,
"Poly", ID_PolynomialSolverMenu,
"Linear", ID_LinearSolverMenu,
"Multi", ID_MultiSolverMenu,
"Finance", ID_FinanceSolverMenu,
"Finance", ID_FinanceSolverMenu,
"Plot", ID_PlotMenu,
"L.R.", ID_StatisticsMenu,
"Eqns", ID_EquationsMenu,

View file

@ -36,10 +36,12 @@
#include "expression.h"
#include "functions.h"
#include "integer.h"
#include "list.h"
#include "recorder.h"
#include "settings.h"
#include "symbol.h"
#include "tag.h"
#include "variables.h"
RECORDER(solve, 16, "Numerical solver");
RECORDER(solve_error, 16, "Numerical solver errors");
@ -313,3 +315,257 @@ algebraic_p solve(program_g eq, symbol_g name, object_g guess)
rt.no_solution_error();
return lx;
}
// ============================================================================
//
// Solving menu
//
// ============================================================================
COMMAND_BODY(StEq)
// ----------------------------------------------------------------------------
// Store expression in `Equation` variable
// ----------------------------------------------------------------------------
{
if (object_p obj = rt.top())
{
id objty = obj->type();
if (objty != ID_expression && objty != ID_polynomial)
rt.type_error();
else if (directory::store_here(static_object(ID_Equation), obj))
if (rt.drop())
return OK;
}
return ERROR;
}
COMMAND_BODY(RcEq)
// ----------------------------------------------------------------------------
// Store expression in `Equation` variable
// ----------------------------------------------------------------------------
{
if (expression_p expr = expression::current_equation())
if (rt.push(expr))
return OK;
return ERROR;
}
MENU_BODY(SolvingMenu)
// ----------------------------------------------------------------------------
// Process the MENU command for SolvingMenu
// ----------------------------------------------------------------------------
{
expression_p expr = expression::current_equation();
if (!expr)
return false;
list_g vars = expr->names();
if (!vars)
return false;
size_t nitems = vars->items();
items_init(mi, nitems, 3, 1);
uint skip = mi.skip;
// First row: Store variables
mi.plane = 0;
mi.planes = 1;
for (auto name : *vars)
if (symbol_p sym = name->as<symbol>())
menu::items(mi, sym, menu::ID_SolvingMenuStore);
// Second row: Solve for variables
mi.plane = 1;
mi.planes = 2;
mi.skip = skip;
mi.index = mi.plane * ui.NUM_SOFTKEYS;
for (auto name : *vars)
if (symbol_p sym = name->as<symbol>())
menu::items(mi, sym, menu::ID_SolvingMenuSolve);
// Third row: Recall variable
mi.plane = 2;
mi.planes = 3;
mi.skip = skip;
mi.index = mi.plane * ui.NUM_SOFTKEYS;
for (auto name : *vars)
{
if (symbol_g sym = name->as<symbol>())
{
settings::SaveDisplayDigits sdd(3);
object_p value = directory::recall_all(sym, false);
if (!value)
value = symbol::make("?");
if (value)
sym = value->as_symbol(false);
menu::items(mi, sym, menu::ID_SolvingMenuRecall);
}
}
// Add markers
for (uint k = 0; k < ui.NUM_SOFTKEYS - (mi.pages > 1); k++)
{
ui.marker(k + 0 * ui.NUM_SOFTKEYS, L'', true);
ui.marker(k + 1 * ui.NUM_SOFTKEYS, L'?', false);
ui.marker(k + 2 * ui.NUM_SOFTKEYS, L'', false);
}
return true;
}
static symbol_p expression_variable(uint index)
// ----------------------------------------------------------------------------
// Return the variable in EQ for a given index
// ----------------------------------------------------------------------------
{
if (expression_p expr = expression::current_equation())
if (list_g vars = expr->names())
if (object_p obj = vars->at(index))
if (symbol_p sym = obj->as<symbol>())
return sym;
return nullptr;
}
static tag_p tagged_value(symbol_p sym, object_p value)
// ----------------------------------------------------------------------------
// Tag a solver value with a symbol
// ----------------------------------------------------------------------------
{
size_t nlen = 0;
gcutf8 ntxt = sym->value(&nlen);
tag_p tagged = tag::make(ntxt, nlen, value);
return tagged;
}
static bool is_well_defined(expression_r expr, symbol_r sym)
// ----------------------------------------------------------------------------
// Check if all variables but the one we solve for are defined
// ----------------------------------------------------------------------------
{
list_p vars = expr->names();
for (auto var : *vars)
{
if (symbol_p vsym = var->as<symbol>())
{
if (!sym->is_same_as(vsym))
{
if (!directory::recall_all(var, false))
{
rt.some_undefined_name_error();
return false;
}
}
}
else
{
rt.some_invalid_name_error();
return false;
}
}
return true;
}
COMMAND_BODY(SolvingMenuRecall)
// ----------------------------------------------------------------------------
// Recall a variable from the SolvingMenu
// ----------------------------------------------------------------------------
{
int key = ui.evaluating;
if (key >= KEY_F1 && key <= KEY_F6)
{
uint index = key - KEY_F1 + 5 * ui.page();
if (symbol_g sym = expression_variable(index))
if (object_p value = directory::recall_all(sym, true))
if (tag_p tagged = tagged_value(sym, value))
if (rt.push(tagged))
return OK;
}
return ERROR;
}
INSERT_BODY(SolvingMenuRecall)
// ----------------------------------------------------------------------------
// Insert the name of a variable with `Recall` after it
// ----------------------------------------------------------------------------
{
int key = ui.evaluating;
return ui.insert_softkey(key, " '", "' Recall ", false);
}
COMMAND_BODY(SolvingMenuStore)
// ----------------------------------------------------------------------------
// Store a variable from the SolvingMenu
// ----------------------------------------------------------------------------
{
int key = ui.evaluating;
if (key >= KEY_F1 && key <= KEY_F6)
{
uint index = key - KEY_F1 + 5 * ui.page();
if (symbol_p sym = expression_variable(index))
if (object_p value = rt.pop())
if (directory::store_here(sym, value))
if (ui.menu_refresh())
return OK;
}
return ERROR;
}
INSERT_BODY(SolvingMenuStore)
// ----------------------------------------------------------------------------
// Insert the name of a variable with `Store` after it
// ----------------------------------------------------------------------------
{
int key = ui.evaluating;
return ui.insert_softkey(key, " '", "' Store ", false);
}
COMMAND_BODY(SolvingMenuSolve)
// ----------------------------------------------------------------------------
// Solve for a given variable
// ----------------------------------------------------------------------------
{
int key = ui.evaluating;
if (key >= KEY_F1 && key <= KEY_F6)
{
uint index = key - KEY_F1 + 5 * ui.page();
if (symbol_g sym = expression_variable(index))
{
object_g value = directory::recall_all(sym, false);
if (!value)
value = integer::make(0);
if (expression_g eq = expression::current_equation())
if (value && is_well_defined(eq, sym))
if (algebraic_g result = solve(+eq, sym, value))
if (directory::store_here(sym, result))
if (tag_p tagged = tagged_value(sym, result))
if (rt.push(tagged))
if (ui.menu_refresh())
return OK;
}
}
return ERROR;
}
INSERT_BODY(SolvingMenuSolve)
// ----------------------------------------------------------------------------
// Insert the name of a variable
// ----------------------------------------------------------------------------
{
int key = ui.evaluating;
return ui.insert_softkey(key, " EQ '", "' 0 Root ", false);
}

View file

@ -6,7 +6,7 @@
//
// File Description:
//
//
// Numerical solver and solver menu
//
//
//
@ -31,10 +31,36 @@
#include "algebraic.h"
#include "command.h"
#include "menu.h"
#include "symbol.h"
algebraic_p solve(program_g eq, symbol_g name, object_g guess);
COMMAND_DECLARE(Root,3);
COMMAND_DECLARE(StEq, 1);
COMMAND_DECLARE(RcEq, 0);
struct SolvingMenu : menu
// ----------------------------------------------------------------------------
// The solving menu is built dynamically from current expression
// ----------------------------------------------------------------------------
// The SolvingMenu shows expression variables in the menu
// For each variable, the function key has the three following features:
// - Unshifted sets the variable value
// - Shifted solves for the variable
// - XShifted recalls the variable value
{
SolvingMenu(id type = ID_SolvingMenu) : menu(type) {}
public:
OBJECT_DECL(SolvingMenu);
MENU_DECL(SolvingMenu);
};
COMMAND_DECLARE_INSERT(SolvingMenuStore,1);
COMMAND_DECLARE_INSERT(SolvingMenuSolve,0);
COMMAND_DECLARE_INSERT(SolvingMenuRecall,0);
#endif // SOLVE_H

View file

@ -468,6 +468,21 @@ object_p directory::recall_all(object_p name, bool report_missing)
}
bool directory::store_here(object_p name, object_p value)
// ----------------------------------------------------------------------------
// Store a variable in the current directory
// ----------------------------------------------------------------------------
{
directory *dir = rt.variables(0);
if (!dir)
{
rt.no_directory_error();
return false;
}
return dir->store(name, value);
}
size_t directory::purge(object_p name)
// ----------------------------------------------------------------------------
// Purge a name (and associated value) from the directory
@ -691,17 +706,10 @@ COMMAND_BODY(Sto)
// Store a global variable into current directory
// ----------------------------------------------------------------------------
{
directory *dir = rt.variables(0);
if (!dir)
{
rt.no_directory_error();
return ERROR;
}
// Check that we have two objects in the stack
object_p name = rt.stack(0);
object_p value = rt.stack(1);
if (name && value && dir->store(name, value))
if (name && value && directory::store_here(name, value))
{
rt.drop(2);
return OK;
@ -735,13 +743,6 @@ static object::result store_op(object::id op)
// Store with a given operation
// ----------------------------------------------------------------------------
{
directory *dir = rt.variables(0);
if (!dir)
{
rt.no_directory_error();
return object::ERROR;
}
object_g name = rt.stack(0);
object_g value = rt.stack(1);
if (!name || !value)
@ -755,7 +756,7 @@ static object::result store_op(object::id op)
if (object::result res = cmd->evaluate())
return res;
value = rt.pop();
if (value && dir->store(name, value))
if (value && directory::store_here(name, value))
return object::OK;
return object::ERROR;
}
@ -772,19 +773,11 @@ static object::result store_op(object::id op, object_p cstval)
// Store with a given operation
// ----------------------------------------------------------------------------
{
directory *dir = rt.variables(0);
if (!dir)
{
rt.no_directory_error();
return object::ERROR;
}
object_g name = rt.stack(0);
object_g value = cstval;
if (!name || !value)
return object::ERROR;
object_g existing = directory::recall_all(name, true);
if (!existing)
return object::ERROR;
rt.stack(0, existing);
@ -793,7 +786,7 @@ static object::result store_op(object::id op, object_p cstval)
if (object::result res = cmd->evaluate())
return res;
value = rt.top();
if (value && dir->store(name, value))
if (value && directory::store_here(name, value))
return object::OK;
return object::ERROR;
}
@ -822,13 +815,6 @@ static object::result recall_op(object::id op)
// Store with a given operation
// ----------------------------------------------------------------------------
{
directory *dir = rt.variables(0);
if (!dir)
{
rt.no_directory_error();
return object::ERROR;
}
object_g name = rt.stack(0);
if (!name)
return object::ERROR;

View file

@ -106,6 +106,11 @@ struct directory : list
// Check if a name exists in the directory, return value ptr if it does
// ------------------------------------------------------------------------
static bool store_here(object_p name, object_p value);
// ------------------------------------------------------------------------
// Store in the current directory, or fail if it does not exist
// ------------------------------------------------------------------------
object_p lookup(object_p name) const;
// ------------------------------------------------------------------------
// Check if a name exists in the directory, return name ptr if it does
@ -230,7 +235,7 @@ public:
MENU_DECL(VariablesMenu);
};
COMMAND_DECLARE_INSERT(VariablesMenuExecute,-1);
COMMAND_DECLARE_INSERT(VariablesMenuExecute,~0);
COMMAND_DECLARE_INSERT(VariablesMenuRecall,0);
COMMAND_DECLARE_INSERT(VariablesMenuStore,1);