From e35d8f236db443b480569d78f71bc58f6f3f6817 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Sat, 27 Apr 2024 21:44:00 -0500 Subject: [PATCH] feat: add automatic decimal to fraction conversion sheet setting This has been the default but there are cases where sheets may fail to solve because of the excessive length of some of the rational representations. When these situations occur, the error messages have been updated to recommend changing this setting to solve the issues. --- public/dimensional_analysis.py | 71 ++++++++++++++++++++----------- src/App.svelte | 14 ++++-- src/sheet/Sheet.ts | 7 ++- src/stores.ts | 2 +- src/types.ts | 1 + tests/test_number_format.spec.mjs | 32 ++++++++++++++ 6 files changed, 96 insertions(+), 31 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 02d4b963..dc8edf7c 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -441,6 +441,7 @@ class StatementsAndSystems(TypedDict): systemDefinitions: list[SystemDefinition] customBaseUnits: NotRequired[CustomBaseUnits] simplifySymbolicExpressions: bool + convertFloatsToFractions: bool def is_code_function_query_statement(statement: InputAndSystemStatement) -> TypeGuard[CodeFunctionQueryStatement]: return statement.get("isCodeFunctionQuery", False) @@ -787,8 +788,12 @@ def custom_latex(expression: Expr) -> str: try: result_latex = latex(new_expression) except ValueError as e: - result_latex = f"\\text{{Error generating symbolic result: {e}}}" - + result_latex = """ +\\begin{split} +&\\text{Error generating symbolic result.} \\\\ +&\\text{Try disabling the "Automatically Convert Decimal Values to Fractions" sheet setting.} +\\end{split} +""" result_latex = result_latex.replace('_{as variable}','') @@ -1245,10 +1250,10 @@ def as_int_if_int(expr: Expr | float) -> Expr | float: return expr -def get_parameter_subs(parameters: list[ImplicitParameter]): +def get_parameter_subs(parameters: list[ImplicitParameter], convert_floats_to_fractions: bool): # sub parameter values parameter_subs: dict[Symbol, Expr] = { - symbols(param["name"]): sympify(param["si_value"], rational=True) + symbols(param["name"]): sympify(param["si_value"], rational=convert_floats_to_fractions) for param in parameters if param["si_value"] is not None } @@ -1259,16 +1264,16 @@ def get_parameter_subs(parameters: list[ImplicitParameter]): def sympify_statements(statements: list[Statement] | list[EqualityStatement], - sympify_exponents=False): + sympify_exponents=False, convert_floats_to_fractions=True): for i, statement in enumerate(statements): statement["index"] = i if statement["type"] != "local_sub" and statement["type"] != "blank" and \ statement["type"] != "scatterQuery": try: - statement["expression"] = sympify(statement["sympy"], rational=True) + statement["expression"] = sympify(statement["sympy"], rational=convert_floats_to_fractions) if sympify_exponents: for exponent in cast(list[Exponent], statement["exponents"]): - exponent["expression"] = sympify(exponent["sympy"], rational=True) + exponent["expression"] = sympify(exponent["sympy"], rational=convert_floats_to_fractions) except SyntaxError: print(f"Parsing error for equation {statement['sympy']}") raise ParsingError @@ -1279,11 +1284,13 @@ def remove_implicit_and_exponent(input_set: set[str]) -> set[str]: if not variable.startswith( ("implicit_param__", "exponent__") )} -def solve_system(statements: list[EqualityStatement], variables: list[str]): +def solve_system(statements: list[EqualityStatement], variables: list[str], + convert_floats_to_fractions: bool): parameters = get_all_implicit_parameters(statements) - parameter_subs = get_parameter_subs(parameters) + parameter_subs = get_parameter_subs(parameters, convert_floats_to_fractions) - sympify_statements(statements, sympify_exponents=True) + sympify_statements(statements, sympify_exponents=True, + convert_floats_to_fractions=convert_floats_to_fractions) # give all of the statements an index so that they can be re-ordered for i, statement in enumerate(statements): @@ -1360,11 +1367,13 @@ def solve_system(statements: list[EqualityStatement], variables: list[str]): def solve_system_numerical(statements: list[EqualityStatement], variables: list[str], - guesses: list[str], guess_statements: list[GuessAssignmentStatement]): + guesses: list[str], guess_statements: list[GuessAssignmentStatement], + convert_floats_to_fractions: bool): parameters = get_all_implicit_parameters([*statements, *guess_statements]) - parameter_subs = get_parameter_subs(parameters) + parameter_subs = get_parameter_subs(parameters, convert_floats_to_fractions) - sympify_statements(statements, sympify_exponents=True) + sympify_statements(statements, sympify_exponents=True, + convert_floats_to_fractions=convert_floats_to_fractions) # give all of the statements an index so that they can be re-ordered for i, statement in enumerate(statements): @@ -1829,7 +1838,8 @@ def get_hashable_matrix_units(matrix_result: MatrixResult) -> tuple[tuple[str, . def evaluate_statements(statements: list[InputAndSystemStatement], custom_base_units: CustomBaseUnits | None, - simplify_symbolic_expressions: bool) -> tuple[list[Result | FiniteImagResult | list[PlotResult] | MatrixResult], dict[int,bool]]: + simplify_symbolic_expressions: bool, + convert_floats_to_fractions: bool) -> tuple[list[Result | FiniteImagResult | list[PlotResult] | MatrixResult], dict[int,bool]]: num_statements = len(statements) if num_statements == 0: @@ -1839,14 +1849,14 @@ def evaluate_statements(statements: list[InputAndSystemStatement], "cellNum": statement.get("cellNum", -1)} for statement in statements] parameters = get_all_implicit_parameters(statements) - parameter_subs = get_parameter_subs(parameters) + parameter_subs = get_parameter_subs(parameters, convert_floats_to_fractions) dimensional_analysis_subs: dict[Symbol, Expr] = { symbols(param["name"]): get_dims(param["dimensions"]) for param in parameters } expanded_statements: list[Statement] = expand_with_sub_statements(statements) - sympify_statements(expanded_statements) + sympify_statements(expanded_statements, convert_floats_to_fractions=convert_floats_to_fractions) expanded_statements = get_sorted_statements(expanded_statements) @@ -2161,13 +2171,16 @@ def evaluate_statements(statements: list[InputAndSystemStatement], def get_query_values(statements: list[InputAndSystemStatement], custom_base_units: CustomBaseUnits | None, - simplify_symbolic_expressions: bool): + simplify_symbolic_expressions: bool, + convert_floats_to_fractions: bool): error: None | str = None results: list[Result | FiniteImagResult | list[PlotResult] | MatrixResult] = [] numerical_system_cell_errors: dict[int, bool] = {} try: - results, numerical_system_cell_errors = evaluate_statements(statements, custom_base_units, simplify_symbolic_expressions) + results, numerical_system_cell_errors = evaluate_statements(statements, custom_base_units, + simplify_symbolic_expressions, + convert_floats_to_fractions) except DuplicateAssignment as e: error = f"Duplicate assignment of variable {e}" except ReferenceCycle as e: @@ -2178,6 +2191,8 @@ def get_query_values(statements: list[InputAndSystemStatement], error = "Cannot solve overdetermined system" except NoSolutionFound as e: error = "Unable to solve system of equations" + except MemoryError: + error = 'A MemoryError occurred while completing the calculation, try disabling the "Automatically Convert Decimal Values to Fractions" sheet setting' except Exception as e: print(f"Unhandled exception: {type(e).__name__}, {e}") error = f"Unhandled exception: {type(e).__name__}, {e}" @@ -2187,7 +2202,7 @@ def get_query_values(statements: list[InputAndSystemStatement], @lru_cache(maxsize=1024) -def get_system_solution(statements, variables): +def get_system_solution(statements, variables, convert_floats_to_fractions): statements = cast(list[EqualityStatement], loads(statements)) variables = cast(list[str], loads(variables)) @@ -2196,7 +2211,7 @@ def get_system_solution(statements, variables): display_solutions: dict[str, list[str]] try: - new_statements = solve_system(statements, variables) + new_statements = solve_system(statements, variables, convert_floats_to_fractions) except (ParameterError, ParsingError) as e: error = e.__class__.__name__ new_statements = [] @@ -2227,7 +2242,7 @@ def get_system_solution(statements, variables): @lru_cache(maxsize=1024) -def get_system_solution_numerical(statements, variables, guesses, guessStatements): +def get_system_solution_numerical(statements, variables, guesses, guessStatements, convert_floats_to_fractions): statements = cast(list[EqualityStatement], loads(statements)) variables = cast(list[str], loads(variables)) guesses = cast(list[str], loads(guesses)) @@ -2237,7 +2252,8 @@ def get_system_solution_numerical(statements, variables, guesses, guessStatement new_statements: list[list[EqualityUnitsQueryStatement | GuessAssignmentStatement]] = [] display_solutions: dict[str, list[str]] = {} try: - new_statements, display_solutions = solve_system_numerical(statements, variables, guesses, guess_statements) + new_statements, display_solutions = solve_system_numerical(statements, variables, guesses, + guess_statements, convert_floats_to_fractions) except (ParameterError, ParsingError) as e: error = e.__class__.__name__ except OverDeterminedSystem as e: @@ -2260,6 +2276,7 @@ def solve_sheet(statements_and_systems): system_definitions = statements_and_systems["systemDefinitions"] custom_base_units = statements_and_systems.get("customBaseUnits", None) simplify_symbolic_expressions = statements_and_systems["simplifySymbolicExpressions"] + convert_floats_to_fractions = statements_and_systems["convertFloatsToFractions"] system_results: list[SystemResult] = [] equation_to_system_cell_map: dict[int,int] = {} @@ -2276,7 +2293,8 @@ def solve_sheet(statements_and_systems): (system_error, system_solutions, display_solutions) = get_system_solution(dumps(system_definition["statements"]), - dumps(system_definition["variables"])) + dumps(system_definition["variables"]), + convert_floats_to_fractions) else: for statement in system_definition["statements"]: equation_to_system_cell_map[statement["equationIndex"]] = i @@ -2287,7 +2305,8 @@ def solve_sheet(statements_and_systems): display_solutions) = get_system_solution_numerical(dumps(system_definition["statements"]), dumps(system_definition["variables"]), dumps(system_definition["guesses"]), - dumps(system_definition["guessStatements"])) + dumps(system_definition["guessStatements"]), + convert_floats_to_fractions) if system_error is None: if selected_solution > len(system_solutions) - 1: @@ -2304,7 +2323,9 @@ def solve_sheet(statements_and_systems): error: str | None results: list[Result | FiniteImagResult | list[PlotResult] | MatrixResult] numerical_system_cell_errors: dict[int, bool] - error, results, numerical_system_cell_errors = get_query_values(statements, custom_base_units, simplify_symbolic_expressions) + error, results, numerical_system_cell_errors = get_query_values(statements, custom_base_units, + simplify_symbolic_expressions, + convert_floats_to_fractions) # If there was a numerical solve, check to make sure there were not unit mismatches # between the lhs and rhs of each equality in the system diff --git a/src/App.svelte b/src/App.svelte index 7a3baac1..04804390 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -840,7 +840,8 @@ statements: statements, systemDefinitions: systemDefinitions, customBaseUnits: $config.customBaseUnits, - simplifySymbolicExpressions: $config.simplifySymbolicExpressions + simplifySymbolicExpressions: $config.simplifySymbolicExpressions, + convertFloatsToFractions: $config.convertFloatsToFractions }; } @@ -1103,6 +1104,7 @@ Please include a link to this sheet in the email to assist in debugging the prob $config = sheet.config ?? getDefaultConfig(); $config.customBaseUnits = $config.customBaseUnits ?? getDefaultBaseUnits(); // customBaseUnits may not exist $config.simplifySymbolicExpressions = $config.simplifySymbolicExpressions ?? true; // simplifySymboicExpressions may not exist + $config.convertFloatsToFractions = $config.convertFloatsToFractions ?? true; // convertFloatsToFractions may not exist if (!$history.map(item => item.hash !== "file" ? getSheetHash(new URL(item.url)) : "").includes(getSheetHash(window.location))) { $history = requestHistory; @@ -2644,7 +2646,8 @@ Please include a link to this sheet in the email to assist in debugging the prob on:click:button--primary={() => modalInfo.modalOpen = false} on:click:button--secondary={() => {mathCellConfigDialog?.resetDefaults(); baseUnitsConfigDialog?.resetDefaults(); - $config.simplifySymbolicExpressions = true;}} + $config.simplifySymbolicExpressions = true; + $config.convertFloatsToFractions = true;}} bind:open={modalInfo.modalOpen} > {#if modalInfo.mathCell} @@ -2661,10 +2664,15 @@ Please include a link to this sheet in the email to assist in debugging the prob $mathCellChanged = true} /> + $mathCellChanged = true} + /> = writable([]); export const title = writable(defaultTitle); export const results: Writable<(Result | FiniteImagResult | MatrixResult | PlotResult[])[]> = writable([]); diff --git a/src/types.ts b/src/types.ts index feccaa2d..391e37c0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,6 +42,7 @@ export type StatementsAndSystems = { systemDefinitions: SystemDefinition[]; customBaseUnits?: CustomBaseUnits; simplifySymbolicExpressions: boolean; + convertFloatsToFractions: boolean; } diff --git a/tests/test_number_format.spec.mjs b/tests/test_number_format.spec.mjs index 53f46465..999e37e8 100644 --- a/tests/test_number_format.spec.mjs +++ b/tests/test_number_format.spec.mjs @@ -515,3 +515,35 @@ test('Test number input validation', async () => { expect(content).toBe('0.666666666666667'); }); + + +test('Test disabling automatic fraction conversion', async () => { + // turn off automatic simplification + await page.getByRole('button', { name: 'Sheet Settings' }).click(); + await page.locator('label').filter({ hasText: 'Automatically Convert Decimal Values to Fractions' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + + await page.setLatex(0, String.raw`\left(\frac{1.115625000065330001000001000010001}{1.355801000010000100001000010000100010}\right)^{\frac{1}{-.01780001000100010001}}=`); + + await page.waitForSelector('text=Updating...', {state: 'detached'}); + + // check output + let content = await page.textContent('#result-value-0'); + expect(parseLatexFloat(content)).toBeCloseTo(57170.5227437832, precision); +}); + + +test('Test default automatic fraction conversion setting', async () => { + await page.setLatex(0, String.raw`.125=`); + + // turn on symbolic results + await page.getByRole('button', { name: 'Sheet Settings' }).click(); + await page.locator('label').filter({ hasText: 'Display Symbolic Results' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + + await page.waitForSelector('text=Updating...', {state: 'detached'}); + + // check output + let content = await page.textContent('#result-value-0'); + expect(content).toBe(String.raw`\frac{1}{8}`); +});