From b108310d10d21f659d38c1be4db4916ef50f70d9 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Thu, 2 Jan 2025 23:26:50 -0600 Subject: [PATCH 01/53] refactor: simplify logic for preventing unit canceling Removes one recursive walk through the expression tree. --- public/dimensional_analysis.py | 39 ++++++---------------------------- src/parser/LatexToSympy.ts | 4 ++-- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index a5eac9b3..b531e64a 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -897,35 +897,6 @@ def custom_latex(expression: Expr) -> str: _range = Function("_range") -def walk_tree(grandparent_func, parent_func, expr) -> Expr: - - if is_matrix(expr): - rows = [] - for i in range(expr.rows): - row = [] - rows.append(row) - for j in range(expr.cols): - row.append(walk_tree(parent_func, Matrix, expr[i,j])) - - return cast(Expr, Matrix(rows)) - - if len(expr.args) == 0: - if parent_func is not Pow and parent_func is not Inverse and expr.is_negative: - return -1*expr - else: - return expr - - if expr.func == _range: - new_args = expr.args - else: - new_args = (walk_tree(parent_func, expr.func, arg) for arg in expr.args) - - return expr.func(*new_args) - -def subtraction_to_addition(expression: Expr | Matrix) -> Expr: - return walk_tree("root", "root", expression) - - def ensure_dims_all_compatible(*args): if args[0].is_zero: if all(arg.is_zero for arg in args): @@ -1184,6 +1155,9 @@ def custom_integral_dims(local_expr: Expr, global_expr: Expr, dummy_integral_var return global_expr * lower_limit_dims # type: ignore else: return global_expr * integral_var # type: ignore + +def custom_add_dims(*args: Expr): + return Add(*[Abs(arg) for arg in args]) CP = None @@ -1494,6 +1468,7 @@ def get_next_id(self): cast(Function, Function('_Integral')) : {"dim_func": custom_integral_dims, "sympy_func": custom_integral}, cast(Function, Function('_range')) : {"dim_func": custom_range, "sympy_func": custom_range}, cast(Function, Function('_factorial')) : {"dim_func": factorial, "sympy_func": CustomFactorial}, + cast(Function, Function('_add')) : {"dim_func": custom_add_dims, "sympy_func": Add}, } global_placeholder_set = set(global_placeholder_map.keys()) @@ -1612,10 +1587,8 @@ def get_dimensional_analysis_expression(parameter_subs: dict[Symbol, Expr], expression: Expr, placeholder_map: dict[Function, PlaceholderFunction], placeholder_set: set[Function]) -> tuple[Expr | None, Exception | None]: - # need to remove any subtractions or unary negative since this may - # lead to unintentional cancellation during the parameter substitution process - positive_only_expression = subtraction_to_addition(expression) - expression_with_parameter_subs = cast(Expr, positive_only_expression.xreplace(parameter_subs)) + + expression_with_parameter_subs = cast(Expr, expression.xreplace(parameter_subs)) error = None final_expression = None diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index 9986b70e..fce4b4f0 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -1987,11 +1987,11 @@ export class LatexToSympy extends LatexParserVisitor { - return `Add(${this.visit(ctx.expr(0))}, ${this.visit(ctx.expr(1))})`; + return `_add(${this.visit(ctx.expr(0))}, ${this.visit(ctx.expr(1))})`; } visitSubtract = (ctx: SubtractContext) => { - return `Add(${this.visit(ctx.expr(0))}, -(${this.visit(ctx.expr(1))}))`; + return `_add(${this.visit(ctx.expr(0))}, -(${this.visit(ctx.expr(1))}))`; } visitVariable = (ctx: VariableContext) => { From c9f5f73f91ac2a5e3d768d0dc50f4970be565d96 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Fri, 3 Jan 2025 13:28:03 -0600 Subject: [PATCH 02/53] refactor: move all multiplication dimensional analysis logic to placeholder function Simplifies replace_placeholder_funcs function by removing a special case --- public/dimensional_analysis.py | 57 ++++++++++++++++------------------ 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index b531e64a..ee12a18d 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -990,10 +990,32 @@ def custom_matmul(exp1: Expr, exp2: Expr): else: return Mul(exp1, exp2) -def custom_matmul_dims(*args: Expr): - if len(args) == 2 and is_matrix(args[0]) and is_matrix(args[1]) and \ +def custom_multiply_dims(matmult: bool, *args: Expr): + matrix_args: list[Matrix] = [] + scalar_args: list[Expr] = [] + for arg in args: + if is_matrix(arg): + matrix_args.append(arg) + else: + scalar_args.append(arg) + + if len(matrix_args) > 0 and len(scalar_args) > 0: + first_matrix = matrix_args[0] + scalar = Mul(*scalar_args) + new_rows = [] + for i in range(first_matrix.rows): + new_row = [] + new_rows.append(new_row) + for j in range(first_matrix.cols): + new_row.append(scalar*first_matrix[i,j]) # type: ignore + + matrix_args[0] = Matrix(new_rows) + args = cast(tuple[Expr], matrix_args) + + if matmult and len(args) == 2 and is_matrix(args[0]) and is_matrix(args[1]) and \ (((args[0].rows == 3 and args[0].cols == 1) and (args[1].rows == 3 and args[1].cols == 1)) or \ ((args[0].rows == 1 and args[0].cols == 3) and (args[1].rows == 1 and args[1].cols == 3))): + # cross product detected for matrix multiplication operator result = Matrix([Add(Mul(args[0][1],args[1][2]),Mul(args[0][2],args[1][1])), Add(Mul(args[0][2],args[1][0]),Mul(args[0][0],args[1][2])), @@ -1455,8 +1477,8 @@ def get_next_id(self): cast(Function, Function('_Inverse')) : {"dim_func": ensure_inverse_dims, "sympy_func": UniversalInverse}, cast(Function, Function('_Transpose')) : {"dim_func": custom_transpose, "sympy_func": custom_transpose}, cast(Function, Function('_Determinant')) : {"dim_func": custom_determinant, "sympy_func": custom_determinant}, - cast(Function, Function('_mat_multiply')) : {"dim_func": custom_matmul_dims, "sympy_func": custom_matmul}, - cast(Function, Function('_multiply')) : {"dim_func": Mul, "sympy_func": Mul}, + cast(Function, Function('_mat_multiply')) : {"dim_func": partial(custom_multiply_dims, True), "sympy_func": custom_matmul}, + cast(Function, Function('_multiply')) : {"dim_func": partial(custom_multiply_dims, False), "sympy_func": Mul}, cast(Function, Function('_IndexMatrix')) : {"dim_func": IndexMatrix, "sympy_func": IndexMatrix}, cast(Function, Function('_Eq')) : {"dim_func": Eq, "sympy_func": Eq}, cast(Function, Function('_norm')) : {"dim_func": custom_norm, "sympy_func": custom_norm}, @@ -1506,32 +1528,7 @@ def replace_placeholder_funcs(expr: Expr, if len(expr.args) == 0: return expr - if func_key == "dim_func" and expr.func in multiply_placeholder_set: - processed_args = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, data_table_subs) for arg in expr.args] - matrix_args = [] - scalar_args = [] - for arg in processed_args: - if is_matrix(cast(Expr, arg)): - matrix_args.append(arg) - else: - scalar_args.append(arg) - - if len(matrix_args) > 0 and len(scalar_args) > 0: - first_matrix = matrix_args[0] - scalar = math.prod(scalar_args) - new_rows = [] - for i in range(first_matrix.rows): - new_row = [] - new_rows.append(new_row) - for j in range(first_matrix.cols): - new_row.append(scalar*first_matrix[i,j]) - - matrix_args[0] = Matrix(new_rows) - - return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*matrix_args)) - else: - return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*processed_args)) - elif expr.func in dummy_var_placeholder_set and func_key == "dim_func": + if expr.func in dummy_var_placeholder_set and func_key == "dim_func": return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, data_table_subs) if index > 0 else arg for index, arg in enumerate(expr.args)))) elif expr.func in placeholder_set: return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, data_table_subs) for arg in expr.args))) From 4128bdf36ca446179641d3791d649fd4c1eb68e2 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Fri, 3 Jan 2025 17:12:49 -0600 Subject: [PATCH 03/53] refactor: remove unused variable --- public/dimensional_analysis.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index ee12a18d..1a8387a0 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1016,7 +1016,6 @@ def custom_multiply_dims(matmult: bool, *args: Expr): (((args[0].rows == 3 and args[0].cols == 1) and (args[1].rows == 3 and args[1].cols == 1)) or \ ((args[0].rows == 1 and args[0].cols == 3) and (args[1].rows == 1 and args[1].cols == 3))): # cross product detected for matrix multiplication operator - result = Matrix([Add(Mul(args[0][1],args[1][2]),Mul(args[0][2],args[1][1])), Add(Mul(args[0][2],args[1][0]),Mul(args[0][0],args[1][2])), Add(Mul(args[0][0],args[1][1]),Mul(args[0][1],args[1][0]))]) @@ -1495,7 +1494,6 @@ def get_next_id(self): global_placeholder_set = set(global_placeholder_map.keys()) dummy_var_placeholder_set = (Function('_Derivative'), Function('_Integral')) -multiply_placeholder_set = (Function('_multiply'), Function('_mat_multiply')) placeholder_inverse_map = { value["sympy_func"]: key for key, value in reversed(global_placeholder_map.items()) } placeholder_inverse_set = set(placeholder_inverse_map.keys()) From 0aa0e43c48057ac91dda3174f9ed75966b65f17a Mon Sep 17 00:00:00 2001 From: mgreminger Date: Sat, 4 Jan 2025 22:17:18 -0600 Subject: [PATCH 04/53] refactor: eliminate need for unitless expression pass Values needed for dimensional analysis are stored in the evaluation pass instead --- public/dimensional_analysis.py | 93 ++++++++++++++++++++++++---------- src/parser/LatexToSympy.ts | 61 +++------------------- src/parser/utility.ts | 11 +++- 3 files changed, 83 insertions(+), 82 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 1a8387a0..ff6ad06f 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1121,6 +1121,16 @@ def IndexMatrix(expression: Expr, i: Expr, j: Expr) -> Expr: return expression[i-1, j-1] # type: ignore +def IndexMatrix_dims(dim_values: list[Expr], expression: Expr, i: Expr, j: Expr) -> Expr: + if custom_get_dimensional_dependencies(i) != {} or \ + custom_get_dimensional_dependencies(j) != {}: + raise TypeError('Matrix Index Not Dimensionless') + + i_value = dim_values[1] + j_value = dim_values[2] + + return expression[i-1, j-1] # type: ignore + class CustomFactorial(Function): is_real = True @@ -1180,6 +1190,10 @@ def custom_integral_dims(local_expr: Expr, global_expr: Expr, dummy_integral_var def custom_add_dims(*args: Expr): return Add(*[Abs(arg) for arg in args]) +def custom_pow_dims(dim_values: list[Expr], base: Expr, exponent: Expr): + if custom_get_dimensional_dependencies(exponent) != {}: + raise TypeError('Exponent Not Dimensionless') + return base**dim_values[1] CP = None @@ -1447,6 +1461,8 @@ def __init__(self): def get_next_id(self): self._next_id += 1 return self._next_id-1 + +dim_needs_values_wrapper = Function('_dim_needs_values_wrapper') global_placeholder_map: dict[Function, PlaceholderFunction] = { cast(Function, Function('_StrictLessThan')) : {"dim_func": ensure_dims_all_compatible, "sympy_func": StrictLessThan}, @@ -1478,7 +1494,7 @@ def get_next_id(self): cast(Function, Function('_Determinant')) : {"dim_func": custom_determinant, "sympy_func": custom_determinant}, cast(Function, Function('_mat_multiply')) : {"dim_func": partial(custom_multiply_dims, True), "sympy_func": custom_matmul}, cast(Function, Function('_multiply')) : {"dim_func": partial(custom_multiply_dims, False), "sympy_func": Mul}, - cast(Function, Function('_IndexMatrix')) : {"dim_func": IndexMatrix, "sympy_func": IndexMatrix}, + cast(Function, Function('_IndexMatrix')) : {"dim_func": IndexMatrix_dims, "sympy_func": IndexMatrix}, cast(Function, Function('_Eq')) : {"dim_func": Eq, "sympy_func": Eq}, cast(Function, Function('_norm')) : {"dim_func": custom_norm, "sympy_func": custom_norm}, cast(Function, Function('_dot')) : {"dim_func": custom_dot, "sympy_func": custom_dot}, @@ -1490,10 +1506,12 @@ def get_next_id(self): cast(Function, Function('_range')) : {"dim_func": custom_range, "sympy_func": custom_range}, cast(Function, Function('_factorial')) : {"dim_func": factorial, "sympy_func": CustomFactorial}, cast(Function, Function('_add')) : {"dim_func": custom_add_dims, "sympy_func": Add}, + cast(Function, Function('_Pow')) : {"dim_func": custom_pow_dims, "sympy_func": Pow}, } global_placeholder_set = set(global_placeholder_map.keys()) dummy_var_placeholder_set = (Function('_Derivative'), Function('_Integral')) +dim_needs_values_wrapper_placeholder_set = (Function('_Pow')) placeholder_inverse_map = { value["sympy_func"]: key for key, value in reversed(global_placeholder_map.items()) } placeholder_inverse_set = set(placeholder_inverse_map.keys()) @@ -1510,6 +1528,7 @@ def replace_placeholder_funcs(expr: Expr, func_key: Literal["dim_func"] | Literal["sympy_func"], placeholder_map: dict[Function, PlaceholderFunction], placeholder_set: set[Function], + dim_values_dict: dict[int, list[Expr]], data_table_subs: DataTableSubs | None) -> Expr: if is_matrix(expr): rows = [] @@ -1519,25 +1538,42 @@ def replace_placeholder_funcs(expr: Expr, for j in range(expr.cols): row.append(replace_placeholder_funcs(cast(Expr, expr[i,j]), func_key, placeholder_map, placeholder_set, - data_table_subs) ) + dim_values_dict, data_table_subs) ) return cast(Expr, Matrix(rows)) if len(expr.args) == 0: return expr - if expr.func in dummy_var_placeholder_set and func_key == "dim_func": - return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, data_table_subs) if index > 0 else arg for index, arg in enumerate(expr.args)))) + if expr.func == dim_needs_values_wrapper: + if func_key == "sympy_func": + child_expr = expr.args[1] + dim_values = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) for arg in child_expr.args] + dim_values_dict[int(cast(Expr, expr.args[0]))] = dim_values + return cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(*dim_values)) + else: + child_expr = expr.args[1] + dim_values = dim_values_dict[int(cast(Expr, expr.args[0]))] + child_processed_args = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) for arg in child_expr.args] + return cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(dim_values, *child_processed_args)) + elif expr.func in dummy_var_placeholder_set and func_key == "dim_func": + if expr.func in dim_needs_values_wrapper_placeholder_set: + # Reached a dim function that needs values to analyze dims (exponent, for example) + # This path will only be reached in the case of a expression resulting from a system solve + # Will not have values so fall back to raw sympy function + return cast(Expr, cast(Callable, placeholder_map[expr.func]["sympy_func"])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) if index > 0 else arg for index, arg in enumerate(expr.args)))) + else: + return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) if index > 0 else arg for index, arg in enumerate(expr.args)))) elif expr.func in placeholder_set: - return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, data_table_subs) for arg in expr.args))) + return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) for arg in expr.args))) elif data_table_subs is not None and expr.func == data_table_calc_wrapper: if len(expr.args[0].atoms(data_table_id_wrapper)) == 0: - return replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, data_table_subs) + return replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) data_table_subs.subs_stack.append({}) data_table_subs.shortest_col_stack.append(None) - sub_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, data_table_subs) + sub_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) subs = data_table_subs.subs_stack.pop() shortest_col = data_table_subs.shortest_col_stack.pop() @@ -1558,7 +1594,7 @@ def replace_placeholder_funcs(expr: Expr, return cast(Expr, Matrix([sub_expr,]*shortest_col)) elif data_table_subs is not None and expr.func == data_table_id_wrapper: - current_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, data_table_subs) + current_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) new_var = Symbol(f"_data_table_var_{data_table_subs.get_next_id()}") if not is_matrix(current_expr): @@ -1576,12 +1612,13 @@ def replace_placeholder_funcs(expr: Expr, return cast(Expr, current_expr[0,0]) else: - return cast(Expr, expr.func(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, data_table_subs) for arg in expr.args))) + return cast(Expr, expr.func(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) for arg in expr.args))) def get_dimensional_analysis_expression(parameter_subs: dict[Symbol, Expr], expression: Expr, placeholder_map: dict[Function, PlaceholderFunction], - placeholder_set: set[Function]) -> tuple[Expr | None, Exception | None]: + placeholder_set: set[Function], + dim_values_dict: dict[int, list[Expr]]) -> tuple[Expr | None, Exception | None]: expression_with_parameter_subs = cast(Expr, expression.xreplace(parameter_subs)) @@ -1591,7 +1628,7 @@ def get_dimensional_analysis_expression(parameter_subs: dict[Symbol, Expr], try: final_expression = replace_placeholder_funcs(expression_with_parameter_subs, "dim_func", placeholder_map, placeholder_set, - DataTableSubs()) + dim_values_dict, DataTableSubs()) except Exception as e: error = e @@ -1628,9 +1665,9 @@ def dimensional_analysis(dimensional_analysis_expression: Expr | None, dim_sub_e custom_units_defined = True except TypeError as e: - print(f"Dimension Error: {e}") - result = "Dimension Error" - result_latex = "Dimension Error" + result = f"Dimension Error: {e}" + result_latex = result + print(result) return result, result_latex, custom_units_defined, custom_units, custom_units_latex @@ -1819,7 +1856,7 @@ def solve_system(statements: list[EqualityStatement], variables: list[str], {unitless_sub_expression["name"]:unitless_sub_expression["expression"] for unitless_sub_expression in cast(list[UnitlessSubExpression], statement["unitlessSubExpressions"])}) equality = replace_placeholder_funcs(cast(Expr, equality), "sympy_func", - placeholder_map, placeholder_set, None) + placeholder_map, placeholder_set, {}, None) system.append(cast(Expr, equality.doit())) @@ -1910,7 +1947,7 @@ def solve_system_numerical(statements: list[EqualityStatement], variables: list[ equality = equality.subs(parameter_subs) equality = replace_placeholder_funcs(cast(Expr, equality), "sympy_func", - placeholder_map, placeholder_set, None) + placeholder_map, placeholder_set, {}, None) system.append(cast(Expr, equality.doit())) new_statements.extend(statement["equalityUnitsQueries"]) @@ -2341,12 +2378,13 @@ def get_evaluated_expression(expression: Expr, parameter_subs: dict[Symbol, Expr], simplify_symbolic_expressions: bool, placeholder_map: dict[Function, PlaceholderFunction], - placeholder_set: set[Function]) -> tuple[ExprWithAssumptions, str | list[list[str]]]: + placeholder_set: set[Function]) -> tuple[ExprWithAssumptions, str | list[list[str]], dict[int,list[Expr]]]: expression = cast(Expr, expression.xreplace(parameter_subs)) + dim_values_dict: dict[int,list[Expr]] = {} expression = replace_placeholder_funcs(expression, "sympy_func", placeholder_map, - placeholder_set, + placeholder_set, dim_values_dict, DataTableSubs()) if not is_matrix(expression): if simplify_symbolic_expressions: @@ -2371,7 +2409,7 @@ def get_evaluated_expression(expression: Expr, row.append(custom_latex(cast(Expr, expression[i,j]))) evaluated_expression = cast(ExprWithAssumptions, expression.evalf(PRECISION)) - return evaluated_expression, symbolic_expression + return evaluated_expression, symbolic_expression, dim_values_dict def get_result(evaluated_expression: ExprWithAssumptions, dimensional_analysis_expression: Expr | None, dim_sub_error: Exception | None, symbolic_expression: str, @@ -2581,7 +2619,7 @@ def evaluate_statements(statements: list[InputAndSystemStatement], dimensional_analysis_expression, dim_sub_error = get_dimensional_analysis_expression(dimensional_analysis_subs, final_expression, placeholder_map, - placeholder_set) + placeholder_set, {}) dim, _, _, _, _ = dimensional_analysis(dimensional_analysis_expression, dim_sub_error) if dim == "": unit_sub_expression_dimensionless[unitless_sub_expression_name+current_function_name] = True @@ -2592,7 +2630,7 @@ def evaluate_statements(statements: list[InputAndSystemStatement], final_expression = replace_placeholder_funcs(final_expression, "sympy_func", placeholder_map, - placeholder_set, + placeholder_set, {}, None) unitless_sub_expression_subs[symbols(unitless_sub_expression_name+current_function_name)] = final_expression @@ -2701,15 +2739,16 @@ def evaluate_statements(statements: list[InputAndSystemStatement], else: expression = cast(Expr, item["expression"].doit()) - evaluated_expression, symbolic_expression = get_evaluated_expression(expression, - parameter_subs, - simplify_symbolic_expressions, - placeholder_map, - placeholder_set) + evaluated_expression, symbolic_expression, dim_values_dict = get_evaluated_expression(expression, + parameter_subs, + simplify_symbolic_expressions, + placeholder_map, + placeholder_set) dimensional_analysis_expression, dim_sub_error = get_dimensional_analysis_expression(dimensional_analysis_subs, expression, placeholder_map, - placeholder_set) + placeholder_set, + dim_values_dict) if not is_matrix(evaluated_expression): results[index] = get_result(evaluated_expression, dimensional_analysis_expression, diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index fce4b4f0..4b62f31e 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -13,9 +13,8 @@ import type { FieldTypes, Statement, QueryStatement, RangeQueryStatement, UserFu ScatterXValuesQueryStatement, ScatterYValuesQueryStatement, DataTableInfo, DataTableQueryStatement, BlankStatement, SubQueryStatement} from "./types"; -import { isInsertion, isReplacement, - type Insertion, type Replacement, applyEdits, - createSubQuery} from "./utility"; +import { type Insertion, type Replacement, applyEdits, + createSubQuery, cantorPairing } from "./utility"; import { RESERVED, GREEK_CHARS, UNASSIGNABLE, COMPARISON_MAP, UNITS_WITH_OFFSET, TYPE_PARSING_ERRORS, BUILTIN_FUNCTION_MAP, @@ -180,6 +179,8 @@ export class LatexToSympy extends LatexParserVisitor { - const exponentVariableName = this.getNextUnitlessSubExpressionName(); - + visitExponent = (ctx: ExponentContext) => { let base: string; let cursor: number; let exponent: string @@ -1151,61 +1150,15 @@ export class LatexToSympy extends LatexParserVisitor { - const rowVariableName = this.getNextUnitlessSubExpressionName(); - - let cursor = this.params.length; const rowExpression = this.visit(ctx.expr(1)) as string; - - this.unitlessSubExpressions.push({ - type: "assignment", - name: rowVariableName, - sympy: rowExpression, - params: this.params.slice(cursor), - isUnitlessSubExpression: true, - unitlessContext: "Matrix Index", - isFunctionArgument: false, - isFunction: false, - unitlessSubExpressions: [] - }); - this.params.push(rowVariableName); - - const colVariableName = this.getNextUnitlessSubExpressionName(); - cursor = this.params.length; const colExpression = this.visit(ctx.expr(2)) as string; - this.unitlessSubExpressions.push({ - type: "assignment", - name: colVariableName, - sympy: colExpression, - params: this.params.slice(cursor), - isUnitlessSubExpression: true, - unitlessContext: "Matrix Index", - isFunctionArgument: false, - isFunction: false, - unitlessSubExpressions: [] - }); - this.params.push(colVariableName); - - return `_IndexMatrix(${this.visit(ctx.expr(0))}, ${rowVariableName}, ${colVariableName})`; + return `_dim_needs_values_wrapper(${cantorPairing(this.equationIndex,this.dimNeedsValuesIndex++)},_IndexMatrix(${this.visit(ctx.expr(0))}, ${rowExpression}, ${colExpression}))`; } visitArgument = (ctx: ArgumentContext): (LocalSubstitution | LocalSubstitutionRange)[] => { diff --git a/src/parser/utility.ts b/src/parser/utility.ts index f0909e3b..e1d21558 100644 --- a/src/parser/utility.ts +++ b/src/parser/utility.ts @@ -112,4 +112,13 @@ export function createSubQuery(name: string): SubQueryStatement { isCodeFunctionQuery: false, isCodeFunctionRawQuery: false }; -} \ No newline at end of file +} + +// Implementation of the cantor pairing function https://en.wikipedia.org/wiki/Pairing_function +// Creates a unit mapping from the integer pair (a,b) to an integer +export function cantorPairing(a: number, b: number) { + const A = BigInt(a); + const B = BigInt(b) + const result = (((A+B)*(A+B+BigInt(1))) >> BigInt(1)) + B; + return result.toString(); +} From 439bee29261a4fcd8935bfaccb838f7bd2a89c8a Mon Sep 17 00:00:00 2001 From: mgreminger Date: Mon, 6 Jan 2025 10:35:43 -0600 Subject: [PATCH 05/53] fix: fix user function regressions Code generation still has issues --- public/dimensional_analysis.py | 66 ++++++++++++++++++++-------------- src/parser/LatexToSympy.ts | 8 +++-- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index ff6ad06f..aeb44c83 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1463,6 +1463,7 @@ def get_next_id(self): return self._next_id-1 dim_needs_values_wrapper = Function('_dim_needs_values_wrapper') +function_id_wrapper = Function('_function_id_wrapper') global_placeholder_map: dict[Function, PlaceholderFunction] = { cast(Function, Function('_StrictLessThan')) : {"dim_func": ensure_dims_all_compatible, "sympy_func": StrictLessThan}, @@ -1511,7 +1512,7 @@ def get_next_id(self): global_placeholder_set = set(global_placeholder_map.keys()) dummy_var_placeholder_set = (Function('_Derivative'), Function('_Integral')) -dim_needs_values_wrapper_placeholder_set = (Function('_Pow')) +dim_needs_values_wrapper_placeholder_set = (Function('_Pow'), Function('_IndexMatrix')) placeholder_inverse_map = { value["sympy_func"]: key for key, value in reversed(global_placeholder_map.items()) } placeholder_inverse_set = set(placeholder_inverse_map.keys()) @@ -1528,8 +1529,14 @@ def replace_placeholder_funcs(expr: Expr, func_key: Literal["dim_func"] | Literal["sympy_func"], placeholder_map: dict[Function, PlaceholderFunction], placeholder_set: set[Function], - dim_values_dict: dict[int, list[Expr]], + dim_values_dict: dict[tuple[int,...], list[Expr]], + function_parents: list[int], data_table_subs: DataTableSubs | None) -> Expr: + + if (not is_matrix(expr)) and expr.func == function_id_wrapper: + function_parents.append(int(cast(Expr, expr.args[0]))) + expr = cast(Expr, expr.args[1]) + if is_matrix(expr): rows = [] for i in range(expr.rows): @@ -1538,42 +1545,47 @@ def replace_placeholder_funcs(expr: Expr, for j in range(expr.cols): row.append(replace_placeholder_funcs(cast(Expr, expr[i,j]), func_key, placeholder_map, placeholder_set, - dim_values_dict, data_table_subs) ) + dim_values_dict, function_parents, + data_table_subs) ) return cast(Expr, Matrix(rows)) + expr = cast(Expr,expr) + if len(expr.args) == 0: return expr if expr.func == dim_needs_values_wrapper: if func_key == "sympy_func": child_expr = expr.args[1] - dim_values = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) for arg in child_expr.args] - dim_values_dict[int(cast(Expr, expr.args[0]))] = dim_values + function_parents_snapshot = list(function_parents) + dim_values = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) for arg in child_expr.args] + if data_table_subs is not None and len(data_table_subs.subs_stack) > 0: + dim_values_snapshot = list(dim_values) + for i, value in enumerate(dim_values_snapshot): + dim_values_snapshot[i] = cast(Expr, value.subs({key: cast(Matrix, value)[0,0] for key, value in data_table_subs.subs_stack[-1].items()})) + dim_values_dict[(int(cast(Expr, expr.args[0])), *function_parents_snapshot)] = dim_values_snapshot + else: + dim_values_dict[(int(cast(Expr, expr.args[0])), *function_parents_snapshot)] = dim_values return cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(*dim_values)) else: child_expr = expr.args[1] - dim_values = dim_values_dict[int(cast(Expr, expr.args[0]))] - child_processed_args = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) for arg in child_expr.args] + dim_values = dim_values_dict[(int(cast(Expr, expr.args[0])),*function_parents)] + child_processed_args = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) for arg in child_expr.args] return cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(dim_values, *child_processed_args)) elif expr.func in dummy_var_placeholder_set and func_key == "dim_func": - if expr.func in dim_needs_values_wrapper_placeholder_set: - # Reached a dim function that needs values to analyze dims (exponent, for example) - # This path will only be reached in the case of a expression resulting from a system solve - # Will not have values so fall back to raw sympy function - return cast(Expr, cast(Callable, placeholder_map[expr.func]["sympy_func"])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) if index > 0 else arg for index, arg in enumerate(expr.args)))) - else: - return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) if index > 0 else arg for index, arg in enumerate(expr.args)))) + return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) if index > 0 else arg for index, arg in enumerate(expr.args)))) elif expr.func in placeholder_set: - return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) for arg in expr.args))) + return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) for arg in expr.args))) + elif data_table_subs is not None and expr.func == data_table_calc_wrapper: if len(expr.args[0].atoms(data_table_id_wrapper)) == 0: - return replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) + return replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) data_table_subs.subs_stack.append({}) data_table_subs.shortest_col_stack.append(None) - sub_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) + sub_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) subs = data_table_subs.subs_stack.pop() shortest_col = data_table_subs.shortest_col_stack.pop() @@ -1594,7 +1606,7 @@ def replace_placeholder_funcs(expr: Expr, return cast(Expr, Matrix([sub_expr,]*shortest_col)) elif data_table_subs is not None and expr.func == data_table_id_wrapper: - current_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) + current_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) new_var = Symbol(f"_data_table_var_{data_table_subs.get_next_id()}") if not is_matrix(current_expr): @@ -1612,13 +1624,13 @@ def replace_placeholder_funcs(expr: Expr, return cast(Expr, current_expr[0,0]) else: - return cast(Expr, expr.func(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, data_table_subs) for arg in expr.args))) + return cast(Expr, expr.func(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) for arg in expr.args))) def get_dimensional_analysis_expression(parameter_subs: dict[Symbol, Expr], expression: Expr, placeholder_map: dict[Function, PlaceholderFunction], placeholder_set: set[Function], - dim_values_dict: dict[int, list[Expr]]) -> tuple[Expr | None, Exception | None]: + dim_values_dict: dict[tuple[int,...], list[Expr]]) -> tuple[Expr | None, Exception | None]: expression_with_parameter_subs = cast(Expr, expression.xreplace(parameter_subs)) @@ -1628,7 +1640,7 @@ def get_dimensional_analysis_expression(parameter_subs: dict[Symbol, Expr], try: final_expression = replace_placeholder_funcs(expression_with_parameter_subs, "dim_func", placeholder_map, placeholder_set, - dim_values_dict, DataTableSubs()) + dim_values_dict, [], DataTableSubs()) except Exception as e: error = e @@ -1856,7 +1868,7 @@ def solve_system(statements: list[EqualityStatement], variables: list[str], {unitless_sub_expression["name"]:unitless_sub_expression["expression"] for unitless_sub_expression in cast(list[UnitlessSubExpression], statement["unitlessSubExpressions"])}) equality = replace_placeholder_funcs(cast(Expr, equality), "sympy_func", - placeholder_map, placeholder_set, {}, None) + placeholder_map, placeholder_set, {}, [], None) system.append(cast(Expr, equality.doit())) @@ -1947,7 +1959,7 @@ def solve_system_numerical(statements: list[EqualityStatement], variables: list[ equality = equality.subs(parameter_subs) equality = replace_placeholder_funcs(cast(Expr, equality), "sympy_func", - placeholder_map, placeholder_set, {}, None) + placeholder_map, placeholder_set, {}, [], None) system.append(cast(Expr, equality.doit())) new_statements.extend(statement["equalityUnitsQueries"]) @@ -2378,13 +2390,13 @@ def get_evaluated_expression(expression: Expr, parameter_subs: dict[Symbol, Expr], simplify_symbolic_expressions: bool, placeholder_map: dict[Function, PlaceholderFunction], - placeholder_set: set[Function]) -> tuple[ExprWithAssumptions, str | list[list[str]], dict[int,list[Expr]]]: + placeholder_set: set[Function]) -> tuple[ExprWithAssumptions, str | list[list[str]], dict[tuple[int,...],list[Expr]]]: expression = cast(Expr, expression.xreplace(parameter_subs)) - dim_values_dict: dict[int,list[Expr]] = {} + dim_values_dict: dict[tuple[int,...],list[Expr]] = {} expression = replace_placeholder_funcs(expression, "sympy_func", placeholder_map, - placeholder_set, dim_values_dict, + placeholder_set, dim_values_dict, [], DataTableSubs()) if not is_matrix(expression): if simplify_symbolic_expressions: @@ -2630,7 +2642,7 @@ def evaluate_statements(statements: list[InputAndSystemStatement], final_expression = replace_placeholder_funcs(final_expression, "sympy_func", placeholder_map, - placeholder_set, {}, + placeholder_set, {}, [], None) unitless_sub_expression_subs[symbols(unitless_sub_expression_name+current_function_name)] = final_expression diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index 4b62f31e..9391203c 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -1095,8 +1095,10 @@ export class LatexToSympy extends LatexParserVisitor Date: Mon, 6 Jan 2025 11:40:40 -0600 Subject: [PATCH 06/53] fix: fix matrix indexing regression --- public/dimensional_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index aeb44c83..406e54d1 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1129,7 +1129,7 @@ def IndexMatrix_dims(dim_values: list[Expr], expression: Expr, i: Expr, j: Expr) i_value = dim_values[1] j_value = dim_values[2] - return expression[i-1, j-1] # type: ignore + return expression[i_value-1, j_value-1] # type: ignore class CustomFactorial(Function): is_real = True From 014090a8084eec5d3487825612f4832cdda3305c Mon Sep 17 00:00:00 2001 From: mgreminger Date: Mon, 6 Jan 2025 16:03:03 -0600 Subject: [PATCH 07/53] fix: fix code generation regression --- src/parser/LatexToSympy.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index 9391203c..c4cc3a7a 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -679,11 +679,13 @@ export class LatexToSympy extends LatexParserVisitor Date: Mon, 6 Jan 2025 16:45:58 -0600 Subject: [PATCH 08/53] fix: fix intermediate result rendering regression --- src/MathCell.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MathCell.svelte b/src/MathCell.svelte index 868477b0..af9ab5a1 100644 --- a/src/MathCell.svelte +++ b/src/MathCell.svelte @@ -335,7 +335,7 @@ const currentResultLatex = getLatexResult(createSubQuery(sympyVar), subResults.get(sympyVar), numberConfig); let newLatex: string; if (currentResultLatex.error) { - newLatex = String.raw`\text{${currentResultLatex.error}}`; + newLatex = String.raw`\text{${currentResultLatex.error.startsWith("Dimension Error:") ? "Dimension Error" : currentResultLatex.error}}`; } else { newLatex = ` ${currentResultLatex.resultLatex}${currentResultLatex.resultUnitsLatex} `; } From 8c61541925022c822a73ddea03c2fdbe9603fa06 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Mon, 6 Jan 2025 18:40:16 -0600 Subject: [PATCH 09/53] refactor: remove unnecessary lines --- public/dimensional_analysis.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 406e54d1..c18fe1ec 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -2817,9 +2817,6 @@ def evaluate_statements(statements: list[InputAndSystemStatement], if item["isFunctionArgument"] or item["isUnitsQuery"]: range_dependencies[item["name"]] = cast(Result | FiniteImagResult | MatrixResult, results[index]) - - if item["isCodeFunctionRawQuery"]: - code_func_raw_results[item["name"]] = cast(CombinedExpressionNoRange, item) if item["isCodeFunctionRawQuery"]: current_result = item From 87f8bf0718deec498554a1880a56368b83ce3513 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Mon, 6 Jan 2025 20:48:09 -0600 Subject: [PATCH 10/53] fix: fix plot label regression --- src/parser/LatexToSympy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index c4cc3a7a..c5da534e 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -649,7 +649,7 @@ export class LatexToSympy extends LatexParserVisitor Date: Mon, 6 Jan 2025 22:07:50 -0600 Subject: [PATCH 11/53] refactor: reduce complexity for dims need values logic Use strings instead of BigInt for efficiency --- public/dimensional_analysis.py | 21 +++++++++++---------- src/parser/LatexToSympy.ts | 12 ++++++------ src/parser/utility.ts | 8 -------- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index c18fe1ec..140f23d9 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -63,7 +63,8 @@ ceiling, sign, sqrt, - factorial + factorial, + Basic ) class ExprWithAssumptions(Expr): @@ -1529,12 +1530,12 @@ def replace_placeholder_funcs(expr: Expr, func_key: Literal["dim_func"] | Literal["sympy_func"], placeholder_map: dict[Function, PlaceholderFunction], placeholder_set: set[Function], - dim_values_dict: dict[tuple[int,...], list[Expr]], - function_parents: list[int], + dim_values_dict: dict[tuple[Basic,...], list[Expr]], + function_parents: list[Basic], data_table_subs: DataTableSubs | None) -> Expr: if (not is_matrix(expr)) and expr.func == function_id_wrapper: - function_parents.append(int(cast(Expr, expr.args[0]))) + function_parents.append(expr.args[0]) expr = cast(Expr, expr.args[1]) if is_matrix(expr): @@ -1564,13 +1565,13 @@ def replace_placeholder_funcs(expr: Expr, dim_values_snapshot = list(dim_values) for i, value in enumerate(dim_values_snapshot): dim_values_snapshot[i] = cast(Expr, value.subs({key: cast(Matrix, value)[0,0] for key, value in data_table_subs.subs_stack[-1].items()})) - dim_values_dict[(int(cast(Expr, expr.args[0])), *function_parents_snapshot)] = dim_values_snapshot + dim_values_dict[(expr.args[0], *function_parents_snapshot)] = dim_values_snapshot else: - dim_values_dict[(int(cast(Expr, expr.args[0])), *function_parents_snapshot)] = dim_values + dim_values_dict[(expr.args[0], *function_parents_snapshot)] = dim_values return cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(*dim_values)) else: child_expr = expr.args[1] - dim_values = dim_values_dict[(int(cast(Expr, expr.args[0])),*function_parents)] + dim_values = dim_values_dict[(expr.args[0],*function_parents)] child_processed_args = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) for arg in child_expr.args] return cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(dim_values, *child_processed_args)) elif expr.func in dummy_var_placeholder_set and func_key == "dim_func": @@ -1630,7 +1631,7 @@ def get_dimensional_analysis_expression(parameter_subs: dict[Symbol, Expr], expression: Expr, placeholder_map: dict[Function, PlaceholderFunction], placeholder_set: set[Function], - dim_values_dict: dict[tuple[int,...], list[Expr]]) -> tuple[Expr | None, Exception | None]: + dim_values_dict: dict[tuple[Basic,...], list[Expr]]) -> tuple[Expr | None, Exception | None]: expression_with_parameter_subs = cast(Expr, expression.xreplace(parameter_subs)) @@ -2390,9 +2391,9 @@ def get_evaluated_expression(expression: Expr, parameter_subs: dict[Symbol, Expr], simplify_symbolic_expressions: bool, placeholder_map: dict[Function, PlaceholderFunction], - placeholder_set: set[Function]) -> tuple[ExprWithAssumptions, str | list[list[str]], dict[tuple[int,...],list[Expr]]]: + placeholder_set: set[Function]) -> tuple[ExprWithAssumptions, str | list[list[str]], dict[tuple[Basic,...],list[Expr]]]: expression = cast(Expr, expression.xreplace(parameter_subs)) - dim_values_dict: dict[tuple[int,...],list[Expr]] = {} + dim_values_dict: dict[tuple[Basic,...],list[Expr]] = {} expression = replace_placeholder_funcs(expression, "sympy_func", placeholder_map, diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index c5da534e..29bd185b 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -14,7 +14,7 @@ import type { FieldTypes, Statement, QueryStatement, RangeQueryStatement, UserFu DataTableInfo, DataTableQueryStatement, BlankStatement, SubQueryStatement} from "./types"; import { type Insertion, type Replacement, applyEdits, - createSubQuery, cantorPairing } from "./utility"; + createSubQuery } from "./utility"; import { RESERVED, GREEK_CHARS, UNASSIGNABLE, COMPARISON_MAP, UNITS_WITH_OFFSET, TYPE_PARSING_ERRORS, BUILTIN_FUNCTION_MAP, @@ -1154,7 +1154,7 @@ export class LatexToSympy extends LatexParserVisitor { @@ -1162,7 +1162,7 @@ export class LatexToSympy extends LatexParserVisitor { @@ -1354,7 +1354,7 @@ export class LatexToSympy extends LatexParserVisitor> BigInt(1)) + B; - return result.toString(); -} From f6ca302bacd341bc2d377a9d2ad649fa1b767b25 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Mon, 6 Jan 2025 22:39:47 -0600 Subject: [PATCH 12/53] tests: increase timeout for symbolic expression error test Firefox is consistently timing out on this test --- tests/test_symbolic_expression_error_handling.spec.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_symbolic_expression_error_handling.spec.mjs b/tests/test_symbolic_expression_error_handling.spec.mjs index c5e94cc6..b04f80ef 100644 --- a/tests/test_symbolic_expression_error_handling.spec.mjs +++ b/tests/test_symbolic_expression_error_handling.spec.mjs @@ -11,6 +11,6 @@ test('Test handling of symbolic expression error', async ({ page, browserName }) await page.locator('text=Updating...').waitFor({state: 'detached', timeout: pyodideLoadTimeout}); - let content = await page.locator('#result-value-21').textContent(); + let content = await page.locator('#result-value-21').textContent({timeout: 240000}); expect(parseLatexFloat(content)).toBeCloseTo(57168.5056551697, precision); }); From 2f053c2029db6b100d5860b92ec7a32c8391d645 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Tue, 7 Jan 2025 13:03:15 -0600 Subject: [PATCH 13/53] refactor: remove unused unitless sub query code from python code Still needs to be removed from JS code --- public/dimensional_analysis.py | 203 ++++----------------------------- 1 file changed, 24 insertions(+), 179 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 140f23d9..ceb5f229 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -116,34 +116,13 @@ class ImplicitParameter(TypedDict): original_value: str si_value: str - -# generated on the fly in evaluate_statements function, does in exist in incoming json -class UnitlessSubExpressionName(TypedDict): - name: str - unitlessContext: str - -class UnitlessSubExpression(TypedDict): - type: Literal["assignment"] - name: str - sympy: str - params: list[str] - isUnitlessSubExpression: Literal[True] - unitlessContext: str - isFunctionArgument: Literal[False] - isFunction: Literal[False] - unitlessSubExpressions: list['UnitlessSubExpression | UnitlessSubExpressionName'] - index: int # added in Python, not pressent in json - expression: Expr # added in Python, not pressent in json - class BaseUserFunction(TypedDict): type: Literal["assignment"] name: str sympy: str params: list[str] - isUnitlessSubExpression: Literal[False] isFunctionArgument: Literal[False] isFunction: Literal[True] - unitlessSubExpressions: list[UnitlessSubExpression | UnitlessSubExpressionName] functionParameters: list[str] index: int # added in Python, not pressent in json expression: Expr # added in Python, not pressent in json @@ -163,10 +142,8 @@ class UserFunctionRange(BaseUserFunction): class FunctionUnitsQuery(TypedDict): type: Literal["query"] sympy: str - unitlessSubExpressions: list[UnitlessSubExpression | UnitlessSubExpressionName] params: list[str] units: Literal[""] - isUnitlessSubExpression: Literal[False] isFunctionArgument: Literal[False] isFunction: Literal[False] isUnitsQuery: Literal[True] @@ -197,9 +174,7 @@ class FunctionArgumentAssignment(TypedDict): type: Literal["assignment"] name: str sympy: str - unitlessSubExpressions: list[UnitlessSubExpression | UnitlessSubExpressionName] params: list[str] - isUnitlessSubExpression: Literal[False] isFunctionArgument: Literal[True] isFunction: Literal[False] index: int # added in Python, not pressent in json @@ -208,10 +183,8 @@ class FunctionArgumentAssignment(TypedDict): class FunctionArgumentQuery(TypedDict): type: Literal["query"] sympy: str - unitlessSubExpressions: list[UnitlessSubExpression | UnitlessSubExpressionName] params: list[str] name: str - isUnitlessSubExpression: Literal[False] isFunctionArgument: Literal[True] isFunction: Literal[False] isUnitsQuery: Literal[False] @@ -226,7 +199,6 @@ class BlankStatement(TypedDict): type: Literal["blank"] params: list[str] # will be empty list implicitParams: list[ImplicitParameter] # will be empty list - unitlessSubExpressions: list[UnitlessSubExpression | UnitlessSubExpressionName] # will be empty list isFromPlotCell: Literal[False] index: int # added in Python, not pressent in json @@ -236,7 +208,6 @@ class QueryAssignmentCommon(TypedDict): functions: list[UserFunction | UserFunctionRange | FunctionUnitsQuery] arguments: list[FunctionArgumentQuery | FunctionArgumentAssignment] localSubs: list[LocalSubstitution | LocalSubstitutionRange] - unitlessSubExpressions: list[UnitlessSubExpression | UnitlessSubExpressionName] params: list[str] index: int # added in Python, not pressent in json expression: Expr # added in Python, not pressent in json @@ -244,7 +215,6 @@ class QueryAssignmentCommon(TypedDict): class AssignmentStatement(QueryAssignmentCommon): type: Literal["assignment"] name: str - isUnitlessSubExpression: Literal[False] isFunctionArgument: Literal[False] isFunction: Literal[False] isFromPlotCell: Literal[False] @@ -259,7 +229,6 @@ class SystemSolutionAssignmentStatement(AssignmentStatement): class BaseQueryStatement(QueryAssignmentCommon): type: Literal["query"] - isUnitlessSubExpression: Literal[False] isFunctionArgument: Literal[False] isFunction: Literal[False] isUnitsQuery: Literal[False] @@ -317,7 +286,6 @@ class ScatterXValuesQueryStatement(QueryAssignmentCommon): isDataTableQuery: Literal[False] isCodeFunctionQuery: Literal[False] isCodeFunctionRawQuery: Literal[False] - isUnitlessSubExpression: Literal[False] isFunctionArgument: Literal[False] isFunction: Literal[False] isUnitsQuery: Literal[False] @@ -337,7 +305,6 @@ class ScatterYValuesQueryStatement(QueryAssignmentCommon): isDataTableQuery: Literal[False] isCodeFunctionQuery: Literal[False] isCodeFunctionRawQuery: Literal[False] - isUnitlessSubExpression: Literal[False] isFunctionArgument: Literal[False] isFunction: Literal[False] isUnitsQuery: Literal[False] @@ -362,7 +329,6 @@ class ScatterQueryStatement(TypedDict): arguments: list[FunctionArgumentQuery | FunctionArgumentAssignment] localSubs: list[LocalSubstitution | LocalSubstitutionRange] implicitParams: list[ImplicitParameter] - unitlessSubExpressions: list[UnitlessSubExpression | UnitlessSubExpressionName] xValuesQuery: ScatterXValuesQueryStatement yValuesQuery: ScatterYValuesQueryStatement xName: str @@ -397,7 +363,6 @@ class EqualityUnitsQueryStatement(QueryAssignmentCommon): isDataTableQuery: Literal[False] isCodeFunctionQuery: Literal[False] isCodeFunctionRawQuery: Literal[False] - isUnitlessSubExpression: Literal[False] isFunctionArgument: Literal[False] isFunction: Literal[False] isUnitsQuery: Literal[False] @@ -408,7 +373,6 @@ class EqualityUnitsQueryStatement(QueryAssignmentCommon): class EqualityStatement(QueryAssignmentCommon): type: Literal["equality"] - isUnitlessSubExpression: Literal[False] isFunctionArgument: Literal[False] isFunction: Literal[False] isFromPlotCell: Literal[False] @@ -488,14 +452,13 @@ class LocalSubstitutionStatement(TypedDict): name: str params: list[str] function_subs: dict[str, dict[str, str]] - isUnitlessSubExpression: Literal[False] index: int InputStatement = AssignmentStatement | QueryStatement | RangeQueryStatement | BlankStatement | \ CodeFunctionQueryStatement | ScatterQueryStatement | SubQueryStatement InputAndSystemStatement = InputStatement | EqualityUnitsQueryStatement | GuessAssignmentStatement | \ SystemSolutionAssignmentStatement -Statement = InputStatement | UnitlessSubExpression | UserFunction | UserFunctionRange | FunctionUnitsQuery | \ +Statement = InputStatement | UserFunction | UserFunctionRange | FunctionUnitsQuery | \ FunctionArgumentQuery | FunctionArgumentAssignment | \ SystemSolutionAssignmentStatement | LocalSubstitutionStatement | \ GuessAssignmentStatement | EqualityUnitsQueryStatement | CodeFunctionRawQuery | \ @@ -627,7 +590,6 @@ class CombinedExpressionBlank(TypedDict): isBlank: Literal[True] isRange: Literal[False] isScatter: Literal[False] - unitlessSubExpressions: list[UnitlessSubExpression | UnitlessSubExpressionName] isSubQuery: Literal[False] subQueryName: Literal[""] @@ -635,7 +597,6 @@ class CombinedExpressionNoRange(TypedDict): index: int name: str expression: Expr - unitlessSubExpressions: list[UnitlessSubExpression | UnitlessSubExpressionName] isBlank: Literal[False] isRange: Literal[False] isScatter: Literal[False] @@ -654,7 +615,6 @@ class CombinedExpressionRange(TypedDict): index: int name: str expression: Expr - unitlessSubExpressions: list[UnitlessSubExpression | UnitlessSubExpressionName] isBlank: Literal[False] isRange: Literal[True] isParametric: bool @@ -1769,16 +1729,7 @@ def expand_with_sub_statements(statements: list[InputAndSystemStatement]): local_sub_statements: dict[str, LocalSubstitutionStatement] = {} - included_unitless_sub_expressions: set[str] = set() - for statement in statements: - # need to prevent inclusion of already included exponents since solving a system of equations - # will repeat exponents for each variable that is solved for - for unitless_sub_expression in cast(list[UnitlessSubExpression], statement["unitlessSubExpressions"]): - if unitless_sub_expression["name"] not in included_unitless_sub_expressions: - new_statements.append(unitless_sub_expression) - included_unitless_sub_expressions.update([unitless_sub_expression["name"] for unitless_sub_expression in statement["unitlessSubExpressions"]]) - new_statements.extend(statement.get("functions", [])) new_statements.extend(statement.get("arguments", [])) for local_sub in statement.get("localSubs", []): @@ -1788,7 +1739,6 @@ def expand_with_sub_statements(statements: list[InputAndSystemStatement]): "index": 0, # placeholder, will be set in sympy_statements "params": [], "function_subs": {}, - "isUnitlessSubExpression": False }) combined_sub["params"].append(local_sub["argument"]) function_subs = combined_sub["function_subs"] @@ -1820,25 +1770,22 @@ def get_parameter_subs(parameters: list[ImplicitParameter], convert_floats_to_fr return parameter_subs -def sympify_statements(statements: list[Statement] | list[EqualityStatement], - sympify_unitless_sub_expressions=False, convert_floats_to_fractions=True): +def sympify_statements(statements: list[Statement] | list[EqualityStatement], 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=convert_floats_to_fractions) - if sympify_unitless_sub_expressions: - for unitless_sub_expression in cast(list[UnitlessSubExpression], statement["unitlessSubExpressions"]): - unitless_sub_expression["expression"] = sympify(unitless_sub_expression["sympy"], rational=convert_floats_to_fractions) + except SyntaxError: print(f"Parsing error for equation {statement['sympy']}") raise ParsingError -def remove_implicit_and_unitless_sub_expression(input_set: set[str]) -> set[str]: +def remove_implicit(input_set: set[str]) -> set[str]: return {variable for variable in input_set - if not variable.startswith( ("implicit_param__", "unitless__") )} + if not variable.startswith("implicit_param__")} def solve_system(statements: list[EqualityStatement], variables: list[str], @@ -1847,8 +1794,7 @@ def solve_system(statements: list[EqualityStatement], variables: list[str], parameters = get_all_implicit_parameters(statements) parameter_subs = get_parameter_subs(parameters, convert_floats_to_fractions) - sympify_statements(statements, sympify_unitless_sub_expressions=True, - convert_floats_to_fractions=convert_floats_to_fractions) + sympify_statements(statements, 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): @@ -1856,18 +1802,14 @@ def solve_system(statements: list[EqualityStatement], variables: list[str], # define system of equations for sympy.solve function # substitute in all exponents and placeholder functions - system_unitless_sub_expressions: list[UnitlessSubExpression | UnitlessSubExpressionName] = [] system_implicit_params: list[ImplicitParameter] = [] system_variables: set[str] = set() system: list[Expr] = [] for statement in statements: system_variables.update(statement["params"]) - system_unitless_sub_expressions.extend(statement["unitlessSubExpressions"]) system_implicit_params.extend(statement["implicitParams"]) - equality = cast(Expr, statement["expression"]).subs( - {unitless_sub_expression["name"]:unitless_sub_expression["expression"] for unitless_sub_expression in cast(list[UnitlessSubExpression], statement["unitlessSubExpressions"])}) - equality = replace_placeholder_funcs(cast(Expr, equality), + equality = replace_placeholder_funcs(cast(Expr, statement["expression"]), "sympy_func", placeholder_map, placeholder_set, {}, [], None) @@ -1875,7 +1817,7 @@ def solve_system(statements: list[EqualityStatement], variables: list[str], # remove implicit parameters before solving - system_variables = remove_implicit_and_unitless_sub_expression(system_variables) + system_variables = remove_implicit(system_variables) solutions: list[dict[Symbol, Expr]] = [] solutions = solve(system, variables, dict=True) @@ -1906,8 +1848,6 @@ def solve_system(statements: list[EqualityStatement], variables: list[str], "expression": expression, "implicitParams": system_implicit_params if counter == 0 else [], # only include for one variable in solution to prevent dups "params": [variable.name for variable in cast(list[Symbol], expression.free_symbols)], - "unitlessSubExpressions": system_unitless_sub_expressions, - "isUnitlessSubExpression": False, "isFunction": False, "isFunctionArgument": False, "isRange": False, @@ -1937,8 +1877,7 @@ def solve_system_numerical(statements: list[EqualityStatement], variables: list[ parameters = get_all_implicit_parameters([*statements, *guess_statements]) parameter_subs = get_parameter_subs(parameters, convert_floats_to_fractions) - sympify_statements(statements, sympify_unitless_sub_expressions=True, - convert_floats_to_fractions=convert_floats_to_fractions) + sympify_statements(statements, 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): @@ -1947,17 +1886,13 @@ def solve_system_numerical(statements: list[EqualityStatement], variables: list[ # define system of equations for sympy.solve function # substitute in all exponents, implicit params, and placeholder functions # add equalityUnitsQueries to new_statements that will be added to the whole sheet - system_unitless_sub_expressions: list[UnitlessSubExpression | UnitlessSubExpressionName] = [] system_variables: set[str] = set() system: list[Expr] = [] new_statements: list[EqualityUnitsQueryStatement | GuessAssignmentStatement] = [] for statement in statements: system_variables.update(statement["params"]) - system_unitless_sub_expressions.extend(statement["unitlessSubExpressions"]) - equality = cast(Expr, statement["expression"]).subs( - {unitless_sub_expression["name"]: unitless_sub_expression["expression"] for unitless_sub_expression in cast(list[UnitlessSubExpression], statement["unitlessSubExpressions"])}) - equality = equality.subs(parameter_subs) + equality = cast(Expr, statement["expression"]).subs(parameter_subs) equality = replace_placeholder_funcs(cast(Expr, equality), "sympy_func", placeholder_map, placeholder_set, {}, [], None) @@ -1965,7 +1900,7 @@ def solve_system_numerical(statements: list[EqualityStatement], variables: list[ new_statements.extend(statement["equalityUnitsQueries"]) # remove implicit parameters before solving - system_variables = remove_implicit_and_unitless_sub_expression(system_variables) + system_variables = remove_implicit(system_variables) solutions: list[dict[Symbol, float]] | list[Any] = [] try: @@ -2426,9 +2361,7 @@ def get_evaluated_expression(expression: Expr, def get_result(evaluated_expression: ExprWithAssumptions, dimensional_analysis_expression: Expr | None, dim_sub_error: Exception | None, symbolic_expression: str, - unitless_sub_expressions: list[UnitlessSubExpression | UnitlessSubExpressionName], - isRange: bool, unitless_sub_expression_dimensionless: dict[str, bool], - custom_base_units: CustomBaseUnits | None, + isRange: bool, custom_base_units: CustomBaseUnits | None, isSubQuery: bool, subQueryName: str ) -> Result | FiniteImagResult: @@ -2436,12 +2369,7 @@ def get_result(evaluated_expression: ExprWithAssumptions, dimensional_analysis_e custom_units = "" custom_units_latex = "" - if not all([unitless_sub_expression_dimensionless[local_item["name"]] for local_item in unitless_sub_expressions]): - context_set = {local_item["unitlessContext"] for local_item in unitless_sub_expressions if not unitless_sub_expression_dimensionless[local_item["name"]]} - context_combined = ", ".join(context_set) - dim = f"Dimension Error: {context_combined} Not Dimensionless" - dim_latex = f"Dimension Error: {context_combined} Not Dimensionless" - elif isRange: + if isRange: # a separate unitsQuery function is used for plots, no need to perform dimensional analysis before subs are made dim = "" dim_latex = "" @@ -2527,21 +2455,16 @@ def evaluate_statements(statements: list[InputAndSystemStatement], expanded_statements = get_sorted_statements(expanded_statements, custom_definition_names) combined_expressions: list[CombinedExpression] = [] - unitless_sub_expression_subs: dict[str, Expr | float] = {} - unit_sub_expression_dimensionless: dict[str, bool] = {} - function_unitless_sub_expression_replacements: dict[str, dict[Symbol, Symbol]] = {} - function_unitless_sub_expression_context: dict[str, str] = {} + for i, statement in enumerate(expanded_statements): if statement["type"] == "local_sub" or statement["type"] == "blank": continue - if statement["type"] == "assignment" and not statement["isUnitlessSubExpression"] and \ - not statement.get("isFunction", False): + if statement["type"] == "assignment" and not statement.get("isFunction", False): combined_expressions.append({"index": statement["index"], "isBlank": True, "isRange": False, "isScatter": False, - "unitlessSubExpressions": [], "isSubQuery": False, "subQueryName": ""}) continue @@ -2566,110 +2489,34 @@ def evaluate_statements(statements: list[InputAndSystemStatement], # sub equations into each other in topological order if there are more than one function_name = "" - unitless_sub_expression_name = "" - unitless_sub_expression_context = "" + if statement["isFunction"] is True: is_function = True function_name = statement["name"] - is_unitless_sub_expression = False - elif statement["isUnitlessSubExpression"] is True: - is_unitless_sub_expression = True - unitless_sub_expression_name = statement["name"] - unitless_sub_expression_context = statement["unitlessContext"] - is_function = False else: - is_unitless_sub_expression = False is_function = False - dependency_unitless_sub_expressions = statement["unitlessSubExpressions"] - new_function_unitless_sub_expressions: dict[str, Expr] = {} + final_expression = statement["expression"] for sub_statement in reversed(temp_statements[0:-1]): - if (sub_statement["type"] == "assignment" or ((is_function or is_unitless_sub_expression) and sub_statement["type"] == "local_sub")) \ - and not sub_statement["isUnitlessSubExpression"]: + if (sub_statement["type"] == "assignment" or (is_function and sub_statement["type"] == "local_sub")): if sub_statement["type"] == "local_sub": if is_function: current_local_subs = sub_statement["function_subs"].get(function_name, {}) if len(current_local_subs) > 0: final_expression = subs_wrapper(final_expression, current_local_subs) - elif is_unitless_sub_expression: - for local_sub_function_name, function_local_subs in sub_statement["function_subs"].items(): - function_unitless_sub_expression = new_function_unitless_sub_expressions.setdefault(local_sub_function_name, final_expression) - new_function_unitless_sub_expressions[local_sub_function_name] = subs_wrapper(function_unitless_sub_expression, function_local_subs) else: if sub_statement["name"] in map(lambda x: str(x), final_expression.free_symbols): - dependency_unitless_sub_expressions.extend(sub_statement["unitlessSubExpressions"]) final_expression = subs_wrapper(final_expression, {symbols(sub_statement["name"]): sub_statement["expression"]}) - - if is_unitless_sub_expression: - new_function_unitless_sub_expressions = { - key:subs_wrapper(expression, {symbols(sub_statement["name"]): sub_statement["expression"]}) for - key, expression in new_function_unitless_sub_expressions.items() - } - - if is_unitless_sub_expression: - for current_function_name in new_function_unitless_sub_expressions.keys(): - function_unitless_sub_expression_replacements.setdefault(current_function_name, {}).update( - {symbols(unitless_sub_expression_name): symbols(unitless_sub_expression_name+current_function_name)} - ) - function_unitless_sub_expression_context[unitless_sub_expression_name] = unitless_sub_expression_context - - new_function_unitless_sub_expressions[''] = final_expression - - for current_function_name, final_expression in new_function_unitless_sub_expressions.items(): - while(True): - available_unitless_subs = set(function_unitless_sub_expression_replacements.get(current_function_name, {}).keys()) & \ - final_expression.free_symbols - if len(available_unitless_subs) == 0: - break - final_expression = subs_wrapper(final_expression, function_unitless_sub_expression_replacements[current_function_name]) - final_expression = subs_wrapper(final_expression, unitless_sub_expression_subs) - - final_expression = subs_wrapper(final_expression, unitless_sub_expression_subs) - final_expression = cast(Expr, final_expression.doit()) - dimensional_analysis_expression, dim_sub_error = get_dimensional_analysis_expression(dimensional_analysis_subs, - final_expression, - placeholder_map, - placeholder_set, {}) - dim, _, _, _, _ = dimensional_analysis(dimensional_analysis_expression, dim_sub_error) - if dim == "": - unit_sub_expression_dimensionless[unitless_sub_expression_name+current_function_name] = True - else: - unit_sub_expression_dimensionless[unitless_sub_expression_name+current_function_name] = False - - final_expression = cast(Expr, cast(Expr, final_expression).xreplace(parameter_subs)) - final_expression = replace_placeholder_funcs(final_expression, - "sympy_func", - placeholder_map, - placeholder_set, {}, [], - None) - - unitless_sub_expression_subs[symbols(unitless_sub_expression_name+current_function_name)] = final_expression - - elif is_function: - while(True): - available_unitless_subs = set(function_unitless_sub_expression_replacements.get(function_name, {}).keys()) & \ - final_expression.free_symbols - if len(available_unitless_subs) == 0: - break - final_expression = subs_wrapper(final_expression, function_unitless_sub_expression_replacements[function_name]) - statement["unitlessSubExpressions"].extend([{"name": str(function_unitless_sub_expression_replacements[function_name][key]), - "unitlessContext": function_unitless_sub_expression_context[str(key)]} for key in available_unitless_subs]) - final_expression = subs_wrapper(final_expression, unitless_sub_expression_subs) - if function_name in function_unitless_sub_expression_replacements: - for unitless_sub_expression_i, unitless_sub_expression in enumerate(statement["unitlessSubExpressions"]): - if symbols(unitless_sub_expression["name"]) in function_unitless_sub_expression_replacements[function_name]: - statement["unitlessSubExpressions"][unitless_sub_expression_i] = UnitlessSubExpressionName(name = str(function_unitless_sub_expression_replacements[function_name][symbols(unitless_sub_expression["name"])]), - unitlessContext = unitless_sub_expression["unitlessContext"]) + if is_function: statement["expression"] = final_expression elif statement["type"] == "query": if statement["isRange"] is not True: current_combined_expression: CombinedExpression = {"index": statement["index"], - "expression": subs_wrapper(final_expression, unitless_sub_expression_subs), - "unitlessSubExpressions": dependency_unitless_sub_expressions, + "expression": final_expression, "isBlank": False, "isRange": False, "isScatter": False, @@ -2687,8 +2534,7 @@ def evaluate_statements(statements: list[InputAndSystemStatement], } else: current_combined_expression: CombinedExpression = {"index": statement["index"], - "expression": subs_wrapper(final_expression, unitless_sub_expression_subs), - "unitlessSubExpressions": dependency_unitless_sub_expressions, + "expression": final_expression, "isBlank": False, "isRange": True, "isParametric": statement.get("isParametric", False), @@ -2766,8 +2612,7 @@ def evaluate_statements(statements: list[InputAndSystemStatement], if not is_matrix(evaluated_expression): results[index] = get_result(evaluated_expression, dimensional_analysis_expression, dim_sub_error, cast(str, symbolic_expression), - item["unitlessSubExpressions"], item["isRange"], - unit_sub_expression_dimensionless, + item["isRange"], custom_base_units, item["isSubQuery"], item["subQueryName"]) @@ -2792,8 +2637,8 @@ def evaluate_statements(statements: list[InputAndSystemStatement], current_result = get_result(cast(ExprWithAssumptions, evaluated_expression[i,j]), cast(Expr, current_dimensional_analysis_expression), - dim_sub_error, symbolic_expression[i][j], item["unitlessSubExpressions"], - item["isRange"], unit_sub_expression_dimensionless, + dim_sub_error, symbolic_expression[i][j], + item["isRange"], custom_base_units, item["isSubQuery"], item["subQueryName"]) From 9d69544d486c40a179fcd70011274cc0d833d4a7 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Tue, 7 Jan 2025 13:42:26 -0600 Subject: [PATCH 14/53] refactor: remove unused unitless subquery code from javascript code --- src/parser/LatexToSympy.ts | 51 ++------------------------------------ src/parser/types.ts | 22 +++------------- src/parser/utility.ts | 2 -- 3 files changed, 5 insertions(+), 70 deletions(-) diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index 29bd185b..f2921136 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -4,7 +4,7 @@ import LatexParserVisitor from "./LatexParserVisitor"; import type { FieldTypes, Statement, QueryStatement, RangeQueryStatement, UserFunctionRange, AssignmentStatement, ImplicitParameter, UserFunction, FunctionArgumentQuery, FunctionArgumentAssignment, LocalSubstitution, LocalSubstitutionRange, - UnitlessSubExpression, GuessAssignmentStatement, FunctionUnitsQuery, + GuessAssignmentStatement, FunctionUnitsQuery, SolveParametersWithGuesses, ErrorStatement, EqualityStatement, EqualityUnitsQueryStatement, SolveParameters, AssignmentList, InsertMatrix, @@ -63,7 +63,7 @@ type ParsingResult = { } export function getBlankStatement(): BlankStatement { - return { type: "blank", params: [], implicitParams: [], unitlessSubExpressions: [], isFromPlotCell: false}; + return { type: "blank", params: [], implicitParams: [], isFromPlotCell: false}; } export function parseLatex(latex: string, id: number, type: FieldTypes, @@ -181,14 +181,11 @@ export class LatexToSympy extends LatexParserVisitor(); - unitlessSubExpressions: UnitlessSubExpression[] = []; subQueries: SubQueryStatement[] = []; subQueryReplacements: [string, Replacement][] = []; inQueryStatement = false; @@ -270,11 +267,6 @@ export class LatexToSympy extends LatexParserVisitor & { @@ -110,17 +107,9 @@ export type UserFunctionRange = Omit & { }; -export type UnitlessSubExpression = Omit & { - isUnitlessSubExpression: true; - unitlessContext: string; - isFunctionArgument: false; - isFunction: false; -}; - export type FunctionArgumentAssignment = Pick & { - isUnitlessSubExpression: false; + "params"> & { isFunctionArgument: true; isFunction: false; }; @@ -168,13 +157,11 @@ export type EqualityStatement = Omit & { type BaseQueryStatement = { type: "query"; sympy: string; - unitlessSubExpressions: UnitlessSubExpression[]; implicitParams: ImplicitParameter[]; params: string[]; functions: (UserFunction | UserFunctionRange | FunctionUnitsQuery)[]; arguments: (FunctionArgumentAssignment | FunctionArgumentQuery) []; localSubs: (LocalSubstitution | LocalSubstitutionRange)[]; - isUnitlessSubExpression: false; isFunctionArgument: false; isFunction: false; isUnitsQuery: false; @@ -264,7 +251,6 @@ export type ScatterQueryStatement = { arguments: (FunctionArgumentAssignment | FunctionArgumentQuery) []; localSubs: (LocalSubstitution | LocalSubstitutionRange)[]; implicitParams: ImplicitParameter[]; - unitlessSubExpressions: UnitlessSubExpression[]; equationIndex: number; cellNum: number; isFromPlotCell: boolean; @@ -298,9 +284,8 @@ export type CodeFunctionRawQuery = BaseQueryStatement & { isCodeFunctionRawQuery: true; } -export type FunctionArgumentQuery = Pick & { +export type FunctionArgumentQuery = Pick & { name: string; - isUnitlessSubExpression: false; isFunctionArgument: true; isFunction: false; isUnitsQuery: false; @@ -310,9 +295,8 @@ export type FunctionArgumentQuery = Pick & { +export type FunctionUnitsQuery = Pick & { units: ''; - isUnitlessSubExpression: false; isFunctionArgument: false; isFunction: false; isUnitsQuery: true; diff --git a/src/parser/utility.ts b/src/parser/utility.ts index 2eb37dd6..29e99ce8 100644 --- a/src/parser/utility.ts +++ b/src/parser/utility.ts @@ -89,7 +89,6 @@ export function applyEdits(source: string, pendingEdits: (Insertion | Replacemen export function createSubQuery(name: string): SubQueryStatement { return { type: "query", - unitlessSubExpressions: [], implicitParams: [], params: [name], functions: [], @@ -97,7 +96,6 @@ export function createSubQuery(name: string): SubQueryStatement { localSubs: [], units: "", unitsLatex: "", - isUnitlessSubExpression: false, isFunctionArgument: false, isFunction: false, isUnitsQuery: false, From 0e447db569a4a5894f48214c8f41fc9f790bb154 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Tue, 7 Jan 2025 15:17:53 -0600 Subject: [PATCH 15/53] fix: apply evalf to exponents rational exponents with large denominators causes very long calculation times. --- public/dimensional_analysis.py | 7 +++++-- tests/test_symbolic_expression_error_handling.spec.mjs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index ceb5f229..e814527a 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1151,10 +1151,13 @@ def custom_integral_dims(local_expr: Expr, global_expr: Expr, dummy_integral_var def custom_add_dims(*args: Expr): return Add(*[Abs(arg) for arg in args]) +def custom_pow(base: Expr, exponent: Expr): + return base**(exponent.evalf(PRECISION)) + def custom_pow_dims(dim_values: list[Expr], base: Expr, exponent: Expr): if custom_get_dimensional_dependencies(exponent) != {}: raise TypeError('Exponent Not Dimensionless') - return base**dim_values[1] + return base**((dim_values[1]).evalf(PRECISION)) CP = None @@ -1468,7 +1471,7 @@ def get_next_id(self): cast(Function, Function('_range')) : {"dim_func": custom_range, "sympy_func": custom_range}, cast(Function, Function('_factorial')) : {"dim_func": factorial, "sympy_func": CustomFactorial}, cast(Function, Function('_add')) : {"dim_func": custom_add_dims, "sympy_func": Add}, - cast(Function, Function('_Pow')) : {"dim_func": custom_pow_dims, "sympy_func": Pow}, + cast(Function, Function('_Pow')) : {"dim_func": custom_pow_dims, "sympy_func": custom_pow}, } global_placeholder_set = set(global_placeholder_map.keys()) diff --git a/tests/test_symbolic_expression_error_handling.spec.mjs b/tests/test_symbolic_expression_error_handling.spec.mjs index b04f80ef..c5e94cc6 100644 --- a/tests/test_symbolic_expression_error_handling.spec.mjs +++ b/tests/test_symbolic_expression_error_handling.spec.mjs @@ -11,6 +11,6 @@ test('Test handling of symbolic expression error', async ({ page, browserName }) await page.locator('text=Updating...').waitFor({state: 'detached', timeout: pyodideLoadTimeout}); - let content = await page.locator('#result-value-21').textContent({timeout: 240000}); + let content = await page.locator('#result-value-21').textContent(); expect(parseLatexFloat(content)).toBeCloseTo(57168.5056551697, precision); }); From 771036ff7aba202cd0e9cc1b01cd4e6ff88725e8 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Tue, 7 Jan 2025 17:33:46 -0600 Subject: [PATCH 16/53] fix: only convert exponent to float for numeric base and exp Prevents losing exactness for symbolic results while speeding up numerical results --- public/dimensional_analysis.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index e814527a..9e408269 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1152,7 +1152,10 @@ def custom_add_dims(*args: Expr): return Add(*[Abs(arg) for arg in args]) def custom_pow(base: Expr, exponent: Expr): - return base**(exponent.evalf(PRECISION)) + if base.is_number and exponent.is_number: + return base**(exponent.evalf(PRECISION)) + else: + return base**exponent def custom_pow_dims(dim_values: list[Expr], base: Expr, exponent: Expr): if custom_get_dimensional_dependencies(exponent) != {}: From 0255ce2ddaf2835cd2e2fec6ddf4c2da1e87b1da Mon Sep 17 00:00:00 2001 From: mgreminger Date: Tue, 7 Jan 2025 22:43:47 -0600 Subject: [PATCH 17/53] fix: only switch to floating point exponents for very large rationals Add test for symbolic representation of small rationals --- public/dimensional_analysis.py | 20 +++++++++++++++----- tests/test_number_format.spec.mjs | 9 +++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 9e408269..f2732354 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -64,7 +64,9 @@ sign, sqrt, factorial, - Basic + Basic, + Rational, + Integer ) class ExprWithAssumptions(Expr): @@ -727,6 +729,9 @@ def get_base_units(custom_base_units: CustomBaseUnits | None= None) -> dict[tupl # precision for sympy evalf calls to convert expressions to floating point values PRECISION = 64 +# very large rationals are inefficient for exponential calculations +LARGE_RATIONAL = 1000000 + # num of digits to round to for unit exponents # this makes sure units with a very small difference are identified as the same EXP_NUM_DIGITS = 12 @@ -1152,15 +1157,20 @@ def custom_add_dims(*args: Expr): return Add(*[Abs(arg) for arg in args]) def custom_pow(base: Expr, exponent: Expr): - if base.is_number and exponent.is_number: - return base**(exponent.evalf(PRECISION)) + large_rational = False + for atom in (exponent.atoms(Rational) | base.atoms(Rational)): + if abs(atom.q) > LARGE_RATIONAL: + large_rational = True + + if large_rational: + return Pow(base.evalf(PRECISION), exponent.evalf(PRECISION)) else: - return base**exponent + return Pow(base, exponent) def custom_pow_dims(dim_values: list[Expr], base: Expr, exponent: Expr): if custom_get_dimensional_dependencies(exponent) != {}: raise TypeError('Exponent Not Dimensionless') - return base**((dim_values[1]).evalf(PRECISION)) + return Pow(base.evalf(PRECISION), (dim_values[1]).evalf(PRECISION)) CP = None diff --git a/tests/test_number_format.spec.mjs b/tests/test_number_format.spec.mjs index 6054c883..ebb35318 100644 --- a/tests/test_number_format.spec.mjs +++ b/tests/test_number_format.spec.mjs @@ -53,6 +53,10 @@ test('Test symbolic format', async () => { await page.locator('#add-math-cell').click(); await page.setLatex(2, String.raw`\frac{-3\left\lbrack mm\right\rbrack}{\sqrt2}=`); + // symbolic expression with fractional exponent + await page.locator('#add-math-cell').click(); + await page.setLatex(3, String.raw`3.0^{.500}=`); + await page.waitForSelector('text=Updating...', {state: 'detached'}); // check all values rendered as floating point values first @@ -69,6 +73,9 @@ test('Test symbolic format', async () => { content = await page.textContent('#result-units-2'); expect(content).toBe('m'); + content = await page.textContent('#result-value-3'); + expect(parseLatexFloat(content)).toBeCloseTo(sqrt(3), precision); + // switch to symbolic formatting await page.getByRole('button', { name: 'Sheet Settings' }).click(); await page.locator('label').filter({ hasText: 'Display Symbolic Results' }).click(); @@ -87,6 +94,8 @@ test('Test symbolic format', async () => { content = await page.textContent('#result-units-2'); expect(content).toBe('m'); + content = await page.textContent('#result-value-3'); + expect(content).toBe(String.raw`\sqrt{3}`); }); test('Test disabling automatic expressions simplification', async () => { From 14ecb9c63d9ac12a3ed786fa8b5e0b17ee4585cd Mon Sep 17 00:00:00 2001 From: mgreminger Date: Tue, 7 Jan 2025 23:58:03 -0600 Subject: [PATCH 18/53] feat: add result to DimValues dict This allows functions like range that need to know the result of the calculation to set the correct dims. Range now works with inputs that have consistent units. New test added for this functionality. --- public/dimensional_analysis.py | 44 +++++++++++++++++----------- src/parser/LatexToSympy.ts | 9 ++++-- src/parser/constants.ts | 2 ++ tests/test_matrix_functions.spec.mjs | 13 ++++++-- 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index f2732354..cfbd0b39 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -653,6 +653,10 @@ class CombinedExpressionScatter(TypedDict): CombinedExpression = CombinedExpressionBlank | CombinedExpressionNoRange | CombinedExpressionRange | \ CombinedExpressionScatter +class DimValues(TypedDict): + args: list[Expr] + result: Expr + # maps from mathjs dimensions object to sympy dimensions dim_map: dict[int, Dimension] = { 0: mass, @@ -1073,6 +1077,9 @@ def custom_range(*args: Expr): return Matrix(values) +def custom_range_dims(dim_values: DimValues, *args: Expr): + return Matrix([ensure_dims_all_compatible(*args)]*len(cast(Matrix, dim_values["result"]))) + class PlaceholderFunction(TypedDict): dim_func: Callable | Function sympy_func: object @@ -1087,13 +1094,13 @@ def IndexMatrix(expression: Expr, i: Expr, j: Expr) -> Expr: return expression[i-1, j-1] # type: ignore -def IndexMatrix_dims(dim_values: list[Expr], expression: Expr, i: Expr, j: Expr) -> Expr: +def IndexMatrix_dims(dim_values: DimValues, expression: Expr, i: Expr, j: Expr) -> Expr: if custom_get_dimensional_dependencies(i) != {} or \ custom_get_dimensional_dependencies(j) != {}: raise TypeError('Matrix Index Not Dimensionless') - i_value = dim_values[1] - j_value = dim_values[2] + i_value = dim_values["args"][1] + j_value = dim_values["args"][2] return expression[i_value-1, j_value-1] # type: ignore @@ -1167,10 +1174,10 @@ def custom_pow(base: Expr, exponent: Expr): else: return Pow(base, exponent) -def custom_pow_dims(dim_values: list[Expr], base: Expr, exponent: Expr): +def custom_pow_dims(dim_values: DimValues, base: Expr, exponent: Expr): if custom_get_dimensional_dependencies(exponent) != {}: raise TypeError('Exponent Not Dimensionless') - return Pow(base.evalf(PRECISION), (dim_values[1]).evalf(PRECISION)) + return Pow(base.evalf(PRECISION), (dim_values["args"][1]).evalf(PRECISION)) CP = None @@ -1481,7 +1488,7 @@ def get_next_id(self): cast(Function, Function('_round')) : {"dim_func": ensure_unitless_in, "sympy_func": custom_round}, cast(Function, Function('_Derivative')) : {"dim_func": custom_derivative_dims, "sympy_func": custom_derivative}, cast(Function, Function('_Integral')) : {"dim_func": custom_integral_dims, "sympy_func": custom_integral}, - cast(Function, Function('_range')) : {"dim_func": custom_range, "sympy_func": custom_range}, + cast(Function, Function('_range')) : {"dim_func": custom_range_dims, "sympy_func": custom_range}, cast(Function, Function('_factorial')) : {"dim_func": factorial, "sympy_func": CustomFactorial}, cast(Function, Function('_add')) : {"dim_func": custom_add_dims, "sympy_func": Add}, cast(Function, Function('_Pow')) : {"dim_func": custom_pow_dims, "sympy_func": custom_pow}, @@ -1502,11 +1509,12 @@ def replace_sympy_funcs_with_placeholder_funcs(expression: Expr) -> Expr: return expression + def replace_placeholder_funcs(expr: Expr, func_key: Literal["dim_func"] | Literal["sympy_func"], placeholder_map: dict[Function, PlaceholderFunction], placeholder_set: set[Function], - dim_values_dict: dict[tuple[Basic,...], list[Expr]], + dim_values_dict: dict[tuple[Basic,...], DimValues], function_parents: list[Basic], data_table_subs: DataTableSubs | None) -> Expr: @@ -1536,15 +1544,17 @@ def replace_placeholder_funcs(expr: Expr, if func_key == "sympy_func": child_expr = expr.args[1] function_parents_snapshot = list(function_parents) - dim_values = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) for arg in child_expr.args] + dim_args = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) for arg in child_expr.args] + result = cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(*dim_args)) if data_table_subs is not None and len(data_table_subs.subs_stack) > 0: - dim_values_snapshot = list(dim_values) - for i, value in enumerate(dim_values_snapshot): - dim_values_snapshot[i] = cast(Expr, value.subs({key: cast(Matrix, value)[0,0] for key, value in data_table_subs.subs_stack[-1].items()})) - dim_values_dict[(expr.args[0], *function_parents_snapshot)] = dim_values_snapshot + dim_args_snapshot = list(dim_args) + for i, value in enumerate(dim_args_snapshot): + dim_args_snapshot[i] = cast(Expr, value.subs({key: cast(Matrix, value)[0,0] for key, value in data_table_subs.subs_stack[-1].items()})) + result_snapshot = cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(*dim_args_snapshot)) + dim_values_dict[(expr.args[0], *function_parents_snapshot)] = DimValues(args=dim_args_snapshot, result=result_snapshot) else: - dim_values_dict[(expr.args[0], *function_parents_snapshot)] = dim_values - return cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(*dim_values)) + dim_values_dict[(expr.args[0], *function_parents_snapshot)] = DimValues(args=dim_args, result=result) + return result else: child_expr = expr.args[1] dim_values = dim_values_dict[(expr.args[0],*function_parents)] @@ -1607,7 +1617,7 @@ def get_dimensional_analysis_expression(parameter_subs: dict[Symbol, Expr], expression: Expr, placeholder_map: dict[Function, PlaceholderFunction], placeholder_set: set[Function], - dim_values_dict: dict[tuple[Basic,...], list[Expr]]) -> tuple[Expr | None, Exception | None]: + dim_values_dict: dict[tuple[Basic,...], DimValues]) -> tuple[Expr | None, Exception | None]: expression_with_parameter_subs = cast(Expr, expression.xreplace(parameter_subs)) @@ -2342,9 +2352,9 @@ def get_evaluated_expression(expression: Expr, parameter_subs: dict[Symbol, Expr], simplify_symbolic_expressions: bool, placeholder_map: dict[Function, PlaceholderFunction], - placeholder_set: set[Function]) -> tuple[ExprWithAssumptions, str | list[list[str]], dict[tuple[Basic,...],list[Expr]]]: + placeholder_set: set[Function]) -> tuple[ExprWithAssumptions, str | list[list[str]], dict[tuple[Basic,...],DimValues]]: expression = cast(Expr, expression.xreplace(parameter_subs)) - dim_values_dict: dict[tuple[Basic,...],list[Expr]] = {} + dim_values_dict: dict[tuple[Basic,...], DimValues] = {} expression = replace_placeholder_funcs(expression, "sympy_func", placeholder_map, diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index f2921136..6226816b 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -18,7 +18,7 @@ import { type Insertion, type Replacement, applyEdits, import { RESERVED, GREEK_CHARS, UNASSIGNABLE, COMPARISON_MAP, UNITS_WITH_OFFSET, TYPE_PARSING_ERRORS, BUILTIN_FUNCTION_MAP, - ZERO_PLACEHOLDER } from "./constants.js"; + BUILTIN_FUNCTION_NEEDS_VALUES, ZERO_PLACEHOLDER } from "./constants.js"; import { MAX_MATRIX_COLS } from "../constants"; @@ -1273,7 +1273,12 @@ export class LatexToSympy extends LatexParserVisitor { - await page.setLatex(0, String.raw`\mathrm{range}\left(1\left\lbrack m\right\rbrack,2\left\lbrack m\right\rbrack,.1\left\lbrack m\right\rbrack\right)=`); +test('Test range with consistent units', async () => { + await page.setLatex(0, String.raw`\mathrm{range}\left(1\left\lbrack m\right\rbrack,2\left\lbrack m\right\rbrack,1\left\lbrack m\right\rbrack\right)=`); + + await page.waitForSelector('text=Updating...', {state: 'detached'}); + + let content = await page.textContent(`#result-value-0`); + expect(content).toBe(String.raw`\begin{bmatrix} 1\left\lbrack m\right\rbrack \\ 2\left\lbrack m\right\rbrack \end{bmatrix}`); +}); + +test('Test range with inconsistent units', async () => { + await page.setLatex(0, String.raw`\mathrm{range}\left(1\left\lbrack m\right\rbrack,2\left\lbrack s\right\rbrack,1\left\lbrack m\right\rbrack\right)=`); await page.waitForSelector('text=Updating...', {state: 'detached'}); From 3643dc22764bb8640e653a4e8e3199db86a811ff Mon Sep 17 00:00:00 2001 From: mgreminger Date: Wed, 8 Jan 2025 00:09:27 -0600 Subject: [PATCH 19/53] fix: fix typescript compiler error Removed remaining unitless sub query code --- src/cells/FluidCell.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cells/FluidCell.ts b/src/cells/FluidCell.ts index 21c834fa..37b656b9 100644 --- a/src/cells/FluidCell.ts +++ b/src/cells/FluidCell.ts @@ -294,10 +294,8 @@ export default class FluidCell extends BaseCell { name: this.mathField.statement.name, sympy: `${fluidFuncName}(0,0)`, params: [], - isUnitlessSubExpression: false, isFunctionArgument: false, isFunction: false, - unitlessSubExpressions: [], implicitParams: [], functions: [], arguments: [], From 0ba5ebabef2e4ce9d5fa07549a6b0b09ab9fdc06 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Wed, 8 Jan 2025 10:47:34 -0600 Subject: [PATCH 20/53] tests: add test for nested function calls with exponents and units --- public/dimensional_analysis.py | 1 - tests/test_basic.spec.mjs | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index cfbd0b39..4ff90c6a 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1496,7 +1496,6 @@ def get_next_id(self): global_placeholder_set = set(global_placeholder_map.keys()) dummy_var_placeholder_set = (Function('_Derivative'), Function('_Integral')) -dim_needs_values_wrapper_placeholder_set = (Function('_Pow'), Function('_IndexMatrix')) placeholder_inverse_map = { value["sympy_func"]: key for key, value in reversed(global_placeholder_map.items()) } placeholder_inverse_set = set(placeholder_inverse_map.keys()) diff --git a/tests/test_basic.spec.mjs b/tests/test_basic.spec.mjs index 7057420e..022aa743 100644 --- a/tests/test_basic.spec.mjs +++ b/tests/test_basic.spec.mjs @@ -750,6 +750,22 @@ test('Test function notation with exponents and units', async () => { }); +test('Test function notation with exponents and units and nested functions', async () => { + + await page.setLatex(0, String.raw`t\left(s=y\left(x=2\left\lbrack in\right\rbrack\right)\cdot1\left\lbrack in\right\rbrack\right)=`); + await page.click('#add-math-cell'); + await page.setLatex(1, String.raw`t=2^{\frac{s}{1\left\lbrack in\right\rbrack}}`); + await page.click('#add-math-cell'); + await page.setLatex(2, String.raw`y=3^{\frac{x}{1\left\lbrack in\right\rbrack}}`); + + await page.waitForSelector('text=Updating...', {state: 'detached'}); + + let content = await page.textContent('#result-value-0'); + expect(parseLatexFloat(content)).toBeCloseTo(512, precision-1); + content = await page.textContent('#result-units-0'); + expect(content).toBe(''); +}); + test('Test function notation with integrals', async () => { From c79931776a2165bfe0a12ae70fba380ef2f1ec8f Mon Sep 17 00:00:00 2001 From: mgreminger Date: Thu, 9 Jan 2025 10:57:08 -0600 Subject: [PATCH 21/53] fix: fix zero cancelling bug in dimensional analysis Test has been added --- public/dimensional_analysis.py | 12 ++++++++---- src/parser/LatexToSympy.ts | 4 ++++ tests/test_basic.spec.mjs | 16 +++++++++++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 4ff90c6a..61784324 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1516,10 +1516,14 @@ def replace_placeholder_funcs(expr: Expr, dim_values_dict: dict[tuple[Basic,...], DimValues], function_parents: list[Basic], data_table_subs: DataTableSubs | None) -> Expr: + + if (not is_matrix(expr)): + if isinstance(expr, Symbol) and expr.name == "_zero_delayed_substitution": + return sympify('0') - if (not is_matrix(expr)) and expr.func == function_id_wrapper: - function_parents.append(expr.args[0]) - expr = cast(Expr, expr.args[1]) + elif expr.func == function_id_wrapper: + function_parents.append(expr.args[0]) + expr = cast(Expr, expr.args[1]) if is_matrix(expr): rows = [] @@ -1736,7 +1740,7 @@ def get_sorted_statements(statements: list[Statement], custom_definition_names: zero_place_holder: ImplicitParameter = { "dimensions": [0]*9, "original_value": "0", - "si_value": "0", + "si_value": "_zero_delayed_substitution", "name": ZERO_PLACEHOLDER, "units": "" } diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index 6226816b..70301b60 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -1976,6 +1976,10 @@ export class LatexToSympy extends LatexParserVisitor { + + await page.setLatex(0, String.raw`y=\frac{0\left\lbrack m\right\rbrack}{2^{x}}`); + await page.click('#add-math-cell'); + await page.setLatex(1, String.raw`y\left(x=1\right)=`); + + await page.waitForSelector('text=Updating...', {state: 'detached'}); + + let content = await page.textContent('#result-value-1'); + expect(parseLatexFloat(content)).toBeCloseTo(0, precision); + content = await page.textContent('#result-units-1'); + expect(content).toBe('m'); +}); + test('Test function notation with integrals', async () => { From 3caee939f2e4170c6caf0c86cc27f4220ff91922 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Thu, 9 Jan 2025 12:51:13 -0600 Subject: [PATCH 22/53] fix: fix function with zero with units arg regression --- public/dimensional_analysis.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 61784324..bb03db12 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1517,13 +1517,12 @@ def replace_placeholder_funcs(expr: Expr, function_parents: list[Basic], data_table_subs: DataTableSubs | None) -> Expr: - if (not is_matrix(expr)): - if isinstance(expr, Symbol) and expr.name == "_zero_delayed_substitution": - return sympify('0') + if (not is_matrix(expr)) and expr.func == function_id_wrapper: + function_parents.append(expr.args[0]) + expr = cast(Expr, expr.args[1]) - elif expr.func == function_id_wrapper: - function_parents.append(expr.args[0]) - expr = cast(Expr, expr.args[1]) + if (not is_matrix(expr)) and isinstance(expr, Symbol) and expr.name == "_zero_delayed_substitution": + return sympify('0') if is_matrix(expr): rows = [] From 2bd9f135cce667b055bb88f8a3f3c3b4125ec3d9 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Thu, 9 Jan 2025 13:30:41 -0600 Subject: [PATCH 23/53] fix: fix numerical solve with zero units starting guess regression --- src/parser/LatexToSympy.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index 70301b60..657bea56 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -385,6 +385,9 @@ export class LatexToSympy extends LatexParserVisitor Date: Fri, 10 Jan 2025 00:11:37 -0600 Subject: [PATCH 24/53] fix: fix error recognizing small differences in dimension exponents as being equal Test added --- public/dimensional_analysis.py | 30 ++++++++++++++++++++++++++---- tests/test_basic.spec.mjs | 22 ++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index bb03db12..a9c5d40e 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -69,6 +69,8 @@ Integer ) +from sympy import S + class ExprWithAssumptions(Expr): is_finite: bool is_integer: bool @@ -867,7 +869,22 @@ def custom_latex(expression: Expr) -> str: _range = Function("_range") -def ensure_dims_all_compatible(*args): +def dimensional_dependencies_equal(input1, input2): + keys1 = set(input1.keys()) + keys2 = set(input2.keys()) + + for key in keys1 ^ keys2: # symmetric difference + if input1.get(key, S.Zero).round(EXP_NUM_DIGITS) != S.Zero or \ + input2.get(key, S.Zero).round(EXP_NUM_DIGITS) != S.Zero: + return False + + for key in keys1 & keys2: # union + if input1[key].round(EXP_NUM_DIGITS) != input2[key].round(EXP_NUM_DIGITS): + return False + + return True + +def ensure_dims_all_compatible(*args, error_message=None): if args[0].is_zero: if all(arg.is_zero for arg in args): first_arg = sympify('0') @@ -880,10 +897,14 @@ def ensure_dims_all_compatible(*args): return first_arg first_arg_dims = custom_get_dimensional_dependencies(first_arg) - if all(custom_get_dimensional_dependencies(arg) == first_arg_dims for arg in args[1:]): + + if all(dimensional_dependencies_equal(custom_get_dimensional_dependencies(arg), first_arg_dims) for arg in args[1:]): return first_arg - raise TypeError('All input arguments to function need to have compatible units') + if error_message is None: + raise TypeError('All input arguments to function need to have compatible units') + else: + raise TypeError(error_message) def ensure_dims_all_compatible_scalar_or_matrix(*args): if len(args) == 1 and is_matrix(args[0]): @@ -1161,7 +1182,8 @@ def custom_integral_dims(local_expr: Expr, global_expr: Expr, dummy_integral_var return global_expr * integral_var # type: ignore def custom_add_dims(*args: Expr): - return Add(*[Abs(arg) for arg in args]) + return ensure_dims_all_compatible(*[Abs(arg) for arg in args], + error_message="Only equivalent dimensions can be added or subtracted.") def custom_pow(base: Expr, exponent: Expr): large_rational = False diff --git a/tests/test_basic.spec.mjs b/tests/test_basic.spec.mjs index 95ea83d8..192a1110 100644 --- a/tests/test_basic.spec.mjs +++ b/tests/test_basic.spec.mjs @@ -780,6 +780,28 @@ test('Test zero canceling bug with exponent', async () => { expect(content).toBe('m'); }); +test('Test floating point exponent rounding', async () => { + + await page.setLatex(0, String.raw`1\left\lbrack m\right\rbrack+1\left\lbrack\frac{N^{\frac13}}{m^{\frac23}}\right\rbrack\cdot1\left\lbrack\frac{m^{\frac53}}{N^{\frac13}}\right\rbrack=`); + await page.click('#add-math-cell'); + await page.setLatex(1, String.raw`1\left\lbrack N\cdot s^{.0000000000001}\right\rbrack+2\left\lbrack N\right\rbrack=`); + await page.click('#add-math-cell'); + await page.setLatex(2, String.raw`1\left\lbrack N\cdot s^{.000000000001}\right\rbrack+2\left\lbrack N\right\rbrack=`); + + await page.waitForSelector('text=Updating...', {state: 'detached'}); + + let content = await page.textContent('#result-value-0'); + expect(parseLatexFloat(content)).toBeCloseTo(2, precision); + content = await page.textContent('#result-units-0'); + expect(content).toBe('m'); + + content = await page.textContent('#result-value-1'); + expect(parseLatexFloat(content)).toBeCloseTo(3, precision); + content = await page.textContent('#result-units-1'); + expect(content).toBe('N'); + + await expect(page.locator('#cell-2 >> text=Dimension Error')).toBeVisible(); +}); test('Test function notation with integrals', async () => { From 1be9d2877a85c256a89de452c0987a724b32a120 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Fri, 10 Jan 2025 01:14:36 -0600 Subject: [PATCH 25/53] Revert "fix: fix numerical solve with zero units starting guess regression" This reverts commit 2bd9f135cce667b055bb88f8a3f3c3b4125ec3d9. --- src/parser/LatexToSympy.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index 657bea56..70301b60 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -385,9 +385,6 @@ export class LatexToSympy extends LatexParserVisitor Date: Fri, 10 Jan 2025 01:18:42 -0600 Subject: [PATCH 26/53] fix: preventing rounding off of exponents for dimensional analysis Rounding off can prevent addition from working --- public/dimensional_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index a9c5d40e..9e278280 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1199,7 +1199,7 @@ def custom_pow(base: Expr, exponent: Expr): def custom_pow_dims(dim_values: DimValues, base: Expr, exponent: Expr): if custom_get_dimensional_dependencies(exponent) != {}: raise TypeError('Exponent Not Dimensionless') - return Pow(base.evalf(PRECISION), (dim_values["args"][1]).evalf(PRECISION)) + return Pow(base, dim_values["args"][1]) CP = None From 1d0dbbdf928341215601c007cfda10d4a51fedef Mon Sep 17 00:00:00 2001 From: mgreminger Date: Thu, 9 Jan 2025 13:30:41 -0600 Subject: [PATCH 27/53] fix: fix numerical solve with zero units starting guess regression --- src/parser/LatexToSympy.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index 70301b60..657bea56 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -385,6 +385,9 @@ export class LatexToSympy extends LatexParserVisitor Date: Fri, 10 Jan 2025 01:24:33 -0600 Subject: [PATCH 28/53] Revert "fix: fix error recognizing small differences in dimension exponents as being equal" This reverts commit d060821506340cd3390e56a0ac7e23e37ff00b9c. --- public/dimensional_analysis.py | 30 ++++-------------------------- tests/test_basic.spec.mjs | 22 ---------------------- 2 files changed, 4 insertions(+), 48 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 9e278280..b5f202b5 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -69,8 +69,6 @@ Integer ) -from sympy import S - class ExprWithAssumptions(Expr): is_finite: bool is_integer: bool @@ -869,22 +867,7 @@ def custom_latex(expression: Expr) -> str: _range = Function("_range") -def dimensional_dependencies_equal(input1, input2): - keys1 = set(input1.keys()) - keys2 = set(input2.keys()) - - for key in keys1 ^ keys2: # symmetric difference - if input1.get(key, S.Zero).round(EXP_NUM_DIGITS) != S.Zero or \ - input2.get(key, S.Zero).round(EXP_NUM_DIGITS) != S.Zero: - return False - - for key in keys1 & keys2: # union - if input1[key].round(EXP_NUM_DIGITS) != input2[key].round(EXP_NUM_DIGITS): - return False - - return True - -def ensure_dims_all_compatible(*args, error_message=None): +def ensure_dims_all_compatible(*args): if args[0].is_zero: if all(arg.is_zero for arg in args): first_arg = sympify('0') @@ -897,14 +880,10 @@ def ensure_dims_all_compatible(*args, error_message=None): return first_arg first_arg_dims = custom_get_dimensional_dependencies(first_arg) - - if all(dimensional_dependencies_equal(custom_get_dimensional_dependencies(arg), first_arg_dims) for arg in args[1:]): + if all(custom_get_dimensional_dependencies(arg) == first_arg_dims for arg in args[1:]): return first_arg - if error_message is None: - raise TypeError('All input arguments to function need to have compatible units') - else: - raise TypeError(error_message) + raise TypeError('All input arguments to function need to have compatible units') def ensure_dims_all_compatible_scalar_or_matrix(*args): if len(args) == 1 and is_matrix(args[0]): @@ -1182,8 +1161,7 @@ def custom_integral_dims(local_expr: Expr, global_expr: Expr, dummy_integral_var return global_expr * integral_var # type: ignore def custom_add_dims(*args: Expr): - return ensure_dims_all_compatible(*[Abs(arg) for arg in args], - error_message="Only equivalent dimensions can be added or subtracted.") + return Add(*[Abs(arg) for arg in args]) def custom_pow(base: Expr, exponent: Expr): large_rational = False diff --git a/tests/test_basic.spec.mjs b/tests/test_basic.spec.mjs index 192a1110..95ea83d8 100644 --- a/tests/test_basic.spec.mjs +++ b/tests/test_basic.spec.mjs @@ -780,28 +780,6 @@ test('Test zero canceling bug with exponent', async () => { expect(content).toBe('m'); }); -test('Test floating point exponent rounding', async () => { - - await page.setLatex(0, String.raw`1\left\lbrack m\right\rbrack+1\left\lbrack\frac{N^{\frac13}}{m^{\frac23}}\right\rbrack\cdot1\left\lbrack\frac{m^{\frac53}}{N^{\frac13}}\right\rbrack=`); - await page.click('#add-math-cell'); - await page.setLatex(1, String.raw`1\left\lbrack N\cdot s^{.0000000000001}\right\rbrack+2\left\lbrack N\right\rbrack=`); - await page.click('#add-math-cell'); - await page.setLatex(2, String.raw`1\left\lbrack N\cdot s^{.000000000001}\right\rbrack+2\left\lbrack N\right\rbrack=`); - - await page.waitForSelector('text=Updating...', {state: 'detached'}); - - let content = await page.textContent('#result-value-0'); - expect(parseLatexFloat(content)).toBeCloseTo(2, precision); - content = await page.textContent('#result-units-0'); - expect(content).toBe('m'); - - content = await page.textContent('#result-value-1'); - expect(parseLatexFloat(content)).toBeCloseTo(3, precision); - content = await page.textContent('#result-units-1'); - expect(content).toBe('N'); - - await expect(page.locator('#cell-2 >> text=Dimension Error')).toBeVisible(); -}); test('Test function notation with integrals', async () => { From 131357537c5600e0c0a28c336dad32805e17206a Mon Sep 17 00:00:00 2001 From: mgreminger Date: Fri, 10 Jan 2025 22:00:15 -0600 Subject: [PATCH 29/53] fix: retry at fixing recognizing small differences in dimension exponents as compatible for addition --- playwright.config.mjs | 2 +- public/dimensional_analysis.py | 88 +++++++++++++++++++++++++++++++++- tests/test_basic.spec.mjs | 22 ++++++++- 3 files changed, 108 insertions(+), 4 deletions(-) diff --git a/playwright.config.mjs b/playwright.config.mjs index 2c18b63f..bc920a2e 100644 --- a/playwright.config.mjs +++ b/playwright.config.mjs @@ -27,7 +27,7 @@ const config = { forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 4 : 8, + workers: process.env.CI ? 4 : 4, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: process.env.CI ? 'github' : 'list', reportSlowTests: null, diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index b5f202b5..d7dfed95 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -12,6 +12,8 @@ import traceback from importlib import import_module +import collections + from json import loads, dumps import math @@ -97,12 +99,14 @@ class ExprWithAssumptions(Expr): amount_of_substance, angle, information) dimension_symbols = set((dimension.name for dimension in dimensions)) -from sympy.physics.units.systems.si import dimsys_SI +from sympy.physics.units.systems.si import dimsys_SI, DimensionSystem from sympy.utilities.iterables import topological_sort from sympy.utilities.lambdify import lambdify, implemented_function +from sympy.functions.elementary.trigonometric import TrigonometricFunction + import numbers from typing import TypedDict, Literal, cast, TypeGuard, Sequence, Any, Callable, NotRequired @@ -744,6 +748,86 @@ def get_base_units(custom_base_units: CustomBaseUnits | None= None) -> dict[tupl ZERO_PLACEHOLDER = "implicit_param__zero" +# Monkey patch of SymPy's get_dimensional_dependencies so that units that have a small +# exponent difference (within EXP_NUM_DIGITS) are still considered equivalent for addition +def custom_get_dimensional_dependencies_for_name(self, dimension): + if isinstance(dimension, str): + dimension = Dimension(Symbol(dimension)) + elif not isinstance(dimension, Dimension): + dimension = Dimension(dimension) + + if dimension.name.is_Symbol: + # Dimensions not included in the dependencies are considered + # as base dimensions: + return dict(self.dimensional_dependencies.get(dimension, {dimension: 1})) + + if dimension.name.is_number or dimension.name.is_NumberSymbol: + return {} + + get_for_name = self._get_dimensional_dependencies_for_name + + if dimension.name.is_Mul: + ret = collections.defaultdict(int) + dicts = [get_for_name(i) for i in dimension.name.args] + for d in dicts: + for k, v in d.items(): + ret[k] += v + return {k: v for (k, v) in ret.items() if v != 0} + + if dimension.name.is_Add: + dicts = [get_for_name(i) for i in dimension.name.args] + + for d in dicts: + keys_to_remove = set() + for key, exp in d.items(): + new_exp = exp.round(EXP_NUM_DIGITS) + if new_exp == sympify("0"): + keys_to_remove.add(key) + else: + d[key] = new_exp + + for key in keys_to_remove: + d.pop(key) + + if all(d == dicts[0] for d in dicts[1:]): + return dicts[0] + raise TypeError("Only equivalent dimensions can be added or subtracted.") + + if dimension.name.is_Pow: + dim_base = get_for_name(dimension.name.base) + dim_exp = get_for_name(dimension.name.exp) + if dim_exp == {} or dimension.name.exp.is_Symbol: + return {k: v * dimension.name.exp for (k, v) in dim_base.items()} + else: + raise TypeError("The exponent for the power operator must be a Symbol or dimensionless.") + + if dimension.name.is_Function: + args = (Dimension._from_dimensional_dependencies( # type: ignore + get_for_name(arg)) for arg in dimension.name.args) + result = dimension.name.func(*args) + + dicts = [get_for_name(i) for i in dimension.name.args] + + if isinstance(result, Dimension): + return self.get_dimensional_dependencies(result) + elif result.func == dimension.name.func: + if isinstance(dimension.name, TrigonometricFunction): + if dicts[0] in ({}, {Dimension('angle'): 1}): + return {} + else: + raise TypeError("The input argument for the function {} must be dimensionless or have dimensions of angle.".format(dimension.func)) + else: + if all(item == {} for item in dicts): + return {} + else: + raise TypeError("The input arguments for the function {} must be dimensionless.".format(dimension.func)) + else: + return get_for_name(result) + + raise TypeError("Type {} not implemented for get_dimensional_dependencies".format(type(dimension.name))) + +DimensionSystem._get_dimensional_dependencies_for_name = custom_get_dimensional_dependencies_for_name # type: ignore + def round_exp(value: float) -> float | int: value = round(value, EXP_NUM_DIGITS) @@ -1177,7 +1261,7 @@ def custom_pow(base: Expr, exponent: Expr): def custom_pow_dims(dim_values: DimValues, base: Expr, exponent: Expr): if custom_get_dimensional_dependencies(exponent) != {}: raise TypeError('Exponent Not Dimensionless') - return Pow(base, dim_values["args"][1]) + return Pow(base.evalf(PRECISION), (dim_values["args"][1]).evalf(PRECISION)) CP = None diff --git a/tests/test_basic.spec.mjs b/tests/test_basic.spec.mjs index 95ea83d8..2793042d 100644 --- a/tests/test_basic.spec.mjs +++ b/tests/test_basic.spec.mjs @@ -780,6 +780,27 @@ test('Test zero canceling bug with exponent', async () => { expect(content).toBe('m'); }); +test('Test floating point exponent rounding', async () => { + await page.setLatex(0, String.raw`1\left\lbrack m\right\rbrack+1\left\lbrack\frac{N^{\frac13}}{m^{\frac23}}\right\rbrack\cdot1\left\lbrack\frac{m^{\frac53}}{N^{\frac13}}\right\rbrack=`); + await page.click('#add-math-cell'); + await page.setLatex(1, String.raw`1\left\lbrack kg\cdot s^{.0000000000001}\right\rbrack+2\left\lbrack kg\right\rbrack=`); + await page.click('#add-math-cell'); + await page.setLatex(2, String.raw`1\left\lbrack kg\cdot s^{.000000000001}\right\rbrack+2\left\lbrack kg\right\rbrack=`); + + await page.waitForSelector('text=Updating...', {state: 'detached'}); + + let content = await page.textContent('#result-value-0'); + expect(parseLatexFloat(content)).toBeCloseTo(2, precision); + content = await page.textContent('#result-units-0'); + expect(content).toBe('m'); + + content = await page.textContent('#result-value-1'); + expect(parseLatexFloat(content)).toBeCloseTo(3, precision); + content = await page.textContent('#result-units-1'); + expect(content).toBe('kg'); + + await expect(page.locator('#cell-2 >> text=Dimension Error')).toBeVisible(); +}); test('Test function notation with integrals', async () => { @@ -804,7 +825,6 @@ test('Test function notation with integrals', async () => { }); - test('Test greek characters as variables', async () => { await page.type(':nth-match(math-field.editable, 1)', 'alpha+beta+gamma+delta+epsilon+zeta+eta+theta+iota+kappa+lambda+' + From 1e23b20c36cb04e36700f6e499ec09c71786a988 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Fri, 10 Jan 2025 22:18:56 -0600 Subject: [PATCH 30/53] fix: apply same exponent comparison logic used for adding to ensure_dims_all_compatible --- public/dimensional_analysis.py | 23 ++++++----------------- tests/test_basic.spec.mjs | 13 ++++++++++++- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index d7dfed95..6da84e70 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -952,22 +952,12 @@ def custom_latex(expression: Expr) -> str: _range = Function("_range") def ensure_dims_all_compatible(*args): - if args[0].is_zero: - if all(arg.is_zero for arg in args): - first_arg = sympify('0') - else: - first_arg = sympify('1') - else: - first_arg = args[0] - - if len(args) == 1: - return first_arg - - first_arg_dims = custom_get_dimensional_dependencies(first_arg) - if all(custom_get_dimensional_dependencies(arg) == first_arg_dims for arg in args[1:]): - return first_arg - - raise TypeError('All input arguments to function need to have compatible units') + try: + # try adding, will only succeed for compatible units + custom_get_dimensional_dependencies(custom_add_dims(*args)) + except TypeError: + raise TypeError('All input arguments to function need to have compatible units') + return args[0] def ensure_dims_all_compatible_scalar_or_matrix(*args): if len(args) == 1 and is_matrix(args[0]): @@ -1752,7 +1742,6 @@ def dimensional_analysis(dimensional_analysis_expression: Expr | None, dim_sub_e except TypeError as e: result = f"Dimension Error: {e}" result_latex = result - print(result) return result, result_latex, custom_units_defined, custom_units, custom_units_latex diff --git a/tests/test_basic.spec.mjs b/tests/test_basic.spec.mjs index 2793042d..e0153732 100644 --- a/tests/test_basic.spec.mjs +++ b/tests/test_basic.spec.mjs @@ -786,6 +786,10 @@ test('Test floating point exponent rounding', async () => { await page.setLatex(1, String.raw`1\left\lbrack kg\cdot s^{.0000000000001}\right\rbrack+2\left\lbrack kg\right\rbrack=`); await page.click('#add-math-cell'); await page.setLatex(2, String.raw`1\left\lbrack kg\cdot s^{.000000000001}\right\rbrack+2\left\lbrack kg\right\rbrack=`); + await page.click('#add-math-cell'); + await page.setLatex(3, String.raw`\mathrm{sum}\left(1\left\lbrack K\cdot s^{.0000000000001}\right\rbrack,3\left\lbrack K\right\rbrack\right)=`); + await page.click('#add-math-cell'); + await page.setLatex(4, String.raw`\mathrm{sum}\left(1\left\lbrack K\cdot s^{.000000000001}\right\rbrack,3\left\lbrack K\right\rbrack\right)=`); await page.waitForSelector('text=Updating...', {state: 'detached'}); @@ -799,7 +803,14 @@ test('Test floating point exponent rounding', async () => { content = await page.textContent('#result-units-1'); expect(content).toBe('kg'); - await expect(page.locator('#cell-2 >> text=Dimension Error')).toBeVisible(); + await expect(page.locator('#cell-2 >> text=Dimension Error: Only equivalent dimensions can be added or subtracted')).toBeVisible(); + + content = await page.textContent('#result-value-3'); + expect(parseLatexFloat(content)).toBeCloseTo(4, precision); + content = await page.textContent('#result-units-3'); + expect(content).toBe('K'); + + await expect(page.locator('#cell-4 >> text=Dimension Error: All input arguments to function need to have compatible units')).toBeVisible(); }); test('Test function notation with integrals', async () => { From bec2e6e0c4ce307aa2da8094fde19a974106c1e9 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Fri, 10 Jan 2025 22:49:40 -0600 Subject: [PATCH 31/53] fix: fix dimensional analysis regression Check wasn't working for non-sympy dimension exponents --- public/dimensional_analysis.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 6da84e70..9ddc462f 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -780,6 +780,11 @@ def custom_get_dimensional_dependencies_for_name(self, dimension): for d in dicts: keys_to_remove = set() for key, exp in d.items(): + if isinstance(exp, int): + exp = sympify(float(exp)) + elif isinstance(exp, float): + exp = sympify(exp) + new_exp = exp.round(EXP_NUM_DIGITS) if new_exp == sympify("0"): keys_to_remove.add(key) From c9fef7fcebe27435b5e6a41dbc4e9d47289358b1 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Fri, 10 Jan 2025 23:03:42 -0600 Subject: [PATCH 32/53] Revert "fix: apply same exponent comparison logic used for adding to ensure_dims_all_compatible" This reverts commit 1e23b20c36cb04e36700f6e499ec09c71786a988. --- public/dimensional_analysis.py | 23 +++++++++++++++++------ tests/test_basic.spec.mjs | 13 +------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 9ddc462f..2112dadd 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -957,12 +957,22 @@ def custom_latex(expression: Expr) -> str: _range = Function("_range") def ensure_dims_all_compatible(*args): - try: - # try adding, will only succeed for compatible units - custom_get_dimensional_dependencies(custom_add_dims(*args)) - except TypeError: - raise TypeError('All input arguments to function need to have compatible units') - return args[0] + if args[0].is_zero: + if all(arg.is_zero for arg in args): + first_arg = sympify('0') + else: + first_arg = sympify('1') + else: + first_arg = args[0] + + if len(args) == 1: + return first_arg + + first_arg_dims = custom_get_dimensional_dependencies(first_arg) + if all(custom_get_dimensional_dependencies(arg) == first_arg_dims for arg in args[1:]): + return first_arg + + raise TypeError('All input arguments to function need to have compatible units') def ensure_dims_all_compatible_scalar_or_matrix(*args): if len(args) == 1 and is_matrix(args[0]): @@ -1747,6 +1757,7 @@ def dimensional_analysis(dimensional_analysis_expression: Expr | None, dim_sub_e except TypeError as e: result = f"Dimension Error: {e}" result_latex = result + print(result) return result, result_latex, custom_units_defined, custom_units, custom_units_latex diff --git a/tests/test_basic.spec.mjs b/tests/test_basic.spec.mjs index e0153732..2793042d 100644 --- a/tests/test_basic.spec.mjs +++ b/tests/test_basic.spec.mjs @@ -786,10 +786,6 @@ test('Test floating point exponent rounding', async () => { await page.setLatex(1, String.raw`1\left\lbrack kg\cdot s^{.0000000000001}\right\rbrack+2\left\lbrack kg\right\rbrack=`); await page.click('#add-math-cell'); await page.setLatex(2, String.raw`1\left\lbrack kg\cdot s^{.000000000001}\right\rbrack+2\left\lbrack kg\right\rbrack=`); - await page.click('#add-math-cell'); - await page.setLatex(3, String.raw`\mathrm{sum}\left(1\left\lbrack K\cdot s^{.0000000000001}\right\rbrack,3\left\lbrack K\right\rbrack\right)=`); - await page.click('#add-math-cell'); - await page.setLatex(4, String.raw`\mathrm{sum}\left(1\left\lbrack K\cdot s^{.000000000001}\right\rbrack,3\left\lbrack K\right\rbrack\right)=`); await page.waitForSelector('text=Updating...', {state: 'detached'}); @@ -803,14 +799,7 @@ test('Test floating point exponent rounding', async () => { content = await page.textContent('#result-units-1'); expect(content).toBe('kg'); - await expect(page.locator('#cell-2 >> text=Dimension Error: Only equivalent dimensions can be added or subtracted')).toBeVisible(); - - content = await page.textContent('#result-value-3'); - expect(parseLatexFloat(content)).toBeCloseTo(4, precision); - content = await page.textContent('#result-units-3'); - expect(content).toBe('K'); - - await expect(page.locator('#cell-4 >> text=Dimension Error: All input arguments to function need to have compatible units')).toBeVisible(); + await expect(page.locator('#cell-2 >> text=Dimension Error')).toBeVisible(); }); test('Test function notation with integrals', async () => { From 8a8814e0784c137b107df7eb7bd09ac446db4f6e Mon Sep 17 00:00:00 2001 From: mgreminger Date: Sat, 11 Jan 2025 15:34:23 -0600 Subject: [PATCH 33/53] refactor: simplify exponent check for dimensional analysis add method Exponent won't be of float type --- public/dimensional_analysis.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 2112dadd..49207b42 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -782,8 +782,6 @@ def custom_get_dimensional_dependencies_for_name(self, dimension): for key, exp in d.items(): if isinstance(exp, int): exp = sympify(float(exp)) - elif isinstance(exp, float): - exp = sympify(exp) new_exp = exp.round(EXP_NUM_DIGITS) if new_exp == sympify("0"): From 67f7ba41281cefc2246fb9d2f7d6aece5543fb15 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Sat, 11 Jan 2025 19:12:31 -0600 Subject: [PATCH 34/53] fix: use the adding dims logic for functions that accept multiple inputs with same dims --- public/dimensional_analysis.py | 55 +++++++++++++++++----------------- tests/test_basic.spec.mjs | 21 +++++++++++++ 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 49207b42..f8ffb9c2 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -68,7 +68,8 @@ factorial, Basic, Rational, - Integer + Integer, + S ) class ExprWithAssumptions(Expr): @@ -748,6 +749,20 @@ def get_base_units(custom_base_units: CustomBaseUnits | None= None) -> dict[tupl ZERO_PLACEHOLDER = "implicit_param__zero" +def normalize_dims_dict(input): + keys_to_remove = set() + for key, value in input.items(): + new_value = value.round(EXP_NUM_DIGITS) + if new_value == S.Zero: + keys_to_remove.add(key) + else: + input[key] = new_value + + for key in keys_to_remove: + input.pop(key) + + return input + # Monkey patch of SymPy's get_dimensional_dependencies so that units that have a small # exponent difference (within EXP_NUM_DIGITS) are still considered equivalent for addition def custom_get_dimensional_dependencies_for_name(self, dimension): @@ -759,7 +774,7 @@ def custom_get_dimensional_dependencies_for_name(self, dimension): if dimension.name.is_Symbol: # Dimensions not included in the dependencies are considered # as base dimensions: - return dict(self.dimensional_dependencies.get(dimension, {dimension: 1})) + return dict(self.dimensional_dependencies.get(dimension, {dimension: S.One})) if dimension.name.is_number or dimension.name.is_NumberSymbol: return {} @@ -775,22 +790,7 @@ def custom_get_dimensional_dependencies_for_name(self, dimension): return {k: v for (k, v) in ret.items() if v != 0} if dimension.name.is_Add: - dicts = [get_for_name(i) for i in dimension.name.args] - - for d in dicts: - keys_to_remove = set() - for key, exp in d.items(): - if isinstance(exp, int): - exp = sympify(float(exp)) - - new_exp = exp.round(EXP_NUM_DIGITS) - if new_exp == sympify("0"): - keys_to_remove.add(key) - else: - d[key] = new_exp - - for key in keys_to_remove: - d.pop(key) + dicts = [normalize_dims_dict(get_for_name(i)) for i in dimension.name.args] if all(d == dicts[0] for d in dicts[1:]): return dicts[0] @@ -957,17 +957,17 @@ def custom_latex(expression: Expr) -> str: def ensure_dims_all_compatible(*args): if args[0].is_zero: if all(arg.is_zero for arg in args): - first_arg = sympify('0') + first_arg = S.Zero else: - first_arg = sympify('1') + first_arg = S.One else: first_arg = args[0] if len(args) == 1: return first_arg - first_arg_dims = custom_get_dimensional_dependencies(first_arg) - if all(custom_get_dimensional_dependencies(arg) == first_arg_dims for arg in args[1:]): + first_arg_dims = normalize_dims_dict(custom_get_dimensional_dependencies(first_arg)) + if all(normalize_dims_dict(custom_get_dimensional_dependencies(arg)) == first_arg_dims for arg in args[1:]): return first_arg raise TypeError('All input arguments to function need to have compatible units') @@ -1019,7 +1019,7 @@ def ensure_inverse_dims(arg): for j in range(arg.cols): dim, _ = get_mathjs_units(cast(dict[Dimension, float], custom_get_dimensional_dependencies(cast(Expr, arg[j,i])))) if dim == "": - row.append(sympify('0')) + row.append(S.Zero) else: row.append(cast(Expr, arg[j,i])**-1) column_dims.setdefault(i, []).append(dim) @@ -1137,8 +1137,8 @@ def custom_range(*args: Expr): if not all( (arg.is_real and arg.is_finite and not isinstance(arg, Dimension) for arg in args ) ): # type: ignore raise TypeError('All range inputs must be unitless and must evaluate to real and finite values') - start = cast(Expr, sympify('1')) - step = cast(Expr, sympify('1')) + start = cast(Expr, S.One) + step = cast(Expr, S.One) if len(args) == 1: stop = args[0] @@ -1609,7 +1609,7 @@ def replace_placeholder_funcs(expr: Expr, expr = cast(Expr, expr.args[1]) if (not is_matrix(expr)) and isinstance(expr, Symbol) and expr.name == "_zero_delayed_substitution": - return sympify('0') + return S.Zero if is_matrix(expr): rows = [] @@ -1725,7 +1725,7 @@ def get_dimensional_analysis_expression(parameter_subs: dict[Symbol, Expr], def custom_get_dimensional_dependencies(expression: Expr | None): if expression is not None: - expression = subs_wrapper(expression, {cast(Symbol, symbol): sympify('1') for symbol in (expression.free_symbols - dimension_symbols)}) + expression = subs_wrapper(expression, {cast(Symbol, symbol): S.One for symbol in (expression.free_symbols - dimension_symbols)}) return dimsys_SI.get_dimensional_dependencies(expression) def dimensional_analysis(dimensional_analysis_expression: Expr | None, dim_sub_error: Exception | None, @@ -1755,7 +1755,6 @@ def dimensional_analysis(dimensional_analysis_expression: Expr | None, dim_sub_e except TypeError as e: result = f"Dimension Error: {e}" result_latex = result - print(result) return result, result_latex, custom_units_defined, custom_units, custom_units_latex diff --git a/tests/test_basic.spec.mjs b/tests/test_basic.spec.mjs index 2793042d..10731b8f 100644 --- a/tests/test_basic.spec.mjs +++ b/tests/test_basic.spec.mjs @@ -781,12 +781,21 @@ test('Test zero canceling bug with exponent', async () => { }); test('Test floating point exponent rounding', async () => { + // check matching equivalent dims for adding await page.setLatex(0, String.raw`1\left\lbrack m\right\rbrack+1\left\lbrack\frac{N^{\frac13}}{m^{\frac23}}\right\rbrack\cdot1\left\lbrack\frac{m^{\frac53}}{N^{\frac13}}\right\rbrack=`); await page.click('#add-math-cell'); await page.setLatex(1, String.raw`1\left\lbrack kg\cdot s^{.0000000000001}\right\rbrack+2\left\lbrack kg\right\rbrack=`); await page.click('#add-math-cell'); await page.setLatex(2, String.raw`1\left\lbrack kg\cdot s^{.000000000001}\right\rbrack+2\left\lbrack kg\right\rbrack=`); + // check matching equivalent dims for sum function + await page.click('#add-math-cell'); + await page.setLatex(3, String.raw`\mathrm{sum}\left(1\left\lbrack m\right\rbrack,1\left\lbrack\frac{N^{\frac13}}{m^{\frac23}}\right\rbrack\cdot3\left\lbrack\frac{m^{\frac53}}{N^{\frac13}}\right\rbrack\right)=`); + await page.click('#add-math-cell'); + await page.setLatex(4, String.raw`\mathrm{sum}\left(1\left\lbrack K\cdot s^{.0000000000001}\right\rbrack,4\left\lbrack K\right\rbrack\right)=`); + await page.click('#add-math-cell'); + await page.setLatex(5, String.raw`\mathrm{sum}\left(1\left\lbrack K\cdot s^{.000000000001}\right\rbrack,4\left\lbrack K\right\rbrack\right)=`); + await page.waitForSelector('text=Updating...', {state: 'detached'}); let content = await page.textContent('#result-value-0'); @@ -800,6 +809,18 @@ test('Test floating point exponent rounding', async () => { expect(content).toBe('kg'); await expect(page.locator('#cell-2 >> text=Dimension Error')).toBeVisible(); + + content = await page.textContent('#result-value-3'); + expect(parseLatexFloat(content)).toBeCloseTo(4, precision); + content = await page.textContent('#result-units-3'); + expect(content).toBe('m'); + + content = await page.textContent('#result-value-4'); + expect(parseLatexFloat(content)).toBeCloseTo(5, precision); + content = await page.textContent('#result-units-4'); + expect(content).toBe('K'); + + await expect(page.locator('#cell-5 >> text=Dimension Error')).toBeVisible(); }); test('Test function notation with integrals', async () => { From 77dcdbc0fe22ce6147218e3615daef572e558054 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Sat, 11 Jan 2025 21:20:33 -0600 Subject: [PATCH 35/53] fix: regression recognition of very small unit exponent as unitless --- public/dimensional_analysis.py | 6 +++--- tests/test_basic.spec.mjs | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index f8ffb9c2..5b0c5662 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -966,8 +966,8 @@ def ensure_dims_all_compatible(*args): if len(args) == 1: return first_arg - first_arg_dims = normalize_dims_dict(custom_get_dimensional_dependencies(first_arg)) - if all(normalize_dims_dict(custom_get_dimensional_dependencies(arg)) == first_arg_dims for arg in args[1:]): + first_arg_dims = custom_get_dimensional_dependencies(first_arg) + if all(custom_get_dimensional_dependencies(arg) == first_arg_dims for arg in args[1:]): return first_arg raise TypeError('All input arguments to function need to have compatible units') @@ -1726,7 +1726,7 @@ def get_dimensional_analysis_expression(parameter_subs: dict[Symbol, Expr], def custom_get_dimensional_dependencies(expression: Expr | None): if expression is not None: expression = subs_wrapper(expression, {cast(Symbol, symbol): S.One for symbol in (expression.free_symbols - dimension_symbols)}) - return dimsys_SI.get_dimensional_dependencies(expression) + return normalize_dims_dict(dimsys_SI.get_dimensional_dependencies(expression)) def dimensional_analysis(dimensional_analysis_expression: Expr | None, dim_sub_error: Exception | None, custom_base_units: CustomBaseUnits | None = None): diff --git a/tests/test_basic.spec.mjs b/tests/test_basic.spec.mjs index 10731b8f..c107c2a4 100644 --- a/tests/test_basic.spec.mjs +++ b/tests/test_basic.spec.mjs @@ -796,6 +796,12 @@ test('Test floating point exponent rounding', async () => { await page.click('#add-math-cell'); await page.setLatex(5, String.raw`\mathrm{sum}\left(1\left\lbrack K\cdot s^{.000000000001}\right\rbrack,4\left\lbrack K\right\rbrack\right)=`); + // check small exponent rounding for dimensionless exponent check + await page.click('#add-math-cell'); + await page.setLatex(6, String.raw`6^{1\left\lbrack s^{.0000000000001}\right\rbrack}=`); + await page.click('#add-math-cell'); + await page.setLatex(7, String.raw`6^{1\left\lbrack s^{.000000000001}\right\rbrack}=`); + await page.waitForSelector('text=Updating...', {state: 'detached'}); let content = await page.textContent('#result-value-0'); @@ -808,7 +814,7 @@ test('Test floating point exponent rounding', async () => { content = await page.textContent('#result-units-1'); expect(content).toBe('kg'); - await expect(page.locator('#cell-2 >> text=Dimension Error')).toBeVisible(); + await expect(page.locator('#cell-2 >> text=Dimension Error: Only equivalent dimensions can be added or subtracted')).toBeVisible(); content = await page.textContent('#result-value-3'); expect(parseLatexFloat(content)).toBeCloseTo(4, precision); @@ -820,7 +826,14 @@ test('Test floating point exponent rounding', async () => { content = await page.textContent('#result-units-4'); expect(content).toBe('K'); - await expect(page.locator('#cell-5 >> text=Dimension Error')).toBeVisible(); + await expect(page.locator('#cell-5 >> text=Dimension Error: All input arguments to function need to have compatible units')).toBeVisible(); + + content = await page.textContent('#result-value-6'); + expect(parseLatexFloat(content)).toBeCloseTo(6, precision); + content = await page.textContent('#result-units-6'); + expect(content).toBe(''); + + await expect(page.locator('#cell-7 >> text=Dimension Error: Exponent Not Dimensionless')).toBeVisible(); }); test('Test function notation with integrals', async () => { From ffeaec1271ebc19f989ca3fc38a43b5e02674fe0 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Sat, 11 Jan 2025 22:03:10 -0600 Subject: [PATCH 36/53] refactor: only perform unit exponent rounding when necessary for comparisons --- public/dimensional_analysis.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 5b0c5662..4d6e1254 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -966,8 +966,8 @@ def ensure_dims_all_compatible(*args): if len(args) == 1: return first_arg - first_arg_dims = custom_get_dimensional_dependencies(first_arg) - if all(custom_get_dimensional_dependencies(arg) == first_arg_dims for arg in args[1:]): + first_arg_dims = normalize_dims_dict(custom_get_dimensional_dependencies(first_arg)) + if all(normalize_dims_dict(custom_get_dimensional_dependencies(arg)) == first_arg_dims for arg in args[1:]): return first_arg raise TypeError('All input arguments to function need to have compatible units') @@ -984,13 +984,13 @@ def ensure_dims_all_compatible_piecewise(*args): return ensure_dims_all_compatible(*[arg[0] for arg in args]) def ensure_unitless_in_angle_out(arg): - if custom_get_dimensional_dependencies(arg) == {}: + if normalize_dims_dict(custom_get_dimensional_dependencies(arg)) == {}: return angle else: raise TypeError('Unitless input argument required for function') def ensure_unitless_in(arg): - if custom_get_dimensional_dependencies(arg) == {}: + if normalize_dims_dict(custom_get_dimensional_dependencies(arg)) == {}: return arg else: raise TypeError('Unitless input argument required for function') @@ -1182,8 +1182,8 @@ def IndexMatrix(expression: Expr, i: Expr, j: Expr) -> Expr: return expression[i-1, j-1] # type: ignore def IndexMatrix_dims(dim_values: DimValues, expression: Expr, i: Expr, j: Expr) -> Expr: - if custom_get_dimensional_dependencies(i) != {} or \ - custom_get_dimensional_dependencies(j) != {}: + if normalize_dims_dict(custom_get_dimensional_dependencies(i)) != {} or \ + normalize_dims_dict(custom_get_dimensional_dependencies(j)) != {}: raise TypeError('Matrix Index Not Dimensionless') i_value = dim_values["args"][1] @@ -1262,7 +1262,7 @@ def custom_pow(base: Expr, exponent: Expr): return Pow(base, exponent) def custom_pow_dims(dim_values: DimValues, base: Expr, exponent: Expr): - if custom_get_dimensional_dependencies(exponent) != {}: + if normalize_dims_dict(custom_get_dimensional_dependencies(exponent)) != {}: raise TypeError('Exponent Not Dimensionless') return Pow(base.evalf(PRECISION), (dim_values["args"][1]).evalf(PRECISION)) @@ -1726,7 +1726,7 @@ def get_dimensional_analysis_expression(parameter_subs: dict[Symbol, Expr], def custom_get_dimensional_dependencies(expression: Expr | None): if expression is not None: expression = subs_wrapper(expression, {cast(Symbol, symbol): S.One for symbol in (expression.free_symbols - dimension_symbols)}) - return normalize_dims_dict(dimsys_SI.get_dimensional_dependencies(expression)) + return dimsys_SI.get_dimensional_dependencies(expression) def dimensional_analysis(dimensional_analysis_expression: Expr | None, dim_sub_error: Exception | None, custom_base_units: CustomBaseUnits | None = None): From b9e2d842d0a10c1b27d9cf4aaf53bc960fdc62fc Mon Sep 17 00:00:00 2001 From: mgreminger Date: Sat, 11 Jan 2025 22:21:13 -0600 Subject: [PATCH 37/53] refactor: simplify matrix inverse dimensions code --- public/dimensional_analysis.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 4d6e1254..7bce5a01 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1017,18 +1017,12 @@ def ensure_inverse_dims(arg): row = [] rows.append(row) for j in range(arg.cols): - dim, _ = get_mathjs_units(cast(dict[Dimension, float], custom_get_dimensional_dependencies(cast(Expr, arg[j,i])))) - if dim == "": - row.append(S.Zero) - else: - row.append(cast(Expr, arg[j,i])**-1) - column_dims.setdefault(i, []).append(dim) - - column_checks = [] - for _, values in column_dims.items(): - column_checks.append(all([value == values[0] for value in values[1:]])) - - if not all(column_checks): + row.append(cast(Expr, arg[j,i])**-1) + column_dims.setdefault(i, []).append(arg[j,i]) + try: + for _, values in column_dims.items(): + ensure_dims_all_compatible(*values) + except TypeError: raise TypeError('Dimensions not consistent for matrix inverse') return Matrix(rows) From cb816a31c262f9205d3043da844ec77b37b2935b Mon Sep 17 00:00:00 2001 From: mgreminger Date: Sat, 11 Jan 2025 23:34:02 -0600 Subject: [PATCH 38/53] feat: add custom error messages for all dimension errors --- public/dimensional_analysis.py | 93 ++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 7bce5a01..9a57c378 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -954,7 +954,7 @@ def custom_latex(expression: Expr) -> str: _range = Function("_range") -def ensure_dims_all_compatible(*args): +def ensure_dims_all_compatible(*args, error_message: str | None = None): if args[0].is_zero: if all(arg.is_zero for arg in args): first_arg = S.Zero @@ -970,30 +970,35 @@ def ensure_dims_all_compatible(*args): if all(normalize_dims_dict(custom_get_dimensional_dependencies(arg)) == first_arg_dims for arg in args[1:]): return first_arg - raise TypeError('All input arguments to function need to have compatible units') + if error_message is None: + raise TypeError('All input arguments to function need to have compatible units') + else: + raise TypeError(error_message) + +def ensure_dims_all_compatible_scalar_or_matrix(*args, func_name = ""): + error_message = f"{func_name} function requires that all input values have the same units" -def ensure_dims_all_compatible_scalar_or_matrix(*args): if len(args) == 1 and is_matrix(args[0]): - return ensure_dims_all_compatible(*args[0]) + return ensure_dims_all_compatible(*args[0], error_message=error_message) else: - return ensure_dims_all_compatible(*args) + return ensure_dims_all_compatible(*args, error_message=error_message) def ensure_dims_all_compatible_piecewise(*args): # Need to make sure first element in tuples passed to Piecewise all have compatible units # The second element of the tuples has already been checked by And, StrictLessThan, etc. - return ensure_dims_all_compatible(*[arg[0] for arg in args]) + return ensure_dims_all_compatible(*[arg[0] for arg in args], error_message="Units not consistent for piecewise cell") -def ensure_unitless_in_angle_out(arg): +def ensure_unitless_in_angle_out(arg, func_name=""): if normalize_dims_dict(custom_get_dimensional_dependencies(arg)) == {}: return angle else: - raise TypeError('Unitless input argument required for function') + raise TypeError(f'Unitless input argument required for {func_name} function') -def ensure_unitless_in(arg): +def ensure_unitless_in(arg, func_name=""): if normalize_dims_dict(custom_get_dimensional_dependencies(arg)) == {}: return arg else: - raise TypeError('Unitless input argument required for function') + raise TypeError(f'Unitless input argument required for {func_name} function') def ensure_any_unit_in_angle_out(arg): # ensure input arg units make sense (will raise if inconsistent) @@ -1019,11 +1024,9 @@ def ensure_inverse_dims(arg): for j in range(arg.cols): row.append(cast(Expr, arg[j,i])**-1) column_dims.setdefault(i, []).append(arg[j,i]) - try: - for _, values in column_dims.items(): - ensure_dims_all_compatible(*values) - except TypeError: - raise TypeError('Dimensions not consistent for matrix inverse') + + for _, values in column_dims.items(): + ensure_dims_all_compatible(*values, error_message='Dimensions not consistent for matrix inverse') return Matrix(rows) @@ -1159,7 +1162,7 @@ def custom_range(*args: Expr): return Matrix(values) def custom_range_dims(dim_values: DimValues, *args: Expr): - return Matrix([ensure_dims_all_compatible(*args)]*len(cast(Matrix, dim_values["result"]))) + return Matrix([ensure_dims_all_compatible(*args, error_message="All inputs to the range function must have the same units")]*len(cast(Matrix, dim_values["result"]))) class PlaceholderFunction(TypedDict): dim_func: Callable | Function @@ -1236,7 +1239,7 @@ def custom_integral_dims(local_expr: Expr, global_expr: Expr, dummy_integral_var lower_limit: Expr | None = None, upper_limit: Expr | None = None, lower_limit_dims: Expr | None = None, upper_limit_dims: Expr | None = None): if lower_limit is not None and upper_limit is not None: - ensure_dims_all_compatible(lower_limit_dims, upper_limit_dims) + ensure_dims_all_compatible(lower_limit_dims, upper_limit_dims, error_message="Upper and lower integral limits must have the same dimensions") return global_expr * lower_limit_dims # type: ignore else: return global_expr * integral_var # type: ignore @@ -1304,8 +1307,8 @@ def fdiff(self, argindex=1): def fluid_dims(fluid_function: FluidFunction, input1, input2): - ensure_dims_all_compatible(get_dims(fluid_function["input1Dims"]), input1) - ensure_dims_all_compatible(get_dims(fluid_function["input2Dims"]), input2) + ensure_dims_all_compatible(get_dims(fluid_function["input1Dims"]), input1, error_message=f"First input to fluid function {fluid_function['name'].removesuffix('_as_variable')} has the incorrect units") + ensure_dims_all_compatible(get_dims(fluid_function["input2Dims"]), input2, error_message=f"Second input to fluid function {fluid_function['name'].removesuffix('_as_variable')} has the incorrect units") return get_dims(fluid_function["outputDims"]) @@ -1389,9 +1392,9 @@ def fdiff(self, argindex=1): def HA_fluid_dims(fluid_function: FluidFunction, input1, input2, input3): - ensure_dims_all_compatible(get_dims(fluid_function["input1Dims"]), input1) - ensure_dims_all_compatible(get_dims(fluid_function["input2Dims"]), input2) - ensure_dims_all_compatible(get_dims(fluid_function.get("input3Dims", [])), input3) + ensure_dims_all_compatible(get_dims(fluid_function["input1Dims"]), input1, error_message=f"First input to fluid function {fluid_function['name'].removesuffix('_as_variable')} has the incorrect units") + ensure_dims_all_compatible(get_dims(fluid_function["input2Dims"]), input2, error_message=f"Second input to fluid function {fluid_function['name'].removesuffix('_as_variable')} has the incorrect units") + ensure_dims_all_compatible(get_dims(fluid_function.get("input3Dims", [])), input3, error_message=f"Third input to fluid function {fluid_function['name'].removesuffix('_as_variable')} has the incorrect units") return get_dims(fluid_function["outputDims"]) @@ -1446,7 +1449,7 @@ def _imp_(arg1): def _eval_evalf(self, prec): if (len(self.args) != 1): - raise TypeError(f'The interpolation function {interpolation_function["name"]} requires 1 input value, ({len(self.args)} given)') + raise TypeError(f"The interpolation function {interpolation_function['name'].removesuffix('_as_variable')} requires 1 input value, ({len(self.args)} given)") if (self.args[0].is_number): float_input = float(cast(Expr, self.args[0])) @@ -1465,7 +1468,7 @@ def fdiff(self, argindex=1): interpolation_wrapper.__name__ = interpolation_function["name"] def interpolation_dims_wrapper(input): - ensure_dims_all_compatible(get_dims(interpolation_function["inputDims"]), input) + ensure_dims_all_compatible(get_dims(interpolation_function["inputDims"]), input, error_message=f"Incorrect units for interpolation function {interpolation_function['name'].removesuffix('_as_variable')}") return get_dims(interpolation_function["outputDims"]) @@ -1490,7 +1493,7 @@ def eval(cls, arg1: Expr): polyfit_wrapper.__name__ = polyfit_function["name"] def polyfit_dims_wrapper(input): - ensure_dims_all_compatible(get_dims(polyfit_function["inputDims"]), input) + ensure_dims_all_compatible(get_dims(polyfit_function["inputDims"]), input, error_message=f"Incorrect units for polyfit function {polyfit_function['name'].removesuffix('_as_variable')}") return get_dims(polyfit_function["outputDims"]) @@ -1531,28 +1534,28 @@ def get_next_id(self): function_id_wrapper = Function('_function_id_wrapper') global_placeholder_map: dict[Function, PlaceholderFunction] = { - cast(Function, Function('_StrictLessThan')) : {"dim_func": ensure_dims_all_compatible, "sympy_func": StrictLessThan}, - cast(Function, Function('_LessThan')) : {"dim_func": ensure_dims_all_compatible, "sympy_func": LessThan}, - cast(Function, Function('_StrictGreaterThan')) : {"dim_func": ensure_dims_all_compatible, "sympy_func": StrictGreaterThan}, - cast(Function, Function('_GreaterThan')) : {"dim_func": ensure_dims_all_compatible, "sympy_func": GreaterThan}, - cast(Function, Function('_And')) : {"dim_func": ensure_dims_all_compatible, "sympy_func": And}, + cast(Function, Function('_StrictLessThan')) : {"dim_func": partial(ensure_dims_all_compatible, error_message="Piecewise cell comparison dimensions must match"), "sympy_func": StrictLessThan}, + cast(Function, Function('_LessThan')) : {"dim_func": partial(ensure_dims_all_compatible, error_message="Piecewise cell comparison dimensions must match"), "sympy_func": LessThan}, + cast(Function, Function('_StrictGreaterThan')) : {"dim_func": partial(ensure_dims_all_compatible, error_message="Piecewise cell comparison dimensions must match"), "sympy_func": StrictGreaterThan}, + cast(Function, Function('_GreaterThan')) : {"dim_func": partial(ensure_dims_all_compatible, error_message="Piecewise cell comparison dimensions must match"), "sympy_func": GreaterThan}, + cast(Function, Function('_And')) : {"dim_func": partial(ensure_dims_all_compatible, error_message="Piecewise cell comparison dimensions must match"), "sympy_func": And}, cast(Function, Function('_Piecewise')) : {"dim_func": ensure_dims_all_compatible_piecewise, "sympy_func": Piecewise}, - cast(Function, Function('_asin')) : {"dim_func": ensure_unitless_in_angle_out, "sympy_func": asin}, - cast(Function, Function('_acos')) : {"dim_func": ensure_unitless_in_angle_out, "sympy_func": acos}, - cast(Function, Function('_atan')) : {"dim_func": ensure_unitless_in_angle_out, "sympy_func": atan}, - cast(Function, Function('_asec')) : {"dim_func": ensure_unitless_in_angle_out, "sympy_func": asec}, - cast(Function, Function('_acsc')) : {"dim_func": ensure_unitless_in_angle_out, "sympy_func": acsc}, - cast(Function, Function('_acot')) : {"dim_func": ensure_unitless_in_angle_out, "sympy_func": acot}, + cast(Function, Function('_asin')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arcsin"), "sympy_func": asin}, + cast(Function, Function('_acos')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arccos"), "sympy_func": acos}, + cast(Function, Function('_atan')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arctan"), "sympy_func": atan}, + cast(Function, Function('_asec')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arcsec"), "sympy_func": asec}, + cast(Function, Function('_acsc')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arcscs"), "sympy_func": acsc}, + cast(Function, Function('_acot')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arccot"), "sympy_func": acot}, cast(Function, Function('_arg')) : {"dim_func": ensure_any_unit_in_angle_out, "sympy_func": arg}, cast(Function, Function('_re')) : {"dim_func": ensure_any_unit_in_same_out, "sympy_func": re}, cast(Function, Function('_im')) : {"dim_func": ensure_any_unit_in_same_out, "sympy_func": im}, cast(Function, Function('_conjugate')) : {"dim_func": ensure_any_unit_in_same_out, "sympy_func": conjugate}, - cast(Function, Function('_Max')) : {"dim_func": ensure_dims_all_compatible_scalar_or_matrix, "sympy_func": custom_max}, - cast(Function, Function('_Min')) : {"dim_func": ensure_dims_all_compatible_scalar_or_matrix, "sympy_func": custom_min}, - cast(Function, Function('_sum')) : {"dim_func": ensure_dims_all_compatible_scalar_or_matrix, "sympy_func": custom_sum}, - cast(Function, Function('_average')) : {"dim_func": ensure_dims_all_compatible_scalar_or_matrix, "sympy_func": custom_average}, - cast(Function, Function('_stdev')) : {"dim_func": ensure_dims_all_compatible_scalar_or_matrix, "sympy_func": partial(custom_stdev, False)}, - cast(Function, Function('_stdevp')) : {"dim_func": ensure_dims_all_compatible_scalar_or_matrix, "sympy_func": partial(custom_stdev, True)}, + cast(Function, Function('_Max')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="max"), "sympy_func": custom_max}, + cast(Function, Function('_Min')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="min"), "sympy_func": custom_min}, + cast(Function, Function('_sum')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="sum"), "sympy_func": custom_sum}, + cast(Function, Function('_average')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="average"), "sympy_func": custom_average}, + cast(Function, Function('_stdev')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="stdev"), "sympy_func": partial(custom_stdev, False)}, + cast(Function, Function('_stdevp')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="stdevp"), "sympy_func": partial(custom_stdev, True)}, cast(Function, Function('_count')) : {"dim_func": custom_count, "sympy_func": custom_count}, cast(Function, Function('_Abs')) : {"dim_func": ensure_any_unit_in_same_out, "sympy_func": Abs}, cast(Function, Function('_Inverse')) : {"dim_func": ensure_inverse_dims, "sympy_func": UniversalInverse}, @@ -1564,9 +1567,9 @@ def get_next_id(self): cast(Function, Function('_Eq')) : {"dim_func": Eq, "sympy_func": Eq}, cast(Function, Function('_norm')) : {"dim_func": custom_norm, "sympy_func": custom_norm}, cast(Function, Function('_dot')) : {"dim_func": custom_dot, "sympy_func": custom_dot}, - cast(Function, Function('_ceil')) : {"dim_func": ensure_unitless_in, "sympy_func": ceiling}, - cast(Function, Function('_floor')) : {"dim_func": ensure_unitless_in, "sympy_func": floor}, - cast(Function, Function('_round')) : {"dim_func": ensure_unitless_in, "sympy_func": custom_round}, + cast(Function, Function('_ceil')) : {"dim_func": partial(ensure_unitless_in, func_name="ceil"), "sympy_func": ceiling}, + cast(Function, Function('_floor')) : {"dim_func": partial(ensure_unitless_in, func_name="floor"), "sympy_func": floor}, + cast(Function, Function('_round')) : {"dim_func": partial(ensure_unitless_in, func_name="round"), "sympy_func": custom_round}, cast(Function, Function('_Derivative')) : {"dim_func": custom_derivative_dims, "sympy_func": custom_derivative}, cast(Function, Function('_Integral')) : {"dim_func": custom_integral_dims, "sympy_func": custom_integral}, cast(Function, Function('_range')) : {"dim_func": custom_range_dims, "sympy_func": custom_range}, From b4a92d458ed5caa72d68949b29acc8a9e72ca810 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Sun, 12 Jan 2025 00:08:37 -0600 Subject: [PATCH 39/53] tests: update exponent round of test to match new dimension error message --- tests/test_basic.spec.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_basic.spec.mjs b/tests/test_basic.spec.mjs index c107c2a4..8fe52fb7 100644 --- a/tests/test_basic.spec.mjs +++ b/tests/test_basic.spec.mjs @@ -826,7 +826,7 @@ test('Test floating point exponent rounding', async () => { content = await page.textContent('#result-units-4'); expect(content).toBe('K'); - await expect(page.locator('#cell-5 >> text=Dimension Error: All input arguments to function need to have compatible units')).toBeVisible(); + await expect(page.locator('#cell-5 >> text=Dimension Error: sum function requires that all input values have the same units')).toBeVisible(); content = await page.textContent('#result-value-6'); expect(parseLatexFloat(content)).toBeCloseTo(6, precision); From 163a9437dfc4849473c817a24f92d3d7cdd0bfba Mon Sep 17 00:00:00 2001 From: mgreminger Date: Fri, 17 Jan 2025 09:40:18 -0600 Subject: [PATCH 40/53] fix: fix nested function regression Test added --- public/dimensional_analysis.py | 2 +- tests/test_basic.spec.mjs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 9a57c378..eccabcb8 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1601,7 +1601,7 @@ def replace_placeholder_funcs(expr: Expr, function_parents: list[Basic], data_table_subs: DataTableSubs | None) -> Expr: - if (not is_matrix(expr)) and expr.func == function_id_wrapper: + while (not is_matrix(expr)) and expr.func == function_id_wrapper: function_parents.append(expr.args[0]) expr = cast(Expr, expr.args[1]) diff --git a/tests/test_basic.spec.mjs b/tests/test_basic.spec.mjs index 8fe52fb7..4f40c935 100644 --- a/tests/test_basic.spec.mjs +++ b/tests/test_basic.spec.mjs @@ -1176,6 +1176,22 @@ test("Test complicated function evaluation", async () => { }); +test('Test nested function', async () => { + + await page.setLatex(0, String.raw`y=x+b`); + await page.locator('#add-math-cell').click(); + await page.setLatex(1, String.raw`s=y\left(x=1\left\lbrack m\right\rbrack\right)`); + await page.locator('#add-math-cell').click(); + await page.setLatex(2, String.raw`s\left(b=3\left\lbrack m\right\rbrack\right)=`); + + await page.waitForSelector('text=Updating...', {state: 'detached'}); + + let content = await page.textContent('#result-value-2'); + expect(parseLatexFloat(content)).toBeCloseTo(4); + content = await page.textContent('#result-units-2'); + expect(content).toBe('m'); + +}); test('Test unit exponent rounding', async () => { From 303f07b64428f33966e7ea8d95018a35aef3715f Mon Sep 17 00:00:00 2001 From: mgreminger Date: Fri, 17 Jan 2025 09:43:13 -0600 Subject: [PATCH 41/53] refactor: simplify function wrapper detection logic --- public/dimensional_analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index eccabcb8..c4815899 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1601,9 +1601,9 @@ def replace_placeholder_funcs(expr: Expr, function_parents: list[Basic], data_table_subs: DataTableSubs | None) -> Expr: - while (not is_matrix(expr)) and expr.func == function_id_wrapper: + if (not is_matrix(expr)) and expr.func == function_id_wrapper: function_parents.append(expr.args[0]) - expr = cast(Expr, expr.args[1]) + return replace_placeholder_funcs(cast(Expr, expr.args[1]), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) if (not is_matrix(expr)) and isinstance(expr, Symbol) and expr.name == "_zero_delayed_substitution": return S.Zero From 100844e8cd550cddbf6db757fe138715b6b0bbfd Mon Sep 17 00:00:00 2001 From: mgreminger Date: Fri, 17 Jan 2025 09:46:22 -0600 Subject: [PATCH 42/53] Revert "refactor: simplify function wrapper detection logic" This reverts commit 303f07b64428f33966e7ea8d95018a35aef3715f. --- public/dimensional_analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index c4815899..eccabcb8 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1601,9 +1601,9 @@ def replace_placeholder_funcs(expr: Expr, function_parents: list[Basic], data_table_subs: DataTableSubs | None) -> Expr: - if (not is_matrix(expr)) and expr.func == function_id_wrapper: + while (not is_matrix(expr)) and expr.func == function_id_wrapper: function_parents.append(expr.args[0]) - return replace_placeholder_funcs(cast(Expr, expr.args[1]), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) + expr = cast(Expr, expr.args[1]) if (not is_matrix(expr)) and isinstance(expr, Symbol) and expr.name == "_zero_delayed_substitution": return S.Zero From 6face460c5b17e08a831942104245cb17faf54fb Mon Sep 17 00:00:00 2001 From: mgreminger Date: Sat, 18 Jan 2025 15:15:12 -0600 Subject: [PATCH 43/53] feat: add dim values error message to inform user to report bug --- public/dimensional_analysis.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index eccabcb8..d1564a0b 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1643,7 +1643,9 @@ def replace_placeholder_funcs(expr: Expr, return result else: child_expr = expr.args[1] - dim_values = dim_values_dict[(expr.args[0],*function_parents)] + dim_values = dim_values_dict.get((expr.args[0],*function_parents), None) + if dim_values is None: + raise KeyError('Dim values lookup error, this is likely a bug, please report to support@engineeringpaper.xyz') child_processed_args = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) for arg in child_expr.args] return cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(dim_values, *child_processed_args)) elif expr.func in dummy_var_placeholder_set and func_key == "dim_func": From 2739b215d2e369af467fb56a4b37b43bc60a7467 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Sun, 19 Jan 2025 09:58:45 -0600 Subject: [PATCH 44/53] fix: fix regression with integrals that have exponents at the limits Only impacted integrals with user functions in the integrand --- public/dimensional_analysis.py | 23 +++++++++++------------ tests/test_calc.spec.mjs | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index d1564a0b..dcc9dd3e 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1616,7 +1616,7 @@ def replace_placeholder_funcs(expr: Expr, for j in range(expr.cols): row.append(replace_placeholder_funcs(cast(Expr, expr[i,j]), func_key, placeholder_map, placeholder_set, - dim_values_dict, function_parents, + dim_values_dict, function_parents.copy(), data_table_subs) ) return cast(Expr, Matrix(rows)) @@ -1629,38 +1629,37 @@ def replace_placeholder_funcs(expr: Expr, if expr.func == dim_needs_values_wrapper: if func_key == "sympy_func": child_expr = expr.args[1] - function_parents_snapshot = list(function_parents) - dim_args = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) for arg in child_expr.args] + dim_args = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in child_expr.args] result = cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(*dim_args)) if data_table_subs is not None and len(data_table_subs.subs_stack) > 0: dim_args_snapshot = list(dim_args) for i, value in enumerate(dim_args_snapshot): dim_args_snapshot[i] = cast(Expr, value.subs({key: cast(Matrix, value)[0,0] for key, value in data_table_subs.subs_stack[-1].items()})) result_snapshot = cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(*dim_args_snapshot)) - dim_values_dict[(expr.args[0], *function_parents_snapshot)] = DimValues(args=dim_args_snapshot, result=result_snapshot) + dim_values_dict[(expr.args[0], *function_parents)] = DimValues(args=dim_args_snapshot, result=result_snapshot) else: - dim_values_dict[(expr.args[0], *function_parents_snapshot)] = DimValues(args=dim_args, result=result) + dim_values_dict[(expr.args[0], *function_parents)] = DimValues(args=dim_args, result=result) return result else: child_expr = expr.args[1] dim_values = dim_values_dict.get((expr.args[0],*function_parents), None) if dim_values is None: raise KeyError('Dim values lookup error, this is likely a bug, please report to support@engineeringpaper.xyz') - child_processed_args = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) for arg in child_expr.args] + child_processed_args = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in child_expr.args] return cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(dim_values, *child_processed_args)) elif expr.func in dummy_var_placeholder_set and func_key == "dim_func": - return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) if index > 0 else arg for index, arg in enumerate(expr.args)))) + return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) if index > 0 else arg for index, arg in enumerate(expr.args)))) elif expr.func in placeholder_set: - return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) for arg in expr.args))) + return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in expr.args))) elif data_table_subs is not None and expr.func == data_table_calc_wrapper: if len(expr.args[0].atoms(data_table_id_wrapper)) == 0: - return replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) + return replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) data_table_subs.subs_stack.append({}) data_table_subs.shortest_col_stack.append(None) - sub_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) + sub_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) subs = data_table_subs.subs_stack.pop() shortest_col = data_table_subs.shortest_col_stack.pop() @@ -1681,7 +1680,7 @@ def replace_placeholder_funcs(expr: Expr, return cast(Expr, Matrix([sub_expr,]*shortest_col)) elif data_table_subs is not None and expr.func == data_table_id_wrapper: - current_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) + current_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) new_var = Symbol(f"_data_table_var_{data_table_subs.get_next_id()}") if not is_matrix(current_expr): @@ -1699,7 +1698,7 @@ def replace_placeholder_funcs(expr: Expr, return cast(Expr, current_expr[0,0]) else: - return cast(Expr, expr.func(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents, data_table_subs) for arg in expr.args))) + return cast(Expr, expr.func(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in expr.args))) def get_dimensional_analysis_expression(parameter_subs: dict[Symbol, Expr], expression: Expr, diff --git a/tests/test_calc.spec.mjs b/tests/test_calc.spec.mjs index be2dde12..5845a4b6 100644 --- a/tests/test_calc.spec.mjs +++ b/tests/test_calc.spec.mjs @@ -442,3 +442,17 @@ test('Test unitless nested integral in exponent', async () => { expect(content).toBe(''); }); +test('Test integral with exponent in upper limit and user function in integrand', async () => { + await page.setLatex(0, String.raw`s=t,\:n=3`); + + await page.locator('#add-math-cell').click(); + await page.setLatex(1, String.raw`\int_0^{n^2}\left(s\left(t=1\right)\right)\mathrm{d}\left(t\right)=`); + + await page.waitForSelector('.status-footer', {state: 'detached'}); + + let content = await page.textContent('#result-value-1'); + expect(parseLatexFloat(content)).toBeCloseTo(9, precision); + content = await page.textContent('#result-units-1'); + expect(content).toBe(''); +}); + From 99ef5b723506991db1988b609661bbde77accc34 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Sun, 19 Jan 2025 15:34:46 -0600 Subject: [PATCH 45/53] refactor: combine subs phase with replace placeholder phase --- public/dimensional_analysis.py | 47 ++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index dcc9dd3e..6c8dc34f 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1593,20 +1593,27 @@ def replace_sympy_funcs_with_placeholder_funcs(expression: Expr) -> Expr: return expression -def replace_placeholder_funcs(expr: Expr, +def replace_placeholder_funcs(expr: Expr, + parameter_subs: dict[Symbol, Expr], func_key: Literal["dim_func"] | Literal["sympy_func"], placeholder_map: dict[Function, PlaceholderFunction], placeholder_set: set[Function], dim_values_dict: dict[tuple[Basic,...], DimValues], function_parents: list[Basic], data_table_subs: DataTableSubs | None) -> Expr: - + while (not is_matrix(expr)) and expr.func == function_id_wrapper: function_parents.append(expr.args[0]) expr = cast(Expr, expr.args[1]) - if (not is_matrix(expr)) and isinstance(expr, Symbol) and expr.name == "_zero_delayed_substitution": - return S.Zero + if (not is_matrix(expr)) and isinstance(expr, Symbol): + if expr.name == "_zero_delayed_substitution": + return S.Zero + elif expr in parameter_subs: + sub = parameter_subs[expr] + if isinstance(sub, Symbol) and sub.name == "_zero_delayed_substitution": + sub = S.Zero + return sub if is_matrix(expr): rows = [] @@ -1614,7 +1621,7 @@ def replace_placeholder_funcs(expr: Expr, row = [] rows.append(row) for j in range(expr.cols): - row.append(replace_placeholder_funcs(cast(Expr, expr[i,j]), func_key, + row.append(replace_placeholder_funcs(cast(Expr, expr[i,j]), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) ) @@ -1629,7 +1636,7 @@ def replace_placeholder_funcs(expr: Expr, if expr.func == dim_needs_values_wrapper: if func_key == "sympy_func": child_expr = expr.args[1] - dim_args = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in child_expr.args] + dim_args = [replace_placeholder_funcs(cast(Expr, arg), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in child_expr.args] result = cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(*dim_args)) if data_table_subs is not None and len(data_table_subs.subs_stack) > 0: dim_args_snapshot = list(dim_args) @@ -1645,21 +1652,21 @@ def replace_placeholder_funcs(expr: Expr, dim_values = dim_values_dict.get((expr.args[0],*function_parents), None) if dim_values is None: raise KeyError('Dim values lookup error, this is likely a bug, please report to support@engineeringpaper.xyz') - child_processed_args = [replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in child_expr.args] + child_processed_args = [replace_placeholder_funcs(cast(Expr, arg), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in child_expr.args] return cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(dim_values, *child_processed_args)) elif expr.func in dummy_var_placeholder_set and func_key == "dim_func": - return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) if index > 0 else arg for index, arg in enumerate(expr.args)))) + return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) if index > 0 else arg for index, arg in enumerate(expr.args)))) elif expr.func in placeholder_set: - return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in expr.args))) + return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in expr.args))) elif data_table_subs is not None and expr.func == data_table_calc_wrapper: if len(expr.args[0].atoms(data_table_id_wrapper)) == 0: - return replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) + return replace_placeholder_funcs(cast(Expr, expr.args[0]), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) data_table_subs.subs_stack.append({}) data_table_subs.shortest_col_stack.append(None) - sub_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) + sub_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) subs = data_table_subs.subs_stack.pop() shortest_col = data_table_subs.shortest_col_stack.pop() @@ -1680,7 +1687,7 @@ def replace_placeholder_funcs(expr: Expr, return cast(Expr, Matrix([sub_expr,]*shortest_col)) elif data_table_subs is not None and expr.func == data_table_id_wrapper: - current_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) + current_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) new_var = Symbol(f"_data_table_var_{data_table_subs.get_next_id()}") if not is_matrix(current_expr): @@ -1698,7 +1705,7 @@ def replace_placeholder_funcs(expr: Expr, return cast(Expr, current_expr[0,0]) else: - return cast(Expr, expr.func(*(replace_placeholder_funcs(cast(Expr, arg), func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in expr.args))) + return cast(Expr, expr.func(*(replace_placeholder_funcs(cast(Expr, arg), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in expr.args))) def get_dimensional_analysis_expression(parameter_subs: dict[Symbol, Expr], expression: Expr, @@ -1706,13 +1713,11 @@ def get_dimensional_analysis_expression(parameter_subs: dict[Symbol, Expr], placeholder_set: set[Function], dim_values_dict: dict[tuple[Basic,...], DimValues]) -> tuple[Expr | None, Exception | None]: - expression_with_parameter_subs = cast(Expr, expression.xreplace(parameter_subs)) - error = None final_expression = None try: - final_expression = replace_placeholder_funcs(expression_with_parameter_subs, + final_expression = replace_placeholder_funcs(expression, parameter_subs, "dim_func", placeholder_map, placeholder_set, dim_values_dict, [], DataTableSubs()) except Exception as e: @@ -1901,7 +1906,7 @@ def remove_implicit(input_set: set[str]) -> set[str]: def solve_system(statements: list[EqualityStatement], variables: list[str], - placeholder_map: dict[Function, PlaceholderFunction], + placeholder_map: dict[Function, PlaceholderFunction], placeholder_set: set[Function], convert_floats_to_fractions: bool): parameters = get_all_implicit_parameters(statements) parameter_subs = get_parameter_subs(parameters, convert_floats_to_fractions) @@ -1921,7 +1926,7 @@ def solve_system(statements: list[EqualityStatement], variables: list[str], system_variables.update(statement["params"]) system_implicit_params.extend(statement["implicitParams"]) - equality = replace_placeholder_funcs(cast(Expr, statement["expression"]), + equality = replace_placeholder_funcs(cast(Expr, statement["expression"]), {}, "sympy_func", placeholder_map, placeholder_set, {}, [], None) @@ -2004,8 +2009,7 @@ def solve_system_numerical(statements: list[EqualityStatement], variables: list[ for statement in statements: system_variables.update(statement["params"]) - equality = cast(Expr, statement["expression"]).subs(parameter_subs) - equality = replace_placeholder_funcs(cast(Expr, equality), + equality = replace_placeholder_funcs(cast(Expr, statement["expression"]), parameter_subs, "sympy_func", placeholder_map, placeholder_set, {}, [], None) system.append(cast(Expr, equality.doit())) @@ -2439,9 +2443,8 @@ def get_evaluated_expression(expression: Expr, simplify_symbolic_expressions: bool, placeholder_map: dict[Function, PlaceholderFunction], placeholder_set: set[Function]) -> tuple[ExprWithAssumptions, str | list[list[str]], dict[tuple[Basic,...],DimValues]]: - expression = cast(Expr, expression.xreplace(parameter_subs)) dim_values_dict: dict[tuple[Basic,...], DimValues] = {} - expression = replace_placeholder_funcs(expression, + expression = replace_placeholder_funcs(expression, parameter_subs, "sympy_func", placeholder_map, placeholder_set, dim_values_dict, [], From 8f3b9dfc2e7b1b51d38c8db9695537e0e769488c Mon Sep 17 00:00:00 2001 From: mgreminger Date: Sun, 19 Jan 2025 17:02:23 -0600 Subject: [PATCH 46/53] refactor: remove unnecessary _zero_delayed_substitution --- public/dimensional_analysis.py | 12 +++--------- src/parser/LatexToSympy.ts | 7 ------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 6c8dc34f..f8b0f188 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1606,14 +1606,8 @@ def replace_placeholder_funcs(expr: Expr, function_parents.append(expr.args[0]) expr = cast(Expr, expr.args[1]) - if (not is_matrix(expr)) and isinstance(expr, Symbol): - if expr.name == "_zero_delayed_substitution": - return S.Zero - elif expr in parameter_subs: - sub = parameter_subs[expr] - if isinstance(sub, Symbol) and sub.name == "_zero_delayed_substitution": - sub = S.Zero - return sub + if (not is_matrix(expr)) and isinstance(expr, Symbol) and expr in parameter_subs: + return parameter_subs[expr] if is_matrix(expr): rows = [] @@ -1828,7 +1822,7 @@ def get_sorted_statements(statements: list[Statement], custom_definition_names: zero_place_holder: ImplicitParameter = { "dimensions": [0]*9, "original_value": "0", - "si_value": "_zero_delayed_substitution", + "si_value": "0", "name": ZERO_PLACEHOLDER, "units": "" } diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index 657bea56..f3cc1a18 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -385,9 +385,6 @@ export class LatexToSympy extends LatexParserVisitor Date: Sun, 19 Jan 2025 17:11:53 -0600 Subject: [PATCH 47/53] refactor: simplify branching in replace_placeholder_funcs --- public/dimensional_analysis.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index f8b0f188..f9ea18f7 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1606,9 +1606,6 @@ def replace_placeholder_funcs(expr: Expr, function_parents.append(expr.args[0]) expr = cast(Expr, expr.args[1]) - if (not is_matrix(expr)) and isinstance(expr, Symbol) and expr in parameter_subs: - return parameter_subs[expr] - if is_matrix(expr): rows = [] for i in range(expr.rows): @@ -1619,9 +1616,12 @@ def replace_placeholder_funcs(expr: Expr, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) ) - + return cast(Expr, Matrix(rows)) + elif isinstance(expr, Symbol) and expr in parameter_subs: + return parameter_subs[expr] + expr = cast(Expr,expr) if len(expr.args) == 0: From 3e7c96416c64ef59ad1c8cb78261d23e9970a578 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Mon, 20 Jan 2025 16:11:56 -0600 Subject: [PATCH 48/53] refactor: switch to a single pass for evaluations and dimensions Provides a significant speedup and eliminates any possibility of mismatch between passes for dim functions that need evaluation pass values --- public/dimensional_analysis.py | 285 ++++++++++++++++----------------- src/parser/LatexToSympy.ts | 16 +- 2 files changed, 147 insertions(+), 154 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index f9ea18f7..6c67cf25 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -1167,6 +1167,7 @@ def custom_range_dims(dim_values: DimValues, *args: Expr): class PlaceholderFunction(TypedDict): dim_func: Callable | Function sympy_func: object + dims_need_values: bool def UniversalInverse(expression: Expr) -> Expr: return expression**-1 @@ -1399,7 +1400,7 @@ def HA_fluid_dims(fluid_function: FluidFunction, input1, input2, input3): return get_dims(fluid_function["outputDims"]) def get_fluid_placeholder_map(fluid_functions: list[FluidFunction]) -> dict[Function, PlaceholderFunction]: - new_map = {} + new_map: dict[Function, PlaceholderFunction] = {} for fluid_function in fluid_functions: if fluid_function["fluid"] == "HumidAir": @@ -1416,7 +1417,8 @@ def get_fluid_placeholder_map(fluid_functions: list[FluidFunction]) -> dict[Func dim_func = partial(lambda ff, input1, input2 : fluid_dims(ff, input1, input2), fluid_function) new_map[Function(fluid_function["name"])] = {"dim_func": dim_func, - "sympy_func": sympy_func} + "sympy_func": sympy_func, + "dims_need_values": False} return new_map @@ -1500,7 +1502,7 @@ def polyfit_dims_wrapper(input): return polyfit_wrapper, polyfit_dims_wrapper def get_interpolation_placeholder_map(interpolation_functions: list[InterpolationFunction]) -> dict[Function, PlaceholderFunction]: - new_map = {} + new_map: dict[Function, PlaceholderFunction] = {} for interpolation_function in interpolation_functions: match interpolation_function["type"]: @@ -1512,7 +1514,8 @@ def get_interpolation_placeholder_map(interpolation_functions: list[Interpolatio continue new_map[Function(interpolation_function["name"])] = {"dim_func": dim_func, - "sympy_func": sympy_func} + "sympy_func": sympy_func, + "dims_need_values": False} return new_map @@ -1529,53 +1532,51 @@ def __init__(self): def get_next_id(self): self._next_id += 1 return self._next_id-1 - -dim_needs_values_wrapper = Function('_dim_needs_values_wrapper') -function_id_wrapper = Function('_function_id_wrapper') + global_placeholder_map: dict[Function, PlaceholderFunction] = { - cast(Function, Function('_StrictLessThan')) : {"dim_func": partial(ensure_dims_all_compatible, error_message="Piecewise cell comparison dimensions must match"), "sympy_func": StrictLessThan}, - cast(Function, Function('_LessThan')) : {"dim_func": partial(ensure_dims_all_compatible, error_message="Piecewise cell comparison dimensions must match"), "sympy_func": LessThan}, - cast(Function, Function('_StrictGreaterThan')) : {"dim_func": partial(ensure_dims_all_compatible, error_message="Piecewise cell comparison dimensions must match"), "sympy_func": StrictGreaterThan}, - cast(Function, Function('_GreaterThan')) : {"dim_func": partial(ensure_dims_all_compatible, error_message="Piecewise cell comparison dimensions must match"), "sympy_func": GreaterThan}, - cast(Function, Function('_And')) : {"dim_func": partial(ensure_dims_all_compatible, error_message="Piecewise cell comparison dimensions must match"), "sympy_func": And}, - cast(Function, Function('_Piecewise')) : {"dim_func": ensure_dims_all_compatible_piecewise, "sympy_func": Piecewise}, - cast(Function, Function('_asin')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arcsin"), "sympy_func": asin}, - cast(Function, Function('_acos')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arccos"), "sympy_func": acos}, - cast(Function, Function('_atan')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arctan"), "sympy_func": atan}, - cast(Function, Function('_asec')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arcsec"), "sympy_func": asec}, - cast(Function, Function('_acsc')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arcscs"), "sympy_func": acsc}, - cast(Function, Function('_acot')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arccot"), "sympy_func": acot}, - cast(Function, Function('_arg')) : {"dim_func": ensure_any_unit_in_angle_out, "sympy_func": arg}, - cast(Function, Function('_re')) : {"dim_func": ensure_any_unit_in_same_out, "sympy_func": re}, - cast(Function, Function('_im')) : {"dim_func": ensure_any_unit_in_same_out, "sympy_func": im}, - cast(Function, Function('_conjugate')) : {"dim_func": ensure_any_unit_in_same_out, "sympy_func": conjugate}, - cast(Function, Function('_Max')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="max"), "sympy_func": custom_max}, - cast(Function, Function('_Min')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="min"), "sympy_func": custom_min}, - cast(Function, Function('_sum')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="sum"), "sympy_func": custom_sum}, - cast(Function, Function('_average')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="average"), "sympy_func": custom_average}, - cast(Function, Function('_stdev')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="stdev"), "sympy_func": partial(custom_stdev, False)}, - cast(Function, Function('_stdevp')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="stdevp"), "sympy_func": partial(custom_stdev, True)}, - cast(Function, Function('_count')) : {"dim_func": custom_count, "sympy_func": custom_count}, - cast(Function, Function('_Abs')) : {"dim_func": ensure_any_unit_in_same_out, "sympy_func": Abs}, - cast(Function, Function('_Inverse')) : {"dim_func": ensure_inverse_dims, "sympy_func": UniversalInverse}, - cast(Function, Function('_Transpose')) : {"dim_func": custom_transpose, "sympy_func": custom_transpose}, - cast(Function, Function('_Determinant')) : {"dim_func": custom_determinant, "sympy_func": custom_determinant}, - cast(Function, Function('_mat_multiply')) : {"dim_func": partial(custom_multiply_dims, True), "sympy_func": custom_matmul}, - cast(Function, Function('_multiply')) : {"dim_func": partial(custom_multiply_dims, False), "sympy_func": Mul}, - cast(Function, Function('_IndexMatrix')) : {"dim_func": IndexMatrix_dims, "sympy_func": IndexMatrix}, - cast(Function, Function('_Eq')) : {"dim_func": Eq, "sympy_func": Eq}, - cast(Function, Function('_norm')) : {"dim_func": custom_norm, "sympy_func": custom_norm}, - cast(Function, Function('_dot')) : {"dim_func": custom_dot, "sympy_func": custom_dot}, - cast(Function, Function('_ceil')) : {"dim_func": partial(ensure_unitless_in, func_name="ceil"), "sympy_func": ceiling}, - cast(Function, Function('_floor')) : {"dim_func": partial(ensure_unitless_in, func_name="floor"), "sympy_func": floor}, - cast(Function, Function('_round')) : {"dim_func": partial(ensure_unitless_in, func_name="round"), "sympy_func": custom_round}, - cast(Function, Function('_Derivative')) : {"dim_func": custom_derivative_dims, "sympy_func": custom_derivative}, - cast(Function, Function('_Integral')) : {"dim_func": custom_integral_dims, "sympy_func": custom_integral}, - cast(Function, Function('_range')) : {"dim_func": custom_range_dims, "sympy_func": custom_range}, - cast(Function, Function('_factorial')) : {"dim_func": factorial, "sympy_func": CustomFactorial}, - cast(Function, Function('_add')) : {"dim_func": custom_add_dims, "sympy_func": Add}, - cast(Function, Function('_Pow')) : {"dim_func": custom_pow_dims, "sympy_func": custom_pow}, + cast(Function, Function('_StrictLessThan')) : {"dim_func": partial(ensure_dims_all_compatible, error_message="Piecewise cell comparison dimensions must match"), "sympy_func": StrictLessThan, "dims_need_values": False}, + cast(Function, Function('_LessThan')) : {"dim_func": partial(ensure_dims_all_compatible, error_message="Piecewise cell comparison dimensions must match"), "sympy_func": LessThan, "dims_need_values": False}, + cast(Function, Function('_StrictGreaterThan')) : {"dim_func": partial(ensure_dims_all_compatible, error_message="Piecewise cell comparison dimensions must match"), "sympy_func": StrictGreaterThan, "dims_need_values": False}, + cast(Function, Function('_GreaterThan')) : {"dim_func": partial(ensure_dims_all_compatible, error_message="Piecewise cell comparison dimensions must match"), "sympy_func": GreaterThan, "dims_need_values": False}, + cast(Function, Function('_And')) : {"dim_func": partial(ensure_dims_all_compatible, error_message="Piecewise cell comparison dimensions must match"), "sympy_func": And, "dims_need_values": False}, + cast(Function, Function('_Piecewise')) : {"dim_func": ensure_dims_all_compatible_piecewise, "sympy_func": Piecewise, "dims_need_values": False}, + cast(Function, Function('_asin')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arcsin"), "sympy_func": asin, "dims_need_values": False}, + cast(Function, Function('_acos')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arccos"), "sympy_func": acos, "dims_need_values": False}, + cast(Function, Function('_atan')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arctan"), "sympy_func": atan, "dims_need_values": False}, + cast(Function, Function('_asec')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arcsec"), "sympy_func": asec, "dims_need_values": False}, + cast(Function, Function('_acsc')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arcscs"), "sympy_func": acsc, "dims_need_values": False}, + cast(Function, Function('_acot')) : {"dim_func": partial(ensure_unitless_in_angle_out, func_name="arccot"), "sympy_func": acot, "dims_need_values": False}, + cast(Function, Function('_arg')) : {"dim_func": ensure_any_unit_in_angle_out, "sympy_func": arg, "dims_need_values": False}, + cast(Function, Function('_re')) : {"dim_func": ensure_any_unit_in_same_out, "sympy_func": re, "dims_need_values": False}, + cast(Function, Function('_im')) : {"dim_func": ensure_any_unit_in_same_out, "sympy_func": im, "dims_need_values": False}, + cast(Function, Function('_conjugate')) : {"dim_func": ensure_any_unit_in_same_out, "sympy_func": conjugate, "dims_need_values": False}, + cast(Function, Function('_Max')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="max"), "sympy_func": custom_max, "dims_need_values": False}, + cast(Function, Function('_Min')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="min"), "sympy_func": custom_min, "dims_need_values": False}, + cast(Function, Function('_sum')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="sum"), "sympy_func": custom_sum, "dims_need_values": False}, + cast(Function, Function('_average')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="average"), "sympy_func": custom_average, "dims_need_values": False}, + cast(Function, Function('_stdev')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="stdev"), "sympy_func": partial(custom_stdev, False), "dims_need_values": False}, + cast(Function, Function('_stdevp')) : {"dim_func": partial(ensure_dims_all_compatible_scalar_or_matrix, func_name="stdevp"), "sympy_func": partial(custom_stdev, True), "dims_need_values": False}, + cast(Function, Function('_count')) : {"dim_func": custom_count, "sympy_func": custom_count, "dims_need_values": False}, + cast(Function, Function('_Abs')) : {"dim_func": ensure_any_unit_in_same_out, "sympy_func": Abs, "dims_need_values": False}, + cast(Function, Function('_Inverse')) : {"dim_func": ensure_inverse_dims, "sympy_func": UniversalInverse, "dims_need_values": False}, + cast(Function, Function('_Transpose')) : {"dim_func": custom_transpose, "sympy_func": custom_transpose, "dims_need_values": False}, + cast(Function, Function('_Determinant')) : {"dim_func": custom_determinant, "sympy_func": custom_determinant, "dims_need_values": False}, + cast(Function, Function('_mat_multiply')) : {"dim_func": partial(custom_multiply_dims, True), "sympy_func": custom_matmul, "dims_need_values": False}, + cast(Function, Function('_multiply')) : {"dim_func": partial(custom_multiply_dims, False), "sympy_func": Mul, "dims_need_values": False}, + cast(Function, Function('_IndexMatrix')) : {"dim_func": IndexMatrix_dims, "sympy_func": IndexMatrix, "dims_need_values": True}, + cast(Function, Function('_Eq')) : {"dim_func": Eq, "sympy_func": Eq, "dims_need_values": False}, + cast(Function, Function('_norm')) : {"dim_func": custom_norm, "sympy_func": custom_norm, "dims_need_values": False}, + cast(Function, Function('_dot')) : {"dim_func": custom_dot, "sympy_func": custom_dot, "dims_need_values": False}, + cast(Function, Function('_ceil')) : {"dim_func": partial(ensure_unitless_in, func_name="ceil"), "sympy_func": ceiling, "dims_need_values": False}, + cast(Function, Function('_floor')) : {"dim_func": partial(ensure_unitless_in, func_name="floor"), "sympy_func": floor, "dims_need_values": False}, + cast(Function, Function('_round')) : {"dim_func": partial(ensure_unitless_in, func_name="round"), "sympy_func": custom_round, "dims_need_values": False}, + cast(Function, Function('_Derivative')) : {"dim_func": custom_derivative_dims, "sympy_func": custom_derivative, "dims_need_values": False}, + cast(Function, Function('_Integral')) : {"dim_func": custom_integral_dims, "sympy_func": custom_integral, "dims_need_values": False}, + cast(Function, Function('_range')) : {"dim_func": custom_range_dims, "sympy_func": custom_range, "dims_need_values": True}, + cast(Function, Function('_factorial')) : {"dim_func": factorial, "sympy_func": CustomFactorial, "dims_need_values": False}, + cast(Function, Function('_add')) : {"dim_func": custom_add_dims, "sympy_func": Add, "dims_need_values": False}, + cast(Function, Function('_Pow')) : {"dim_func": custom_pow_dims, "sympy_func": custom_pow, "dims_need_values": True}, } global_placeholder_set = set(global_placeholder_map.keys()) @@ -1593,74 +1594,81 @@ def replace_sympy_funcs_with_placeholder_funcs(expression: Expr) -> Expr: return expression -def replace_placeholder_funcs(expr: Expr, +def replace_placeholder_funcs(expr: Expr, error: Exception | None, needs_dims: bool, parameter_subs: dict[Symbol, Expr], - func_key: Literal["dim_func"] | Literal["sympy_func"], + parameter_dim_subs: dict[Symbol, Expr], placeholder_map: dict[Function, PlaceholderFunction], placeholder_set: set[Function], - dim_values_dict: dict[tuple[Basic,...], DimValues], - function_parents: list[Basic], - data_table_subs: DataTableSubs | None) -> Expr: - - while (not is_matrix(expr)) and expr.func == function_id_wrapper: - function_parents.append(expr.args[0]) - expr = cast(Expr, expr.args[1]) + data_table_subs: DataTableSubs | None) -> tuple[Expr, Expr | None, Exception | None]: if is_matrix(expr): rows = [] + dim_rows = [] for i in range(expr.rows): row = [] rows.append(row) + dim_row = [] + dim_rows.append(dim_row) for j in range(expr.cols): - row.append(replace_placeholder_funcs(cast(Expr, expr[i,j]), parameter_subs, func_key, - placeholder_map, placeholder_set, - dim_values_dict, function_parents.copy(), - data_table_subs) ) + value, dim_value, error = replace_placeholder_funcs(cast(Expr, expr[i,j]), error, needs_dims, parameter_subs, + parameter_dim_subs, placeholder_map, placeholder_set, + data_table_subs) + row.append(value) + dim_row.append(dim_value) - return cast(Expr, Matrix(rows)) + return ( cast(Expr, Matrix(rows)), cast(Expr, Matrix(dim_rows)) if needs_dims and not error else None, error ) elif isinstance(expr, Symbol) and expr in parameter_subs: - return parameter_subs[expr] + return ( parameter_subs[expr], parameter_dim_subs[expr] if needs_dims and not error else None, error ) expr = cast(Expr,expr) if len(expr.args) == 0: - return expr - - if expr.func == dim_needs_values_wrapper: - if func_key == "sympy_func": - child_expr = expr.args[1] - dim_args = [replace_placeholder_funcs(cast(Expr, arg), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in child_expr.args] - result = cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(*dim_args)) - if data_table_subs is not None and len(data_table_subs.subs_stack) > 0: - dim_args_snapshot = list(dim_args) - for i, value in enumerate(dim_args_snapshot): - dim_args_snapshot[i] = cast(Expr, value.subs({key: cast(Matrix, value)[0,0] for key, value in data_table_subs.subs_stack[-1].items()})) - result_snapshot = cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(*dim_args_snapshot)) - dim_values_dict[(expr.args[0], *function_parents)] = DimValues(args=dim_args_snapshot, result=result_snapshot) - else: - dim_values_dict[(expr.args[0], *function_parents)] = DimValues(args=dim_args, result=result) - return result + return ( expr, expr if needs_dims and not error else None, error ) + + + if expr.func in placeholder_set: + skip_first_for_dims = False + if expr.func in dummy_var_placeholder_set: + skip_first_for_dims = True + + processed_args = [] + for index, arg in enumerate(expr.args): + processed_args.append(replace_placeholder_funcs(cast(Expr, arg), error, False if (skip_first_for_dims and index == 0) else needs_dims, parameter_subs, parameter_dim_subs, placeholder_map, placeholder_set, data_table_subs)) + error = processed_args[-1][2] + + result = cast(Expr, cast(Callable, placeholder_map[expr.func]["sympy_func"])(*(arg[0] for arg in processed_args))) + + if needs_dims and not error: + try: + if placeholder_map[expr.func]["dims_need_values"]: + dim_args = [arg[0] for arg in processed_args] + + if data_table_subs is not None and len(data_table_subs.subs_stack) > 0: + for i, value in enumerate(dim_args): + dim_args[i] = cast(Expr, value.subs({key: cast(Matrix, value)[0,0] for key, value in data_table_subs.subs_stack[-1].items()})) + result_snapshot = cast(Expr, cast(Callable, placeholder_map[expr.func]["sympy_func"])(*dim_args)) + dim_result = cast(Expr, cast(Callable, placeholder_map[expr.func]["dim_func"])(DimValues(args=dim_args, result=result_snapshot), *(arg[1] for arg in processed_args))) + else: + dim_result = cast(Expr, cast(Callable, placeholder_map[expr.func]["dim_func"])(DimValues(args=dim_args, result=result), *(arg[1] for arg in processed_args))) + else: + dim_result = cast(Expr, cast(Callable, placeholder_map[expr.func]["dim_func"])(*(arg[1] for arg in processed_args))) + except Exception as e: + error = e + dim_result = None else: - child_expr = expr.args[1] - dim_values = dim_values_dict.get((expr.args[0],*function_parents), None) - if dim_values is None: - raise KeyError('Dim values lookup error, this is likely a bug, please report to support@engineeringpaper.xyz') - child_processed_args = [replace_placeholder_funcs(cast(Expr, arg), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in child_expr.args] - return cast(Expr, cast(Callable, placeholder_map[cast(Function, child_expr.func)][func_key])(dim_values, *child_processed_args)) - elif expr.func in dummy_var_placeholder_set and func_key == "dim_func": - return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) if index > 0 else arg for index, arg in enumerate(expr.args)))) - elif expr.func in placeholder_set: - return cast(Expr, cast(Callable, placeholder_map[expr.func][func_key])(*(replace_placeholder_funcs(cast(Expr, arg), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in expr.args))) + dim_result = None + + return (result, dim_result, error) elif data_table_subs is not None and expr.func == data_table_calc_wrapper: if len(expr.args[0].atoms(data_table_id_wrapper)) == 0: - return replace_placeholder_funcs(cast(Expr, expr.args[0]), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) + return replace_placeholder_funcs(cast(Expr, expr.args[0]), error, needs_dims, parameter_subs, parameter_dim_subs, placeholder_map, placeholder_set, data_table_subs) data_table_subs.subs_stack.append({}) data_table_subs.shortest_col_stack.append(None) - sub_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) + sub_expr, dim_sub_expr, error = replace_placeholder_funcs(cast(Expr, expr.args[0]), error, needs_dims, parameter_subs, parameter_dim_subs, placeholder_map, placeholder_set, data_table_subs) subs = data_table_subs.subs_stack.pop() shortest_col = data_table_subs.shortest_col_stack.pop() @@ -1668,23 +1676,20 @@ def replace_placeholder_funcs(expr: Expr, if shortest_col is None: raise ValueError('Shortest column undefined for data table calculation') - if func_key == "sympy_func": - new_func = lambdify(subs.keys(), sub_expr, - modules=["math", "mpmath", "sympy"]) + new_func = lambdify(subs.keys(), sub_expr, + modules=["math", "mpmath", "sympy"]) - result = [] - for i in range(shortest_col): - result.append([new_func(*[float(cast(Expr, cast(Matrix, value)[i,0])) for value in subs.values()]), ]) + result = [] + for i in range(shortest_col): + result.append([new_func(*[float(cast(Expr, cast(Matrix, value)[i,0])) for value in subs.values()]), ]) - return cast(Expr, Matrix(result)) - else: - return cast(Expr, Matrix([sub_expr,]*shortest_col)) + return ( cast(Expr, Matrix(result)), cast(Expr, Matrix([dim_sub_expr,]*shortest_col)) if needs_dims and not error else None, error ) elif data_table_subs is not None and expr.func == data_table_id_wrapper: - current_expr = replace_placeholder_funcs(cast(Expr, expr.args[0]), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) + current_expr, dim_current_expr, error = replace_placeholder_funcs(cast(Expr, expr.args[0]), error, needs_dims, parameter_subs, parameter_dim_subs, placeholder_map, placeholder_set, data_table_subs) new_var = Symbol(f"_data_table_var_{data_table_subs.get_next_id()}") - if not is_matrix(current_expr): + if not is_matrix(current_expr) or (dim_current_expr is not None and not is_matrix(dim_current_expr)): raise EmptyColumnData(current_expr) if len(data_table_subs.subs_stack) > 0: @@ -1693,31 +1698,25 @@ def replace_placeholder_funcs(expr: Expr, if data_table_subs.shortest_col_stack[-1] is None or current_expr.rows < data_table_subs.shortest_col_stack[-1]: data_table_subs.shortest_col_stack[-1] = current_expr.rows - if func_key == "sympy_func": - return new_var - else: - return cast(Expr, current_expr[0,0]) + return ( new_var, cast(Expr, dim_current_expr[0,0]) if (needs_dims and dim_current_expr is not None) else None, error ) else: - return cast(Expr, expr.func(*(replace_placeholder_funcs(cast(Expr, arg), parameter_subs, func_key, placeholder_map, placeholder_set, dim_values_dict, function_parents.copy(), data_table_subs) for arg in expr.args))) - -def get_dimensional_analysis_expression(parameter_subs: dict[Symbol, Expr], - expression: Expr, - placeholder_map: dict[Function, PlaceholderFunction], - placeholder_set: set[Function], - dim_values_dict: dict[tuple[Basic,...], DimValues]) -> tuple[Expr | None, Exception | None]: + processed_args = [] - error = None - final_expression = None + for arg in expr.args: + processed_args.append(replace_placeholder_funcs(cast(Expr, arg), error, needs_dims, parameter_subs, parameter_dim_subs, placeholder_map, placeholder_set, data_table_subs)) + error = processed_args[-1][2] - try: - final_expression = replace_placeholder_funcs(expression, parameter_subs, - "dim_func", placeholder_map, placeholder_set, - dim_values_dict, [], DataTableSubs()) - except Exception as e: - error = e - - return final_expression, error + result = cast(Expr, expr.func(*(arg[0] for arg in processed_args))) + if needs_dims and not error: + try: + dim_result = cast(Expr, expr.func(*(arg[1] for arg in processed_args))) + except Exception as e: + error = e + dim_result = None + else: + dim_result = None + return ( result, dim_result, error ) def custom_get_dimensional_dependencies(expression: Expr | None): @@ -1920,9 +1919,8 @@ def solve_system(statements: list[EqualityStatement], variables: list[str], system_variables.update(statement["params"]) system_implicit_params.extend(statement["implicitParams"]) - equality = replace_placeholder_funcs(cast(Expr, statement["expression"]), {}, - "sympy_func", - placeholder_map, placeholder_set, {}, [], None) + equality, _, _ = replace_placeholder_funcs(cast(Expr, statement["expression"]), None, False, {}, {}, + placeholder_map, placeholder_set, None) system.append(cast(Expr, equality.doit())) @@ -2003,9 +2001,8 @@ def solve_system_numerical(statements: list[EqualityStatement], variables: list[ for statement in statements: system_variables.update(statement["params"]) - equality = replace_placeholder_funcs(cast(Expr, statement["expression"]), parameter_subs, - "sympy_func", - placeholder_map, placeholder_set, {}, [], None) + equality, _, _ = replace_placeholder_funcs(cast(Expr, statement["expression"]), None, False, parameter_subs, {}, + placeholder_map, placeholder_set, None) system.append(cast(Expr, equality.doit())) new_statements.extend(statement["equalityUnitsQueries"]) @@ -2434,14 +2431,14 @@ def subs_wrapper(expression: Expr, subs: dict[str, str] | dict[str, Expr | float def get_evaluated_expression(expression: Expr, parameter_subs: dict[Symbol, Expr], + dim_subs: dict[Symbol, Expr], simplify_symbolic_expressions: bool, placeholder_map: dict[Function, PlaceholderFunction], - placeholder_set: set[Function]) -> tuple[ExprWithAssumptions, str | list[list[str]], dict[tuple[Basic,...],DimValues]]: - dim_values_dict: dict[tuple[Basic,...], DimValues] = {} - expression = replace_placeholder_funcs(expression, parameter_subs, - "sympy_func", + placeholder_set: set[Function]) -> tuple[ExprWithAssumptions, str | list[list[str]], Expr | None, Exception | None]: + + expression, dim_expression, error = replace_placeholder_funcs(expression, None, True, parameter_subs, dim_subs, placeholder_map, - placeholder_set, dim_values_dict, [], + placeholder_set, DataTableSubs()) if not is_matrix(expression): if simplify_symbolic_expressions: @@ -2466,7 +2463,7 @@ def get_evaluated_expression(expression: Expr, row.append(custom_latex(cast(Expr, expression[i,j]))) evaluated_expression = cast(ExprWithAssumptions, expression.evalf(PRECISION)) - return evaluated_expression, symbolic_expression, dim_values_dict + return evaluated_expression, symbolic_expression, dim_expression, error def get_result(evaluated_expression: ExprWithAssumptions, dimensional_analysis_expression: Expr | None, dim_sub_error: Exception | None, symbolic_expression: str, @@ -2707,16 +2704,12 @@ def evaluate_statements(statements: list[InputAndSystemStatement], else: expression = cast(Expr, item["expression"].doit()) - evaluated_expression, symbolic_expression, dim_values_dict = get_evaluated_expression(expression, - parameter_subs, - simplify_symbolic_expressions, - placeholder_map, - placeholder_set) - dimensional_analysis_expression, dim_sub_error = get_dimensional_analysis_expression(dimensional_analysis_subs, - expression, - placeholder_map, - placeholder_set, - dim_values_dict) + evaluated_expression, symbolic_expression, dimensional_analysis_expression, dim_sub_error = get_evaluated_expression(expression, + parameter_subs, + dimensional_analysis_subs, + simplify_symbolic_expressions, + placeholder_map, + placeholder_set) if not is_matrix(evaluated_expression): results[index] = get_result(evaluated_expression, dimensional_analysis_expression, diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index f3cc1a18..d8ac09a4 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -637,7 +637,7 @@ export class LatexToSympy extends LatexParserVisitor { @@ -1130,7 +1130,7 @@ export class LatexToSympy extends LatexParserVisitor { @@ -1277,7 +1277,7 @@ export class LatexToSympy extends LatexParserVisitor Date: Tue, 21 Jan 2025 11:06:02 -0600 Subject: [PATCH 49/53] tests: add test for units in exponent for symbolic solve Needed since placeholder functions aren't used in this case and future versions of SymPy may change this behavior --- tests/test_system_solve.spec.mjs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_system_solve.spec.mjs b/tests/test_system_solve.spec.mjs index f08447fc..58fe9bf7 100644 --- a/tests/test_system_solve.spec.mjs +++ b/tests/test_system_solve.spec.mjs @@ -1056,4 +1056,34 @@ test('Test zero placeholder symbolic without units', async () => { expect(parseLatexFloat(content)).toBeCloseTo(2, precision); content = await page.textContent('#result-units-2'); expect(content).toBe(''); +}); + +test('Test exponent with units', async () => { + await page.forceDeleteCell(0); + await page.locator('#add-system-cell').click(); + + await page.setLatex(0, String.raw`y=2\left\lbrack m\right\rbrack^{x}`, 0);; + await page.locator('#system-parameterlist-0 math-field.editable').type('y'); + + await page.locator('#add-math-cell').click(); + await page.setLatex(1, String.raw`y\left(x=2\left\lbrack m\right\rbrack\right)=`); + + await page.locator('#add-math-cell').click(); + await page.setLatex(2, String.raw`y\left(x=\frac{4\left\lbrack m\right\rbrack}{2\left\lbrack m\right\rbrack}\right)=`); + + await page.locator('#add-math-cell').click(); + await page.setLatex(3, String.raw`y\left(x=2\right)=`); + + await page.waitForSelector('text=Updating...', {state: 'detached'}); + + await expect(page.locator('#cell-1 >> text=Dimension Error')).toBeVisible(); + + // Technically this shouldn't be a dimension error. However, without the placeholder functions + // raw sympy is unable to cancel exponent units + await expect(page.locator('#cell-2 >> text=Dimension Error')).toBeVisible(); + + let content = await page.textContent('#result-value-3'); + expect(parseLatexFloat(content)).toBeCloseTo(4, precision); + content = await page.textContent('#result-units-3'); + expect(content).toBe('m^2'); }); \ No newline at end of file From 85be828a110105243fbd425e4f66161f52b021cf Mon Sep 17 00:00:00 2001 From: mgreminger Date: Tue, 21 Jan 2025 16:01:32 -0600 Subject: [PATCH 50/53] chore: bump version and update releases dialog --- src/App.svelte | 2 +- src/Updates.svelte | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/App.svelte b/src/App.svelte index cdfca4a0..adec0e4f 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -96,7 +96,7 @@ const apiUrl = window.location.origin; - const currentVersion = 20250116; + const currentVersion = 20250121; const tutorialHash = "moJCuTwjPi7dZeZn5QiuaP"; const termsVersion = 20240110; diff --git a/src/Updates.svelte b/src/Updates.svelte index 98c009c3..95e8b0a9 100644 --- a/src/Updates.svelte +++ b/src/Updates.svelte @@ -16,6 +16,28 @@ } + +January 21, 2025 (Permalink: 20250121.engineeringpaper.xyz) +

Updated Dimension Handling System

+

+ The logic for dimension handling has been significantly revised and improved. The most significant benefit of this update + is improved error messages for dimension errors. Instead of just getting the dreaded Dimension Error message, + some context about the operation or function that lead to this error is included in the error message. This update also + makes calculations faster in general, provides more robust handling of dimensions that are slightly different due to floating + point rounding errors, and makes it easier to maintain and extend the dimension handling code in the future. This update + is required to enable some significant new features that are coming your way! +

+
+

+ Significant testing has gone into ensuring that this change does not introduce bugs. One change you may notice is that dimensions + were sometimes lost in the past for dimensioned values that had zero magnitude. This should no longer happen + but may lead to dimension errors for sheets that relied on the old behaviour. If you do notice a bug, please report the + bug to support@engineeringpaper.xyz + In the meantime, you may use the previous release's permalink (see below) until the bug is fixed. +

+ +
+ January 16, 2025 (Permalink: 20250116.engineeringpaper.xyz)

New Release Permalinks

From e92a2ffbb7c3aab83249048460de0e5b49a19ac8 Mon Sep 17 00:00:00 2001 From: mgreminger Date: Tue, 21 Jan 2025 16:34:12 -0600 Subject: [PATCH 51/53] refactor: remove some unneeded code Not needed with new single pass refactor --- src/parser/LatexToSympy.ts | 8 ++------ src/parser/constants.ts | 2 -- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index d8ac09a4..bc8823a3 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -18,7 +18,7 @@ import { type Insertion, type Replacement, applyEdits, import { RESERVED, GREEK_CHARS, UNASSIGNABLE, COMPARISON_MAP, UNITS_WITH_OFFSET, TYPE_PARSING_ERRORS, BUILTIN_FUNCTION_MAP, - BUILTIN_FUNCTION_NEEDS_VALUES, ZERO_PLACEHOLDER } from "./constants.js"; + ZERO_PLACEHOLDER } from "./constants.js"; import { MAX_MATRIX_COLS } from "../constants"; @@ -1274,11 +1274,7 @@ export class LatexToSympy extends LatexParserVisitor Date: Tue, 21 Jan 2025 16:38:44 -0600 Subject: [PATCH 52/53] refactor: remove unused property --- src/parser/LatexToSympy.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/parser/LatexToSympy.ts b/src/parser/LatexToSympy.ts index bc8823a3..230acba4 100644 --- a/src/parser/LatexToSympy.ts +++ b/src/parser/LatexToSympy.ts @@ -179,8 +179,6 @@ export class LatexToSympy extends LatexParserVisitor Date: Tue, 21 Jan 2025 16:48:19 -0600 Subject: [PATCH 53/53] refactor: remove some unused python imports --- public/dimensional_analysis.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index 6c67cf25..557146ca 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -54,21 +54,15 @@ Derivative, Matrix, MatrixBase, - Inverse, - Determinant, - Transpose, Subs, Pow, - MatMul, Eq, floor, ceiling, sign, sqrt, factorial, - Basic, Rational, - Integer, S ) @@ -76,8 +70,6 @@ class ExprWithAssumptions(Expr): is_finite: bool is_integer: bool -from sympy.core.function import UndefinedFunction - from sympy.printing.latex import modifier_dict from sympy.printing.numpy import NumPyPrinter @@ -104,7 +96,7 @@ class ExprWithAssumptions(Expr): from sympy.utilities.iterables import topological_sort -from sympy.utilities.lambdify import lambdify, implemented_function +from sympy.utilities.lambdify import lambdify from sympy.functions.elementary.trigonometric import TrigonometricFunction