diff --git a/README.md b/README.md index 8972901..e429f88 100644 --- a/README.md +++ b/README.md @@ -1060,7 +1060,7 @@ For Multiplication: `a * b = b * a` ## ConstantsSimplifyRule class ```python (doc) -ConstantsSimplifyRule(self, args, kwargs) +ConstantsSimplifyRule(self, evaluate_fractions: bool = False) ``` Given a binary operation on two constants, simplify to the resulting @@ -1459,7 +1459,6 @@ gen_simplify_multiple_terms( num_terms: int, optional_var: bool = False, op: Optional[List[str], str] = None, - common_variables: bool = True, inner_terms_scaling: float = 0.3, powers_probability: float = 0.33, optional_var_probability: float = 0.8, @@ -1539,1406 +1538,4 @@ a whole range of integers and floats. Using pretty numbers will restrict the numbers that are generated to integers between 1 and 12. When not using pretty numbers, floats and large integers will be included in the output from `rand_number` -# Tokenizer class - -```python (doc) -Tokenizer(self, exclude_padding: bool = True) -``` - -The Tokenizer produces a list of tokens from an input string. - -## eat_token method - -```python (doc) -Tokenizer.eat_token( - self, - context: mathy_core.tokenizer.TokenContext, - typeFn: Callable[[str], bool], -) -> str -``` - -Eat all of the tokens of a given type from the front of the stream -until a different type is hit, and return the text. - -## identify_alphas method - -```python (doc) -Tokenizer.identify_alphas( - self, - context: mathy_core.tokenizer.TokenContext, -) -> int -``` - -Identify and tokenize functions and variables. - -## identify_constants method - -```python (doc) -Tokenizer.identify_constants( - self, - context: mathy_core.tokenizer.TokenContext, -) -> int -``` - -Identify and tokenize a constant number. - -## identify_operators method - -```python (doc) -Tokenizer.identify_operators( - self, - context: mathy_core.tokenizer.TokenContext, -) -> bool -``` - -Identify and tokenize operators. - -## is_alpha method - -```python (doc) -Tokenizer.is_alpha(self, c: str) -> bool -``` - -Is this character a letter - -## is_number method - -```python (doc) -Tokenizer.is_number(self, c: str) -> bool -``` - -Is this character a number - -## tokenize method - -```python (doc) -Tokenizer.tokenize(self, buffer: str) -> List[mathy_core.tokenizer.Token] -``` - -Return an array of `Token`s from a given string input. -This throws an exception if an unknown token type is found in the input. - -# mathy_core.parser - -## ExpressionParser class - -```python (doc) -ExpressionParser(self) -> None -``` - -Parser for converting text into binary trees. Trees encode the order of -operations for an input, and allow evaluating it to detemrine the expression -value. - -### Grammar Rules - -Symbols: - -``` -( ) == Non-terminal -{ }* == 0 or more occurrences -{ }+ == 1 or more occurrences -{ }? == 0 or 1 occurrences -[ ] == Mandatory (1 must occur) -| == logical OR -" " == Terminal symbol (literal) -``` - -Non-terminals defined/parsed by Tokenizer: - -``` -(Constant) = anything that can be parsed by `float(in)` -(Variable) = any string containing only letters (a-z and A-Z) -``` - -Rules: - -``` -(Function) = [ functionName ] "(" (AddExp) ")" -(Factor) = { (Variable) | (Function) | "(" (AddExp) ")" }+ { { "^" }? (UnaryExp) }? -(FactorPrefix) = [ (Constant) { (Factor) }? | (Factor) ] -(UnaryExp) = { "-" }? (FactorPrefix) -(ExpExp) = (UnaryExp) { { "^" }? (UnaryExp) }? -(MultExp) = (ExpExp) { { "*" | "/" }? (ExpExp) }* -(AddExp) = (MultExp) { { "+" | "-" }? (MultExp) }* -(EqualExp) = (AddExp) { { "=" }? (AddExp) }* -(start) = (EqualExp) -``` - -### check method - -```python (doc) -ExpressionParser.check( - self, - tokens: mathy_core.parser.TokenSet, - do_assert: bool = False, -) -> bool -``` - -Check if the `self.current_token` is a member of a set Token types - -Args: - `tokens` The set of Token types to check against - -`Returns` True if the `current_token`'s type is in the set else False - -### eat method - -```python (doc) -ExpressionParser.eat(self, type: int) -> bool -``` - -Assign the next token in the queue to current_token if its type -matches that of the specified parameter. If the type does not match, -raise a syntax exception. - -Args: - `type` The type that your syntax expects @current_token to be - -### next method - -```python (doc) -ExpressionParser.next(self) -> bool -``` - -Assign the next token in the queue to `self.current_token`. - -Return True if there are still more tokens in the queue, or False if there -are no more tokens to look at. - -### parse method - -```python (doc) -ExpressionParser.parse( - self, - input_text: str, -) -> mathy_core.expressions.MathExpression -``` - -Parse a string representation of an expression into a tree -that can be later evaluated. - -Returns : The evaluatable expression tree. - -## TokenSet class - -```python (doc) -TokenSet(self, source: int) -``` - -TokenSet objects are bitmask combinations for checking to see -if a token is part of a valid set. - -### add method - -```python (doc) -TokenSet.add(self, addTokens: int) -> 'TokenSet' -``` - -Add tokens to self set and return a TokenSet representing -their combination of flags. Value can be an integer or an instance -of `TokenSet` - -### contains method - -```python (doc) -TokenSet.contains(self, type: int) -> bool -``` - -Returns true if the given type is part of this set - -# mathy_core.tree - -## BinaryTreeNode class - -```python (doc) -BinaryTreeNode( - self: ~NodeType, - left: Optional[~NodeType] = None, - right: Optional[~NodeType] = None, - parent: Optional[~NodeType] = None, - id: Optional[str] = None, -) -``` - -The binary tree node is the base node for all of our trees, and provides a -rich set of methods for constructing, inspecting, and modifying them. -The node itself defines the structure of the binary tree, having left and right -children, and a parent. - -### clone method - -```python (doc) -BinaryTreeNode.clone(self: ~NodeType) -> ~NodeType -``` - -Create a clone of this tree - -### get_children method - -```python (doc) -BinaryTreeNode.get_children(self: ~NodeType) -> List[~NodeType] -``` - -Get children as an array. If there are two children, the first object will -always represent the left child, and the second will represent the right. - -### get_root method - -```python (doc) -BinaryTreeNode.get_root(self: ~NodeType) -> ~NodeType -``` - -Return the root element of this tree - -### get_root_side method - -```python (doc) -BinaryTreeNode.get_root_side(self: ~NodeType) -> Literal['left', 'right'] -``` - -Return the side of the tree that this node lives on - -### get_sibling method - -```python (doc) -BinaryTreeNode.get_sibling(self: ~NodeType) -> Optional[~NodeType] -``` - -Get the sibling node of this node. If there is no parent, or the node -has no sibling, the return value will be None. - -### get_side method - -```python (doc) -BinaryTreeNode.get_side( - self, - child: Optional[~NodeType], -) -> Literal['left', 'right'] -``` - -Determine whether the given `child` is the left or right child of this -node - -### is_leaf method - -```python (doc) -BinaryTreeNode.is_leaf(self) -> bool -``` - -Is this node a leaf? A node is a leaf if it has no children. - -### rotate method - -```python (doc) -BinaryTreeNode.rotate(self: ~NodeType) -> ~NodeType -``` - -Rotate a node, changing the structure of the tree, without modifying -the order of the nodes in the tree. - -### set_left method - -```python (doc) -BinaryTreeNode.set_left( - self: ~NodeType, - child: Optional[~NodeType] = None, - clear_old_child_parent: bool = False, -) -> ~NodeType -``` - -Set the left node to the passed `child` - -### set_right method - -```python (doc) -BinaryTreeNode.set_right( - self: ~NodeType, - child: Optional[~NodeType] = None, - clear_old_child_parent: bool = False, -) -> ~NodeType -``` - -Set the right node to the passed `child` - -### set_side method - -```python (doc) -BinaryTreeNode.set_side( - self, - child: ~NodeType, - side: Literal['left', 'right'], -) -> ~NodeType -``` - -Set a new `child` on the given `side` - -### visit_inorder method - -```python (doc) -BinaryTreeNode.visit_inorder( - self, - visit_fn: Callable[[Any, int, Optional[Any]], Optional[Literal['stop']]], - depth: int = 0, - data: Optional[Any] = None, -) -> Optional[Literal['stop']] -``` - -Visit the tree inorder, which visits the left child, then the current node, -and then its right child. - -_Left -> Visit -> Right_ - -This method accepts a function that will be invoked for each node in the -tree. The callback function is passed three arguments: the node being -visited, the current depth in the tree, and a user specified data parameter. - -!!! info - - Traversals may be canceled by returning `STOP` from any visit function. - -### visit_postorder method - -```python (doc) -BinaryTreeNode.visit_postorder( - self, - visit_fn: Callable[[Any, int, Optional[Any]], Optional[Literal['stop']]], - depth: int = 0, - data: Optional[Any] = None, -) -> Optional[Literal['stop']] -``` - -Visit the tree postorder, which visits its left child, then its right child, -and finally the current node. - -_Left -> Right -> Visit_ - -This method accepts a function that will be invoked for each node in the -tree. The callback function is passed three arguments: the node being -visited, the current depth in the tree, and a user specified data parameter. - -!!! info - - Traversals may be canceled by returning `STOP` from any visit function. - -### visit_preorder method - -```python (doc) -BinaryTreeNode.visit_preorder( - self, - visit_fn: Callable[[Any, int, Optional[Any]], Optional[Literal['stop']]], - depth: int = 0, - data: Optional[Any] = None, -) -> Optional[Literal['stop']] -``` - -Visit the tree preorder, which visits the current node, then its left -child, and then its right child. - -_Visit -> Left -> Right_ - -This method accepts a function that will be invoked for each node in the -tree. The callback function is passed three arguments: the node being -visited, the current depth in the tree, and a user specified data parameter. - -!!! info - - Traversals may be canceled by returning `STOP` from any visit function. - -## VisitDataType - -Template type of user data passed to visit functions. - -# mathy_core.expressions - -## AbsExpression class - -```python (doc) -AbsExpression( - self, - child: Optional[mathy_core.expressions.MathExpression] = None, - child_on_left: bool = False, -) -``` - -Evaluates the absolute value of an expression. - -## AddExpression class - -```python (doc) -AddExpression( - self, - left: Optional[mathy_core.expressions.MathExpression] = None, - right: Optional[mathy_core.expressions.MathExpression] = None, -) -``` - -Add one and two - -## BinaryExpression class - -```python (doc) -BinaryExpression( - self, - left: Optional[mathy_core.expressions.MathExpression] = None, - right: Optional[mathy_core.expressions.MathExpression] = None, -) -``` - -An expression that operates on two sub-expressions - -### get_priority method - -```python (doc) -BinaryExpression.get_priority(self) -> int -``` - -Return a number representing the order of operations priority -of this node. This can be used to check if a node is `locked` -with respect to another node, i.e. the other node must be resolved -first during evaluation because of it's priority. - -### to_math_ml_fragment method - -```python (doc) -BinaryExpression.to_math_ml_fragment(self) -> str -``` - -Render this node as a MathML element fragment - -## ConstantExpression class - -```python (doc) -ConstantExpression(self, value: Optional[float, int] = None) -``` - -A Constant value node, where the value is accessible as `node.value` - -## DivideExpression class - -```python (doc) -DivideExpression( - self, - left: Optional[mathy_core.expressions.MathExpression] = None, - right: Optional[mathy_core.expressions.MathExpression] = None, -) -``` - -Divide one by two - -## EqualExpression class - -```python (doc) -EqualExpression( - self, - left: Optional[mathy_core.expressions.MathExpression] = None, - right: Optional[mathy_core.expressions.MathExpression] = None, -) -``` - -Evaluate equality of two expressions - -### operate method - -```python (doc) -EqualExpression.operate( - self, - one: Union[float, int], - two: Union[float, int], -) -> Union[float, int] -``` - -Return the value of the equation if one == two. - -Raise ValueError if both sides of the equation don't agree. - -## FactorialExpression class - -```python (doc) -FactorialExpression( - self, - child: Optional[mathy_core.expressions.MathExpression] = None, - child_on_left: bool = False, -) -``` - -Factorial of a constant, e.g. `5` evaluates to `120` - -## FunctionExpression class - -```python (doc) -FunctionExpression( - self, - child: Optional[mathy_core.expressions.MathExpression] = None, - child_on_left: bool = False, -) -``` - -A Specialized UnaryExpression that is used for functions. The function name in -text (used by the parser and tokenizer) is derived from the name() method on the -class. - -## MathExpression class - -```python (doc) -MathExpression( - self, - id: Optional[str] = None, - left: Optional[MathExpression] = None, - right: Optional[MathExpression] = None, - parent: Optional[MathExpression] = None, -) -``` - -Math tree node with helpers for manipulating expressions. - -`mathy:x+y=z` - -### add_class method - -```python (doc) -MathExpression.add_class( - self, - classes: Union[List[str], str], -) -> 'MathExpression' -``` - -Associate a class name with an expression. This class name will be -attached to nodes when the expression is converted to a capable output -format. - -See `MathExpression.to_math_ml_fragment` - -### all_changed method - -```python (doc) -MathExpression.all_changed(self) -> None -``` - -Mark this node and all of its children as changed - -### clear_classes method - -```python (doc) -MathExpression.clear_classes(self) -> None -``` - -Clear all the classes currently set on the nodes in this expression. - -### clone method - -```python (doc) -MathExpression.clone(self) -> 'MathExpression' -``` - -A specialization of the clone method that can track and report a cloned -subtree node. - -See `MathExpression.clone_from_root` for more details. - -### clone_from_root method - -```python (doc) -MathExpression.clone_from_root( - self, - node: Optional[MathExpression] = None, -) -> 'MathExpression' -``` - -Clone this node including the entire parent hierarchy that it has. This -is useful when you want to clone a subtree and still maintain the overall -hierarchy. - -**Arguments** - -- **node (MathExpression)**: The node to clone. - -**Returns** - -`(MathExpression)`: The cloned node. - -### color - -Color to use for this node when rendering it as changed with -`.terminal_text` - -### evaluate method - -```python (doc) -MathExpression.evaluate( - self, - context: Optional[Dict[str, Union[float, int]]] = None, -) -> Union[float, int] -``` - -Evaluate the expression, resolving all variables to constant values - -### find_id method - -```python (doc) -MathExpression.find_id(self, id: str) -> Optional[MathExpression] -``` - -Find an expression by its unique ID. - -Returns: The found `MathExpression` or `None` - -### find_type method - -```python (doc) -MathExpression.find_type(self, instanceType: Type[~NodeType]) -> List[~NodeType] -``` - -Find an expression in this tree by type. - -- instanceType: The type to check for instances of - -Returns the found `MathExpression` objects of the given type. - -### make_ml_tag method - -```python (doc) -MathExpression.make_ml_tag( - self, - tag: str, - content: str, - classes: List[str] = [], -) -> str -``` - -Make a MathML tag for the given content while respecting the node's given -classes. - -**Arguments** - -- **tag (str)**: The ML tag name to create. -- **content (str)**: The ML content to place inside of the tag. - classes (List[str]) An array of classes to attach to this tag. - -**Returns** - -`(str)`: A MathML element with the given tag, content, and classes - -### path_to_root method - -```python (doc) -MathExpression.path_to_root(self) -> str -``` - -Generate a namespaced path key to from the current node to the root. -This key can be used to identify a node inside of a tree. - -### raw - -raw text representation of the expression. - -### set_changed method - -```python (doc) -MathExpression.set_changed(self) -> None -``` - -Mark this node as having been changed by the application of a Rule - -### terminal_text - -Text output of this node that includes terminal color codes that -highlight which nodes have been changed in this tree as a result of -a transformation. - -### to_list method - -```python (doc) -MathExpression.to_list( - self, - visit: str = 'preorder', -) -> List[MathExpression] -``` - -Convert this node hierarchy into a list. - -### to_math_ml method - -```python (doc) -MathExpression.to_math_ml(self) -> str -``` - -Convert this expression into a MathML container. - -### to_math_ml_fragment method - -```python (doc) -MathExpression.to_math_ml_fragment(self) -> str -``` - -Convert this single node into MathML. - -### with_color method - -```python (doc) -MathExpression.with_color(self, text: str, style: str = 'bright') -> str -``` - -Render a string that is colored if something has changed - -## MultiplyExpression class - -```python (doc) -MultiplyExpression( - self, - left: Optional[mathy_core.expressions.MathExpression] = None, - right: Optional[mathy_core.expressions.MathExpression] = None, -) -``` - -Multiply one and two - -## NegateExpression class - -```python (doc) -NegateExpression( - self, - child: Optional[mathy_core.expressions.MathExpression] = None, - child_on_left: bool = False, -) -``` - -Negate an expression, e.g. `4` becomes `-4` - -### to_math_ml_fragment method - -```python (doc) -NegateExpression.to_math_ml_fragment(self) -> str -``` - -Convert this single node into MathML. - -## PowerExpression class - -```python (doc) -PowerExpression( - self, - left: Optional[mathy_core.expressions.MathExpression] = None, - right: Optional[mathy_core.expressions.MathExpression] = None, -) -``` - -Raise one to the power of two - -## SgnExpression class - -```python (doc) -SgnExpression( - self, - child: Optional[mathy_core.expressions.MathExpression] = None, - child_on_left: bool = False, -) -``` - -### operate method - -```python (doc) -SgnExpression.operate(self, value: Union[float, int]) -> Union[float, int] -``` - -Determine the sign of an value. - -**Returns** - -`(int)`: -1 if negative, 1 if positive, 0 if 0 - -## SubtractExpression class - -```python (doc) -SubtractExpression( - self, - left: Optional[mathy_core.expressions.MathExpression] = None, - right: Optional[mathy_core.expressions.MathExpression] = None, -) -``` - -Subtract one from two - -## UnaryExpression class - -```python (doc) -UnaryExpression( - self, - child: Optional[mathy_core.expressions.MathExpression] = None, - child_on_left: bool = False, -) -``` - -An expression that operates on one sub-expression - -# mathy_core.rules.associative_swap - -## AssociativeSwapRule class - -```python (doc) -AssociativeSwapRule(self, args, kwargs) -``` - -Associative Property -Addition: `(a + b) + c = a + (b + c)` - - (y) + + (x) - / \ / \ - / \ / \ - (x) + c -> a + (y) - / \ / \ - / \ / \ - a b b c - -Multiplication: `(ab)c = a(bc)` - - (x) * * (y) - / \ / \ - / \ / \ - (y) * c <- a * (x) - / \ / \ - / \ / \ - a b b c - -# mathy_core.rules.balanced_move - -## BalancedMoveRule class - -```python (doc) -BalancedMoveRule(self, args, kwargs) -``` - -Balanced rewrite rule moves nodes from one side of an equation -to the other by performing the same operation on both sides. - -Addition: `a + 2 = 3` -> `a + 2 - 2 = 3 - 2` -Multiplication: `3a = 3` -> `3a / 3 = 3 / 3` - -### get_type method - -```python (doc) -BalancedMoveRule.get_type( - self, - node: mathy_core.expressions.MathExpression, -) -> Optional[str] -``` - -Determine the configuration of the tree for this transformation. - -Supports the following configurations: - -- Addition is a term connected by an addition to the side of an equation - or inequality. It generates two subtractions to move from one side to the - other. -- Multiply is a coefficient of a term that must be divided on both sides of - the equation or inequality. - -# mathy_core.rules.commutative_swap - -## CommutativeSwapRule class - -```python (doc) -CommutativeSwapRule(self, preferred: bool = True) -``` - -Commutative Property -For Addition: `a + b = b + a` - - + + - / \ / \ - / \ -> / \ - / \ / \ - a b b a - -For Multiplication: `a * b = b * a` - - * * - / \ / \ - / \ -> / \ - / \ / \ - a b b a - -# mathy_core.rules.constants_simplify - -## ConstantsSimplifyRule class - -```python (doc) -ConstantsSimplifyRule(self, args, kwargs) -``` - -Given a binary operation on two constants, simplify to the resulting -constant expression - -### get_type method - -```python (doc) -ConstantsSimplifyRule.get_type( - self, - node: mathy_core.expressions.MathExpression, -) -> Optional[Tuple[str, mathy_core.expressions.ConstantExpression, mathy_core.expressions.ConstantExpression]] -``` - -Determine the configuration of the tree for this transformation. - -Support the three types of tree configurations: - -- Simple is where the node's left and right children are exactly - constants linked by an add operation. -- Chained Right is where the node's left child is a constant, but the right - child is another binary operation of the same type. In this case the left - child of the next binary node is the target. - -Structure: - -- Simple - - node(add),node.left(const),node.right(const) -- Chained Right - - node(add),node.left(const),node.right(add),node.right.left(const) -- Chained Right Deep - - node(add),node.left(const),node.right(add),node.right.left(const) - -# mathy_core.rules.distributive_factor_out - -## DistributiveFactorOutRule class - -```python (doc) -DistributiveFactorOutRule(self, constants: bool = False) -``` - -Distributive Property -`ab + ac = a(b + c)` - -The distributive property can be used to expand out expressions -to allow for simplification, as well as to factor out common properties -of terms. - -**Factor out a common term** - -This handles the `ab + ac` conversion of the distributive property, which -factors out a common term from the given two addition operands. - - + * - / \ / \ - / \ / \ - / \ -> / \ - * * a + - / \ / \ / \ - a b a c b c - -### get_type method - -```python (doc) -DistributiveFactorOutRule.get_type( - self, - node: mathy_core.expressions.MathExpression, -) -> Optional[Tuple[str, mathy_core.util.TermEx, mathy_core.util.TermEx]] -``` - -Determine the configuration of the tree for this transformation. - -Support the three types of tree configurations: - -- Simple is where the node's left and right children are exactly - terms linked by an add operation. -- Chained Left is where the node's left child is a term, but the right - child is another add operation. In this case the left child - of the next add node is the target. -- Chained Right is where the node's right child is a term, but the left - child is another add operation. In this case the right child - of the child add node is the target. - -Structure: - -- Simple - - node(add),node.left(term),node.right(term) -- Chained Left - - node(add),node.left(term),node.right(add),node.right.left(term) -- Chained Right - - node(add),node.right(term),node.left(add),node.left.right(term) - -# mathy_core.rules.distributive_multiply_across - -## DistributiveMultiplyRule class - -```python (doc) -DistributiveMultiplyRule(self, args, kwargs) -``` - -Distributive Property -`a(b + c) = ab + ac` - -The distributive property can be used to expand out expressions -to allow for simplification, as well as to factor out common properties of terms. - -**Distribute across a group** - -This handles the `a(b + c)` conversion of the distributive property, which -distributes `a` across both `b` and `c`. - -_note: this is useful because it takes a complex Multiply expression and -replaces it with two simpler ones. This can expose terms that can be -combined for further expression simplification._ - - + - * / \ - / \ / \ - / \ / \ - a + -> * * - / \ / \ / \ - / \ / \ / \ - b c a b a c - -# mathy_core.rules.variable_multiply - -## VariableMultiplyRule class - -```python (doc) -VariableMultiplyRule(self, args, kwargs) -``` - -This restates `x^b * x^d` as `x^(b + d)` which has the effect of isolating -the exponents attached to the variables, so they can be combined. - - 1. When there are two terms with the same base being multiplied together, their - exponents are added together. "x * x^3" = "x^4" because "x = x^1" so - "x^1 * x^3 = x^(1 + 3) = x^4" - - TODO: 2. When there is a power raised to another power, they can be combined by - multiplying the exponents together. "x^(2^2) = x^4" - -The rule identifies terms with explicit and implicit powers, so the following -transformations are all valid: - -Explicit powers: x^b \* x^d = x^(b+d) - - * - / \ - / \ ^ - / \ = / \ - ^ ^ x + - / \ / \ / \ - x b x d b d - -Implicit powers: x \* x^d = x^(1 + d) - - * - / \ - / \ ^ - / \ = / \ - x ^ x + - / \ / \ - x d 1 d - -### get_type method - -```python (doc) -VariableMultiplyRule.get_type( - self, - node: mathy_core.expressions.MathExpression, -) -> Optional[Tuple[str, mathy_core.util.TermEx, mathy_core.util.TermEx]] -``` - -Determine the configuration of the tree for this transformation. - -Support two types of tree configurations: - -- Simple is where the node's left and right children are exactly - terms that can be multiplied together. -- Chained is where the node's left child is a term, but the right - child is a continuation of a more complex term, as indicated by - the presence of another Multiply node. In this case the left child - of the next multiply node is the target. - -Structure: - -- Simple node(mult),node.left(term),node.right(term) -- Chained node(mult),node.left(term),node.right(mult),node.right.left(term) - -# mathy_core.layout - -## TreeLayout class - -```python (doc) -TreeLayout(self, args, kwargs) -``` - -Calculate a visual layout for input trees. - -### layout method - -```python (doc) -TreeLayout.layout( - self, - node: mathy_core.tree.BinaryTreeNode, - unit_x_multiplier: float = 1.0, - unit_y_multiplier: float = 1.0, -) -> 'TreeMeasurement' -``` - -Assign x/y values to all nodes in the tree, and return an object containing -the measurements of the tree. - -Returns a TreeMeasurement object that describes the bounds of the tree - -### transform method - -```python (doc) -TreeLayout.transform( - self, - node: Optional[mathy_core.tree.BinaryTreeNode] = None, - x: float = 0, - unit_x_multiplier: float = 1, - unit_y_multiplier: float = 1, - measure: Optional[TreeMeasurement] = None, -) -> 'TreeMeasurement' -``` - -Transform relative to absolute coordinates, and measure the bounds of -the tree. - -Return a measurement of the tree in output units. - -## TreeMeasurement class - -```python (doc) -TreeMeasurement(self) -> None -``` - -Summary of the rendered tree - -# mathy_core.problems - -## Problem Generation - -Utility functions for helping generate input problems. - -## DefaultType - -Template type for a default return value - -## gen_binomial_times_binomial function - -```python (doc) -gen_binomial_times_binomial( - op: str = '+', - min_vars: int = 1, - max_vars: int = 2, - simple_variables: bool = True, - powers_probability: float = 0.33, - like_variables_probability: float = 1.0, -) -> Tuple[str, int] -``` - -Generate a binomial multiplied by another binomial. - -**Example** - -``` -(2e + 12p)(16 + 7e) -``` - -`mathy:(2e + 12p)(16 + 7e)` - -## gen_binomial_times_monomial function - -```python (doc) -gen_binomial_times_monomial( - op: str = '+', - min_vars: int = 1, - max_vars: int = 2, - simple_variables: bool = True, - powers_probability: float = 0.33, - like_variables_probability: float = 1.0, -) -> Tuple[str, int] -``` - -Generate a binomial multiplied by a monomial. - -**Example** - -``` -(4x^3 + y) * 2x -``` - -`mathy:(4x^3 + y) * 2x` - -## gen_combine_terms_in_place function - -```python (doc) -gen_combine_terms_in_place( - min_terms: int = 16, - max_terms: int = 26, - easy: bool = True, - powers: bool = False, -) -> Tuple[str, int] -``` - -Generate a problem that puts one pair of like terms next to each other -somewhere inside a large tree of unlike terms. - -The problem is intended to be solved in a very small number of moves, making -training across many episodes relatively quick, and reducing the combinatorial -explosion of branches that need to be searched to solve the task. - -The hope is that by focusing the agent on selecting the right moves inside of a -ridiculously large expression it will learn to select actions to combine like terms -invariant of the sequence length. - -**Example** - -``` -4y + 12j + 73q + 19k + 13z + 56l + (24x + 12x) + 43n + 17j -``` - -`mathy:4y + 12j + 73q + 19k + 13z + 56l + (24x + 12x) + 43n + 17j` - -## gen_commute_haystack function - -```python (doc) -gen_commute_haystack( - min_terms: int = 5, - max_terms: int = 8, - commute_blockers: int = 1, - easy: bool = True, - powers: bool = False, -) -> Tuple[str, int] -``` - -A problem with a bunch of terms that have no matches, and a single -set of two terms that do match, but are separated by one other term. -The challenge is to commute the terms to each other in one move. - -**Example** - -``` -4y + 12j + 73q + 19k + 13z + 24x + 56l + 12x + 43n + 17j" - ^-----------^ -``` - -`mathy:4y + 12j + 73q + 19k + 13z + 24x + 56l + 12x + 43n + 17j` - -## gen_move_around_blockers_one function - -```python (doc) -gen_move_around_blockers_one( - number_blockers: int, - powers_probability: float = 0.5, -) -> Tuple[str, int] -``` - -Two like terms separated by (n) blocker terms. - -**Example** - -``` -4x + (y + f) + x -``` - -`mathy:4x + (y + f) + x` - -## gen_move_around_blockers_two function - -```python (doc) -gen_move_around_blockers_two( - number_blockers: int, - powers_probability: float = 0.5, -) -> Tuple[str, int] -``` - -Two like terms with three blockers. - -**Example** - -``` -7a + 4x + (2f + j) + x + 3d -``` - -`mathy:7a + 4x + (2f + j) + x + 3d` - -## gen_simplify_multiple_terms function - -```python (doc) -gen_simplify_multiple_terms( - num_terms: int, - optional_var: bool = False, - op: Optional[List[str], str] = None, - common_variables: bool = True, - inner_terms_scaling: float = 0.3, - powers_probability: float = 0.33, - optional_var_probability: float = 0.8, - noise_probability: float = 0.8, - shuffle_probability: float = 0.66, - share_var_probability: float = 0.5, - grouping_noise_probability: float = 0.66, - noise_terms: Optional[int] = None, -) -> Tuple[str, int] -``` - -Generate a polynomial problem with like terms that need to be combined and -simplified. - -**Example** - -``` -2a + 3j - 7b + 17.2a + j -``` - -`mathy:2a + 3j - 7b + 17.2a + j` - -## get_blocker function - -```python (doc) -get_blocker( - num_blockers: int = 1, - exclude_vars: Optional[List[str]] = None, -) -> str -``` - -Get a string of terms to place between target simplification terms -in order to challenge the agent's ability to use commutative/associative -rules to move terms around. - -## get_rand_vars function - -```python (doc) -get_rand_vars( - num_vars: int, - exclude_vars: Optional[List[str]] = None, - common_variables: bool = False, -) -> List[str] -``` - -Get a list of random variables, excluding the given list of hold-out variables - -## MathyTermTemplate dataclass - -```python (doc) -MathyTermTemplate( - self, - variable: Optional[str] = None, - exponent: Optional[float, int] = None, -) -> None -``` - -MathyTermTemplate(variable: Optional[str] = None, exponent: Union[float, int, NoneType] = None) - -## split_in_two_random function - -```python (doc) -split_in_two_random(value: int) -> Tuple[int, int] -``` - -Split a given number into two smaller numbers that sum to it. -Returns: a tuple of (lower, higher) numbers that sum to the input - -## use_pretty_numbers function - -```python (doc) -use_pretty_numbers(enabled: bool = True) -> None -``` - -Determine if problems should include only pretty numbers or -a whole range of integers and floats. Using pretty numbers will -restrict the numbers that are generated to integers between 1 and 12. When not using pretty numbers, floats and large integers will -be included in the output from `rand_number` - - - \ No newline at end of file + diff --git a/mathy_core/expressions.py b/mathy_core/expressions.py index bb6309a..960050e 100644 --- a/mathy_core/expressions.py +++ b/mathy_core/expressions.py @@ -576,28 +576,34 @@ def operate(self, one: NumberType, two: NumberType) -> NumberType: return one * two def __str__(self) -> str: - """Multiplication special cases constant*variable to output `4x` instead of - `4 * x`""" + """Special cases: + 1. constant*variable -> 4x + 2. fraction*variable -> 1/2x + 3. constant*variable^power -> 4x^2 + 4. fraction*variable^power -> (1/2)x^2 + """ left, right = self._check() + + # Handle fraction * variable or fraction * power cases + if isinstance(left, DivideExpression): + # Handle both direct variables and variables raised to a power + if isinstance(right, VariableExpression): + return self.with_color(f"({left}){right}") + elif isinstance(right, PowerExpression) and isinstance( + right.left, VariableExpression + ): + return self.with_color(f"({left}){right}") + + # Handle existing constant * variable cases if isinstance(left, ConstantExpression): - # const * var one = isinstance(right, VariableExpression) - # const * var^power two = isinstance(right, PowerExpression) and isinstance( right.left, VariableExpression ) if one or two: return self.with_color(f"{left}{right}") - return super().__str__() - def to_math_ml_fragment(self) -> str: - left, right = self._check() - right_ml = right.to_math_ml_fragment() - left_ml = left.to_math_ml_fragment() - if isinstance(left, ConstantExpression): - if isinstance(right, (VariableExpression, PowerExpression)): - return f"{left_ml}{right_ml}" - return super().to_math_ml_fragment() + return super().__str__() class DivideExpression(BinaryExpression): @@ -626,6 +632,42 @@ def operate(self, one: NumberType, two: NumberType) -> NumberType: else: return one / two + def __str__(self) -> str: + left, right = self._check() + + # Check if we're being used as a coefficient in multiplication with a variable + is_coefficient = ( + isinstance(self.parent, MultiplyExpression) + and self.parent.left is self # We're the left side of the multiplication + and isinstance(self.parent.right, (VariableExpression, PowerExpression)) + ) + + def needs_parens(expr: MathExpression) -> bool: + if isinstance(expr, MultiplyExpression): + # Only need parens for multiplication if: + # 1. It involves a power term (like 3x^2) + # 2. It's a complex multiplication (more than coefficient * variable) + return any( + isinstance(child, PowerExpression) + for child in (expr.left, expr.right) + ) or not ( + isinstance(expr.left, ConstantExpression) + and isinstance(expr.right, VariableExpression) + ) + return False + + left_str = f"({left})" if needs_parens(left) else str(left) + right_str = f"({right})" if needs_parens(right) else str(right) + + if is_coefficient: + # No spaces when coefficient + out = f"{left_str}{self.with_color(self.name)}{right_str}" + else: + # Keep spaces normally + out = f"{left_str} {self.with_color(self.name)} {right_str}" + + return f"({out})" if self.self_parens() else out + class PowerExpression(BinaryExpression): """Raise one to the power of two""" diff --git a/mathy_core/layout.py b/mathy_core/layout.py index af82d1b..50f6c13 100644 --- a/mathy_core/layout.py +++ b/mathy_core/layout.py @@ -1,5 +1,6 @@ -from typing import Optional +from typing import Any, Optional +from .expressions import BinaryExpression, MathExpression from .tree import BinaryTreeNode @@ -239,4 +240,114 @@ def transform( return measure +def render_tree_to_text(expression: MathExpression) -> str: + """Render a tree structure as ASCII text for terminal display. + + Args: + input_text: The math expression text to parse and render + + Returns: + A string containing the ASCII representation of the tree + """ + layout = TreeLayout() + measure: TreeMeasurement = layout.layout(expression, 6, 2) # type: ignore + + # Calculate canvas dimensions + padding = 4 + min_x = int(measure.minX - padding) + max_x = int(measure.maxX + padding) + min_y = int(measure.minY - padding) + max_y = int(measure.maxY + padding) + width = max_x - min_x + 1 + height = max_y - min_y + 1 + + # Create empty canvas + canvas = [[" " for _ in range(width)] for _ in range(height)] + + def draw_line(x1: int, y1: int, x2: int, y2: int) -> None: + """Draw a line between two points using ASCII characters.""" + # Adjust coordinates to canvas space + x1 = int(x1 - min_x) + x2 = int(x2 - min_x) + y1 = int(y1 - min_y) + y2 = int(y2 - min_y) + + if x1 == x2: # Vertical line + for y in range(min(y1, y2), max(y1, y2) + 1): + canvas[y][x1] = "│" + elif y1 == y2: # Horizontal line + for x in range(min(x1, x2), max(x1, x2) + 1): + canvas[y1][x] = "─" + else: # Diagonal lines + # Calculate middle point for drawing corners + mid_x = (x1 + x2) // 2 + mid_y = (y1 + y2) // 2 + + # Draw the diagonal connection + if abs(x2 - x1) > 1: # Only if there's enough space + if y1 < y2: + canvas[mid_y][mid_x] = "╱" if x1 > x2 else "╲" + else: + canvas[mid_y][mid_x] = "╲" if x1 > x2 else "╱" + + def node_visit(node: MathExpression, depth: int, data: Any) -> None: + """Visit each node and draw it on the canvas.""" + # Adjust coordinates to canvas space + x = int(node.x - min_x) # type: ignore + y = int(node.y - min_y) # type: ignore + + # Draw connection to parent + if node.parent: + int(node.parent.x - min_x) # type: ignore + int(node.parent.y - min_y) # type: ignore + draw_line(node.x, node.y, node.parent.x, node.parent.y) # type: ignore + + # Get node value + value = str(node) + if isinstance(node, BinaryExpression): + value = node.name + + # Draw node + node_width = len(value) + 2 # Add space for brackets + start_x = x - node_width // 2 + + # Draw node value + if start_x >= 0 and start_x + node_width < width and y >= 0 and y < height: + for i, char in enumerate(value): + if start_x + i < width: + canvas[y][start_x + i] = char + + # Visit all nodes + expression.visit_postorder(node_visit) + + # Trim empty rows and columns while preserving structure + # Find bounds of actual content + min_row = 0 + max_row = height - 1 + min_col = 0 + max_col = width - 1 + + # Find first and last non-empty rows + while min_row < height and all(c == " " for c in canvas[min_row]): + min_row += 1 + while max_row > 0 and all(c == " " for c in canvas[max_row]): + max_row -= 1 + + # Find first and last non-empty columns + while min_col < width and all(row[min_col] == " " for row in canvas): + min_col += 1 + while max_col > 0 and all(row[max_col] == " " for row in canvas): + max_col -= 1 + + # Extract the trimmed canvas + trimmed_canvas = [ + # fmt: off + row[min_col:max_col + 1] for row in canvas[min_row:max_row + 1] + # fmt: on + ] + + # Convert canvas to string + return "\n" + "\n".join("".join(row) for row in trimmed_canvas) + + __all__ = ("TidierExtreme", "TreeMeasurement", "TreeLayout") diff --git a/mathy_core/parser.py b/mathy_core/parser.py index 89347fe..36c942f 100644 --- a/mathy_core/parser.py +++ b/mathy_core/parser.py @@ -337,6 +337,7 @@ def parse_factors(self) -> MathExpression: if len(factors) == 0: raise InvalidExpression("No factors") + # Handle power expressions for the last factor exp: Optional[MathExpression] = None if self.check(_IS_EXP): opType = self.current_token.type @@ -345,18 +346,18 @@ def parse_factors(self) -> MathExpression: raise InvalidSyntax("Expected an expression after ^ operator") right = self.parse_unary() - exp = PowerExpression(factors[-1], right) + # Create power expression from the last factor + factors[-1] = PowerExpression(factors[-1], right) + # Combine all factors with multiplication if len(factors) == 1: - return exp or factors[0] + return factors[0] - while len(factors) > 0: - if exp is None: - exp = factors.pop(0) + # Build expression from left to right + exp = factors[0] + for i in range(1, len(factors)): + exp = MultiplyExpression(exp, factors[i]) - exp = MultiplyExpression(exp, factors.pop(0)) - - assert exp is not None return exp def parse_function(self) -> MathExpression: diff --git a/mathy_core/problems.py b/mathy_core/problems.py index 0033cf6..b605aa2 100644 --- a/mathy_core/problems.py +++ b/mathy_core/problems.py @@ -82,7 +82,10 @@ def get_rand_term_templates( ) variable = rand_var(common_variables) exponent: Optional[NumberType] = cast( - Union[int, None], maybe_number(exponent_probability * 100, None) + Union[int, None], + maybe_power( + exponent_probability * 100, or_else=None, include_exponent=False + ), ) # Don't generate x^1 if exponent == 1: @@ -127,9 +130,13 @@ def maybe_power( percent_chance: NumberType = 80, max_power: int = 4, or_else: DefaultType = "", # type:ignore + include_exponent: bool = True, ) -> Union[str, DefaultType]: if rand_bool(percent_chance): - return "^{}".format(random.randint(2, max_power)) + if include_exponent: + return "^{}".format(random.randint(2, max_power)) + else: + return str(random.randint(2, max_power)) else: return or_else @@ -320,7 +327,6 @@ def gen_simplify_multiple_terms( num_terms: int, optional_var: bool = False, op: Optional[Union[List[str], str]] = None, - common_variables: bool = True, inner_terms_scaling: float = 0.3, powers_probability: float = 0.33, optional_var_probability: float = 0.8, diff --git a/mathy_core/rule.py b/mathy_core/rule.py index 7b7052a..d0428e5 100644 --- a/mathy_core/rule.py +++ b/mathy_core/rule.py @@ -9,6 +9,12 @@ class BaseRule: """Basic rule class that visits a tree with a specified visit order.""" + @property + def maintains_variables(self) -> bool: + """Whether this rule maintains the same variables in the expression. Rules + that change variables should return False.""" + return True + @property def name(self) -> str: """Readable rule name used for debug rendering and description outputs""" diff --git a/mathy_core/rules/__init__.py b/mathy_core/rules/__init__.py index da949db..1d9982a 100644 --- a/mathy_core/rules/__init__.py +++ b/mathy_core/rules/__init__.py @@ -4,6 +4,7 @@ from .constants_simplify import ConstantsSimplifyRule # noqa from .distributive_factor_out import DistributiveFactorOutRule # noqa from .distributive_multiply_across import DistributiveMultiplyRule # noqa +from .fraction_reduction import FractionReductionRule # noqa from .multiplicative_inverse import MultiplicativeInverseRule # noqa from .restate_subtraction import RestateSubtractionRule # noqa from .variable_multiply import VariableMultiplyRule # noqa @@ -15,6 +16,7 @@ "ConstantsSimplifyRule", "DistributiveFactorOutRule", "DistributiveMultiplyRule", + "FractionReductionRule", "MultiplicativeInverseRule", "RestateSubtractionRule", "VariableMultiplyRule", diff --git a/mathy_core/rules/commutative_swap.test.json b/mathy_core/rules/commutative_swap.test.json index ff45ddf..109f6dc 100644 --- a/mathy_core/rules/commutative_swap.test.json +++ b/mathy_core/rules/commutative_swap.test.json @@ -1,5 +1,18 @@ { "valid": [ + { + "input": "(6/7)k^3", + "output": "k^3 * 6 / 7", + "args": { + "preferred": true + }, + "why": "commuting fractional coefficient with variable raised to a power" + }, + { + "input": "(5 + 12) * a", + "target": "(5 + 12) * a", + "output": "a * (5 + 12)" + }, { "input": "2x = 6x - 8", "target": "2x = 6x - 8", @@ -76,11 +89,7 @@ "output": "2530z + 3.5x + 1m + 2z + 8.9c", "why": "swapping middle children shouldn't introduce parenthesis nesting" }, - { - "input": "(5 + 12) * a", - "target": "(5 + 12) * a", - "output": "a * (5 + 12)" - }, + { "input": "2b^4 * 3x", "target": "2b^4", @@ -138,4 +147,4 @@ "input": "7 / x" } ] -} \ No newline at end of file +} diff --git a/mathy_core/rules/constants_simplify.py b/mathy_core/rules/constants_simplify.py index b67d7c3..470de7b 100644 --- a/mathy_core/rules/constants_simplify.py +++ b/mathy_core/rules/constants_simplify.py @@ -4,6 +4,7 @@ AddExpression, BinaryExpression, ConstantExpression, + DivideExpression, MathExpression, MultiplyExpression, NegateExpression, @@ -25,6 +26,12 @@ class ConstantsSimplifyRule(BaseRule): """Given a binary operation on two constants, simplify to the resulting constant expression""" + evaluate_fractions: bool + + def __init__(self, evaluate_fractions: bool = False): + # If false, terms that are in preferred order will not commute + self.evaluate_fractions = evaluate_fractions + @property def name(self) -> str: return "Constant Arithmetic" @@ -72,6 +79,9 @@ def get_type( and isinstance(node.left, ConstantExpression) and isinstance(node.right, ConstantExpression) ): + # If the parent is a division and we're not evaluating fractions, skip + if not self.evaluate_fractions and isinstance(node, DivideExpression): + return None return _POS_SIMPLE, node.left, node.right # Check for const * var * const diff --git a/mathy_core/rules/constants_simplify.test.json b/mathy_core/rules/constants_simplify.test.json index d70ba1d..b9e543f 100644 --- a/mathy_core/rules/constants_simplify.test.json +++ b/mathy_core/rules/constants_simplify.test.json @@ -80,7 +80,10 @@ }, { "input": "4 / 2", - "output": "2" + "output": "2", + "args": { + "evaluate_fractions": true + } }, { "input": "5 * 5", @@ -92,6 +95,10 @@ } ], "invalid": [ + { + "input": "(15/6)h", + "why": "doesn't simplify fractions by default" + }, { "input": "13f^4 * (5f^4 + 7)", "why": "can't simplify nested child because the nested group has a different priority" diff --git a/mathy_core/rules/fraction_reduction.md b/mathy_core/rules/fraction_reduction.md new file mode 100644 index 0000000..76db7f5 --- /dev/null +++ b/mathy_core/rules/fraction_reduction.md @@ -0,0 +1,48 @@ +# Fraction Reduction + +The `Fraction Reduction` rule simplifies fractions by canceling out common factors in the numerator and denominator. This transformation reduces the complexity of expressions and produces equivalent fractions in their simplest form. + +This rule implements the mathematical principle that when a factor appears in both the numerator and denominator of a fraction, it can be canceled out: `(a × c) / (b × c) = a / b` + +## Operations + +**Numeric Fraction Reduction** + +This handles cases where the numerator and denominator contain common numeric factors. For example, `6/3` simplifies to `2`. + +**Variable Cancellation** + +When the same variable appears in both the numerator and denominator, the rule cancels them out: + +- If the powers are equal, the variable is removed completely +- If the powers differ, the variable remains with the difference of the exponents + +For example: + +- `(6x) / (3x)` simplifies to `2` +- `(6x^2) / (3x)` simplifies to `2x` + +**Coefficient Reduction** + +The rule also handles coefficient reduction in conjunction with variable cancellation. It first factors out common terms, then reduces the numerical coefficient to its simplest form. + +For example, `(3x^2) / (6x)` simplifies to `(1/2)x`. + +**Exponent Simplification** + +When variables with exponents appear in both the numerator and denominator, the rule simplifies by subtracting the exponents. + +For example, `x^4 / x^2` simplifies to `x^2`. + +## Implementation Details + +The rule creates a reduced fraction by: + +1. Identifying common factors in the numerator and denominator +2. Reducing the numeric coefficients to their simplest form using GCD +3. Handling variable terms by adjusting their exponents appropriately +4. Constructing a new expression with the simplified terms + +### Examples + +`rule_tests:fraction_reduction` diff --git a/mathy_core/rules/fraction_reduction.py b/mathy_core/rules/fraction_reduction.py new file mode 100644 index 0000000..9363c22 --- /dev/null +++ b/mathy_core/rules/fraction_reduction.py @@ -0,0 +1,64 @@ +from typing import Optional + +from ..expressions import DivideExpression, MathExpression +from ..rule import BaseRule, ExpressionChangeRule +from ..util import ( + FractionReductionResult, + factor_fraction_terms_ex, + get_term_ex, + make_term_fractional, +) + + +class FractionReductionRule(BaseRule): + """Reduce fractions by cancelling out common factors in the numerator and + denominator.""" + + @property + def maintains_variables(self) -> bool: + # e.g. 3x / 6x -> 1/2 + return False + + @property + def name(self) -> str: + return "Fraction Reduction" + + @property + def code(self) -> str: + return "FR" + + def get_reduction_term( + self, node: MathExpression + ) -> Optional[FractionReductionResult]: + is_division = isinstance(node, DivideExpression) + if not is_division: + return None + left_term = get_term_ex(node.left) + right_term = get_term_ex(node.right) + if left_term is None or right_term is None: + return None + f = factor_fraction_terms_ex(left_term, right_term) + if not f: + return None + return f + + def can_apply_to(self, node: MathExpression) -> bool: + reduced_fraction_result = self.get_reduction_term(node) + return reduced_fraction_result is not None + + def apply_to(self, node: MathExpression) -> ExpressionChangeRule: + change = super().apply_to(node) + reduced_fraction_result = self.get_reduction_term(node) + assert ( + reduced_fraction_result is not None + ), "call can_apply_to before applying a rule" + + change.save_parent() # connect result to node.parent + result = make_term_fractional( + numerator=reduced_fraction_result.numerator, + denominator=reduced_fraction_result.denominator, + variable=reduced_fraction_result.reduced_variable, + exponent=reduced_fraction_result.reduced_exponent, + ) + result.set_changed() # mark this node as changed for visualization + return change.done(result) diff --git a/mathy_core/rules/fraction_reduction.test.json b/mathy_core/rules/fraction_reduction.test.json new file mode 100644 index 0000000..c8b2875 --- /dev/null +++ b/mathy_core/rules/fraction_reduction.test.json @@ -0,0 +1,77 @@ +{ + "valid": [ + { + "why": "Training corner case regression", + "input": "9n + (8/10)n", + "output": "9n + (4/5)n", + "debug": true + }, + { + "why": "Training corner case regression", + "input": "9n + 8n / 10", + "output": "9n + (4/5)n", + "debug": true + }, + { + "why": "Simple numeric fraction", + "input": "(6x) / (3x)", + "output": "2", + "debug": true + }, + { + "why": "Simple numeric fraction", + "input": "(6x^2) / (3x)", + "output": "2x", + "debug": true + }, + { + "why": "Simple numeric fraction", + "input": "(3x^2) / (6x)", + "output": "(1/2)x", + "debug": true + }, + { + "why": "Higher degree polynomial cancellation", + "input": "x^4 / x^2", + "output": "x^2" + } + ], + "invalid": [ + { + "why": "Code coverage - when factoring the fraction terms fails (divide 0 coefficient)", + "input": "4 / 0" + }, + { + "why": "Complex polynomial reduction -- rule sucking part 3", + "input": "(x^3 - x^2) / x^2", + "output": "x - 1" + }, + { + "why": "Cancel and reduce constants with variables -- my rule sucks x2. plz corner case jesus, save me", + "input": "(10x^2y) / (5xy)", + "output": "2x" + }, + { + "why": "Cancel common variable factor -- my rule sucks? needs more corner case.", + "input": "(4xy) / (2y)", + "output": "2x" + }, + + { + "why": "Fraction with no common factors", + "input": "(x + 1) / (y + 1)" + }, + { + "why": "Zero numerator which should not change", + "input": "0 / (x^2 + 1)" + }, + { + "why": "Zero denominator which is undefined", + "input": "(x^2 + 1) / 0" + }, + { + "why": "Fraction with non-factorizable common terms", + "input": "(x + y) / (y + x^2)" + } + ] +} diff --git a/mathy_core/testing.py b/mathy_core/testing.py index 4007c3b..7ed0b49 100644 --- a/mathy_core/testing.py +++ b/mathy_core/testing.py @@ -103,9 +103,13 @@ def run_rule_tests( compare_equation_values(before, after, eval_context=eval_context) else: # Compare the values of the in-memory expressions output from the rule - compare_expression_values(before, after) + compare_expression_values( + before, after, enforce_unique_vars=rule.maintains_variables + ) # Parse the output strings to new expressions, and compare the values - compare_expression_string_values(str(before), str(after)) + compare_expression_string_values( + str(before), str(after), enforce_unique_vars=rule.maintains_variables + ) actual = str(after).strip() expected = ex["output"] assert actual == expected, f"Expected '{actual}' to be '{expected}'" diff --git a/mathy_core/util.py b/mathy_core/util.py index d3e4851..73facf5 100644 --- a/mathy_core/util.py +++ b/mathy_core/util.py @@ -1,5 +1,6 @@ import math import random +from dataclasses import dataclass, field from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple, Union, cast import numpy as np @@ -17,6 +18,7 @@ SubtractExpression, VariableExpression, ) +from .layout import render_tree_to_text # type: ignore from .parser import ExpressionParser from .tree import LEFT, VisitStop from .types import Literal, NumberType @@ -28,13 +30,21 @@ def is_debug_mode() -> bool: def compare_expression_string_values( - from_expression: str, to_expression: str, history: Optional[List[Any]] = None + from_expression: str, + to_expression: str, + history: Optional[List[Any]] = None, + env_name: Optional[str] = None, + enforce_unique_vars: bool = True, ) -> None: """Compare and evaluate two expressions strings to verify they have the same value""" parser = ExpressionParser() return compare_expression_values( - parser.parse(from_expression), parser.parse(to_expression), history + parser.parse(from_expression), + parser.parse(to_expression), + history, + env_name, + enforce_unique_vars, ) @@ -47,7 +57,7 @@ def raise_with_history( history_text: List[str] = [] if history is not None: - history_text = [f" - {h.raw}" for h in history] + history_text = [f"[{h.action}] - {h.raw}" for h in history] history_text.insert(0, description) tb = TracebackPrinter() error = tb(title, "\n".join(history_text), tb=traceback.extract_stack()) @@ -58,6 +68,8 @@ def compare_expression_values( from_expression: MathExpression, to_expression: MathExpression, history: Optional[List[Any]] = None, + env_name: Optional[str] = None, + enforce_unique_vars: bool = True, ) -> None: """Compare and evaluate two expressions to verify they have the same value""" vars_from: Set[str] = set( @@ -70,22 +82,14 @@ def compare_expression_values( for v in to_expression.find_type(VariableExpression) if v.identifier ) - # If there are not the same unique vars in the two expressions, something - # bad happened, and the two expressions can only coincidentally be equal - # in value. - if len(vars_from) != len(vars_to): - raise_with_history( - "Number of variables changed", - f"{list(vars_from)} != {list(vars_to)}", - history, - ) sorted_from = list(vars_from) sorted_from.sort() - sorted_to = list(vars_from) + sorted_to = list(vars_to) sorted_to.sort() - if sorted_from != sorted_to: + # Certain rules like fraction reduction can change the unique variables + if sorted_from != sorted_to and enforce_unique_vars: raise_with_history( "Unique variables changed", f"{sorted_from} != {sorted_to}", @@ -101,15 +105,23 @@ def compare_expression_values( value_to = to_expression.evaluate(eval_context) # Print out the problem steps leading up to error result. - if not math.isclose(value_from, value_to, rel_tol=1e-9, abs_tol=0.0): + if not math.isclose(value_from, value_to, rel_tol=1e-6, abs_tol=0.0): + in_tree_visualize = render_tree_to_text(from_expression) + out_tree_visualize = render_tree_to_text(to_expression) changed = f""" - {from_expression} = {value_from} + IN: {from_expression} = {value_from} - {to_expression} = {value_to} + {in_tree_visualize} - {value_from} != {value_to} + OUT: {to_expression} = {value_to} + + {out_tree_visualize} + + ERROR: {value_from} != {value_to} """ - raise_with_history("Expression value changed", changed, history) + raise_with_history( + f"{env_name or 'unknown env'}: Expression value changed", changed, history + ) def compare_equation_values( @@ -381,58 +393,124 @@ def has_like_terms(expression: MathExpression) -> bool: return False -class FactorResult: - best: NumberType - left: NumberType - right: NumberType - all_left: Dict[NumberType, NumberType] - all_right: Dict[NumberType, NumberType] - variable: Optional[str] - exponent: Optional[NumberType] - leftExponent: Optional[NumberType] - rightExponent: Optional[NumberType] - leftVariable: Optional[str] - rightVariable: Optional[str] +@dataclass +class FractionReductionResult: + numerator: NumberType = 1 + denominator: NumberType = 1 + reduced_variable: Optional[str] = None + reduced_exponent: Optional[NumberType] = None + common_variable: Optional[str] = None + common_exponent: Optional[NumberType] = None - def __init__(self) -> None: - self.best = -1 - self.left = -1 - self.right = -1 - self.all_left = {} - self.all_right = {} - self.variable = None - self.exponent = None - self.leftExponent = None - self.rightExponent = None - self.leftVariable = None - self.rightVariable = None + +@dataclass +class FactorResult: + best: NumberType = -1 + left: NumberType = -1 + right: NumberType = -1 + all_left: Dict[NumberType, NumberType] = field(default_factory=dict) + all_right: Dict[NumberType, NumberType] = field(default_factory=dict) + variable: Optional[str] = None + exponent: Optional[NumberType] = None + leftExponent: Optional[NumberType] = None + rightExponent: Optional[NumberType] = None + leftVariable: Optional[str] = None + rightVariable: Optional[str] = None # Create a term node hierarchy from a given set of # term parameters. This takes into account removing # implicit coefficients of 1 where possible. +def reduce_fraction(num: NumberType, den: NumberType) -> Tuple[NumberType, NumberType]: + """Reduces a fraction to its simplest form""" + + def gcd(a: int, b: int) -> int: + while b: + a, b = b, a % b + return a + + if isinstance(num, int) and isinstance(den, int): + divisor = gcd(abs(num), abs(den)) + return (num // divisor, den // divisor) + return (num, den) + + def make_term( coefficient: NumberType = 1, variable: Optional[str] = None, exponent: Optional[NumberType] = None, ) -> MathExpression: - constExp = ConstantExpression(coefficient) + """Create a term node hierarchy from given parameters""" + # Handle pure constant case if variable is None and exponent is None: - return constExp + return ConstantExpression(coefficient) - varExp = VariableExpression(variable) + # Just a variable if coefficient == 1 and exponent is None: - return varExp + return VariableExpression(variable) - multExp = MultiplyExpression(constExp, varExp) + # Variable with coefficient if exponent is None: - return multExp + if coefficient == 1: + return VariableExpression(variable) + return MultiplyExpression( + ConstantExpression(coefficient), VariableExpression(variable) + ) - expConstExp = ConstantExpression(exponent) + # Variable with exponent + var_exp = PowerExpression( + VariableExpression(variable), ConstantExpression(exponent) + ) if coefficient == 1: - return PowerExpression(varExp, expConstExp) + return var_exp + return MultiplyExpression(ConstantExpression(coefficient), var_exp) + + +def make_term_fractional( + numerator: NumberType = 1, + denominator: NumberType = 1, + variable: Optional[str] = None, + exponent: Optional[NumberType] = None, +) -> MathExpression: + # First reduce the fraction + num, den = reduce_fraction(numerator, denominator) + + # If denominator is 1, we can use the simpler form + if den == 1: + if num == 1 and variable is not None: + if exponent is None or exponent == 1: + return VariableExpression(variable) + return PowerExpression( + VariableExpression(variable), ConstantExpression(exponent) + ) + + base: MathExpression + if variable is not None: + base = MultiplyExpression( + ConstantExpression(num), + VariableExpression(variable), + ) + else: + base = ConstantExpression(num) + + if exponent is None or exponent == 1: + return base + return PowerExpression(base, ConstantExpression(exponent)) - return PowerExpression(multExp, expConstExp) + # For fractions, FIRST create the fractional coefficient + coef = DivideExpression(ConstantExpression(num), ConstantExpression(den)) + + if variable is None: + return coef + + var_term = ( + PowerExpression(VariableExpression(variable), ConstantExpression(exponent)) + if exponent is not None and exponent != 1 + else VariableExpression(variable) + ) + + # Wrap the fraction in parentheses before multiplying with variable + return MultiplyExpression(coef, var_term) class TermResult: @@ -637,6 +715,62 @@ def get_term_ex(node: Optional[MathExpression]) -> Optional[TermEx]: return None +def factor_fraction_terms_ex( + left_term: TermEx, right_term: TermEx +) -> Union[FractionReductionResult, Literal[False]]: + if not left_term or not right_term: + raise ValueError("invalid terms for factoring") + + has_left: bool = left_term.variable is not None + has_right: bool = right_term.variable is not None + + result = FractionReductionResult() + + # Handle coefficients while preserving fractions + left_coef = left_term.coefficient if left_term.coefficient is not None else 1 + right_coef = right_term.coefficient if right_term.coefficient is not None else 1 + + if right_coef == 0: + return False + + result.numerator = left_coef + result.denominator = right_coef + + # Handle variables and exponents + if has_left and has_right and left_term.variable == right_term.variable: + # Both terms have the same variable + result.common_variable = left_term.variable + + left_exp = left_term.exponent if left_term.exponent is not None else 1 + right_exp = right_term.exponent if right_term.exponent is not None else 1 + + reduced_exp = left_exp - right_exp + if reduced_exp == 0: + result.reduced_variable = None + result.reduced_exponent = None + else: + result.reduced_variable = left_term.variable + result.reduced_exponent = reduced_exp + + result.common_exponent = min(left_exp, right_exp) + elif has_left and not has_right: + # Only numerator has a variable - preserve it + result.reduced_variable = left_term.variable + result.reduced_exponent = ( + left_term.exponent if left_term.exponent is not None else 1 + ) + elif not has_left and has_right: + # Only denominator has a variable + result.reduced_variable = right_term.variable + result.reduced_exponent = -( + right_term.exponent if right_term.exponent is not None else 1 + ) + elif not (result.numerator != result.denominator): + return False + + return result + + def factor_add_terms_ex( left_term: TermEx, right_term: TermEx ) -> Union[FactorResult, Literal[False]]: diff --git a/tests/test_parser.py b/tests/test_parser.py index f149e2e..9311d73 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,6 +1,8 @@ from typing import Any + import pytest +from mathy_core.layout import render_tree_to_text from mathy_core.parser import ( ExpressionParser, InvalidExpression, @@ -13,6 +15,10 @@ @pytest.mark.parametrize( "expectation", [ + {"input": "a * (5 + 12)", "output": "a * (5 + 12)"}, + {"input": "(6 / 7)k^3", "output": "(6/7)k^3"}, + {"input": "(1 / 2)x", "output": "(1/2)x"}, + {"input": "(3x^2) / (6x)", "output": "(3x^2) / 6x"}, {"input": "4x * p^(1 + 3) * 12x^2", "output": "4x * p^(1 + 3) * 12x^2"}, { "input": "(-2.257893300159429e+16h^2 * v) * j^4", @@ -31,6 +37,7 @@ def test_parser_to_string(expectation: dict[str, str]) -> None: parser = ExpressionParser() expression = parser.parse(expectation["input"]) + print(render_tree_to_text(expression)) out_str = str(expression) assert out_str == expectation["output"] @@ -45,6 +52,7 @@ def test_parser_factorials() -> None: def test_parser_operator_precedence() -> None: expects: list[dict[str, float | int | str]] = [ + {"input": "(1 / 2) * 4^2", "output": 8}, {"input": "9 / 8 * 9", "output": 10.125}, {"input": "4 + 9 / 8 * 9", "output": 14.125}, ] diff --git a/tests/test_rules.py b/tests/test_rules.py index 14b18df..2cf7c5e 100644 --- a/tests/test_rules.py +++ b/tests/test_rules.py @@ -6,6 +6,7 @@ ConstantsSimplifyRule, DistributiveFactorOutRule, DistributiveMultiplyRule, + FractionReductionRule, MultiplicativeInverseRule, RestateSubtractionRule, VariableMultiplyRule, @@ -55,6 +56,13 @@ def debug(ex): run_rule_tests("restate_subtraction", RestateSubtractionRule, debug) +def test_rules_fraction_reduction(): + def debug(ex): + pass + + run_rule_tests("fraction_reduction", FractionReductionRule, debug) + + def test_rules_multiplicative_inverse(): def debug(ex): pass diff --git a/tests/test_util.py b/tests/test_util.py index fd8d50c..d38b8a5 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,3 +1,6 @@ +import pytest + +from mathy_core.layout import render_tree_to_text from mathy_core.parser import ExpressionParser from mathy_core.util import ( TermEx, @@ -83,6 +86,22 @@ def test_util_has_like_terms(): assert input == input and has_like_terms(expr) == expected +@pytest.mark.parametrize( + "text", + [ + "7 + 4x - 2", + "1/4 + 1/2", + "3x^7 - 4x^(3+1) + 2!", + "g + -x^3 + 4x^3 + 19p^4 + -1y", + "1f + 98i + 3f * 14t - (1/2x)", + ], +) +def test_util_render_tree_to_text(text: str) -> None: + parser = ExpressionParser() + expr = parser.parse(text) + assert render_tree_to_text(expr) is not None + + def test_util_terms_are_like(): parser = ExpressionParser() expr = parser.parse("10 + (7x + 6x)") diff --git a/website/docs/api/problems.md b/website/docs/api/problems.md index f6fe0fd..d0043c9 100644 --- a/website/docs/api/problems.md +++ b/website/docs/api/problems.md @@ -147,7 +147,6 @@ gen_simplify_multiple_terms( num_terms: int, optional_var: bool = False, op: Optional[List[str], str] = None, - common_variables: bool = True, inner_terms_scaling: float = 0.3, powers_probability: float = 0.33, optional_var_probability: float = 0.8, diff --git a/website/docs/api/rule.md b/website/docs/api/rule.md index b6a29ea..2b03cfa 100644 --- a/website/docs/api/rule.md +++ b/website/docs/api/rule.md @@ -51,6 +51,9 @@ Find all nodes in an expression that can have this rule applied to them. Each node is marked with it's token index in the expression, according to the visit strategy, and stored as `node.r_index` starting with index 0 +### maintains_variables +Whether this rule maintains the same variables in the expression. Rules +that change variables should return False. ### name Readable rule name used for debug rendering and description outputs ## ExpressionChangeRule @@ -80,4 +83,4 @@ ExpressionChangeRule.save_parent( ) -> 'ExpressionChangeRule' ``` Note the parent of the node being modified, and set it as the parent of the -rule output automatically. \ No newline at end of file +rule output automatically. diff --git a/website/docs/api/rules/fraction_reduction.md b/website/docs/api/rules/fraction_reduction.md new file mode 100644 index 0000000..581e0b1 --- /dev/null +++ b/website/docs/api/rules/fraction_reduction.md @@ -0,0 +1,63 @@ +```python + +import mathy_core.rules.fraction_reduction +``` +# Fraction Reduction + +The `Fraction Reduction` rule simplifies fractions by canceling out common factors in the numerator and denominator. This transformation reduces the complexity of expressions and produces equivalent fractions in their simplest form. + +This rule implements the mathematical principle that when a factor appears in both the numerator and denominator of a fraction, it can be canceled out: `(a × c) / (b × c) = a / b` + +## Operations + +**Numeric Fraction Reduction** + +This handles cases where the numerator and denominator contain common numeric factors. For example, `6/3` simplifies to `2`. + +**Variable Cancellation** + +When the same variable appears in both the numerator and denominator, the rule cancels them out: + +- If the powers are equal, the variable is removed completely +- If the powers differ, the variable remains with the difference of the exponents + +For example: + +- `(6x) / (3x)` simplifies to `2` +- `(6x^2) / (3x)` simplifies to `2x` + +**Coefficient Reduction** + +The rule also handles coefficient reduction in conjunction with variable cancellation. It first factors out common terms, then reduces the numerical coefficient to its simplest form. + +For example, `(3x^2) / (6x)` simplifies to `(1/2)x`. + +**Exponent Simplification** + +When variables with exponents appear in both the numerator and denominator, the rule simplifies by subtracting the exponents. + +For example, `x^4 / x^2` simplifies to `x^2`. + +## Implementation Details + +The rule creates a reduced fraction by: + +1. Identifying common factors in the numerator and denominator +2. Reducing the numeric coefficients to their simplest form using GCD +3. Handling variable terms by adjusting their exponents appropriately +4. Constructing a new expression with the simplified terms + +### Examples + +`rule_tests:fraction_reduction` + + +## API + + +## FractionReductionRule +```python +FractionReductionRule(self, args, kwargs) +``` +Reduce fractions by cancelling out common factors in the numerator and +denominator. diff --git a/website/docs/api/util.md b/website/docs/api/util.md index e87841f..c7ca5e5 100644 --- a/website/docs/api/util.md +++ b/website/docs/api/util.md @@ -21,6 +21,8 @@ compare_expression_string_values( from_expression: str, to_expression: str, history: Optional[List[Any]] = None, + env_name: Optional[str] = None, + enforce_unique_vars: bool = True, ) -> None ``` Compare and evaluate two expressions strings to verify they have the @@ -31,6 +33,8 @@ compare_expression_values( from_expression: mathy_core.expressions.MathExpression, to_expression: mathy_core.expressions.MathExpression, history: Optional[List[Any]] = None, + env_name: Optional[str] = None, + enforce_unique_vars: bool = True, ) -> None ``` Compare and evaluate two expressions to verify they have the same value @@ -48,6 +52,112 @@ accessible by key. That is, factoring 2 would return 2 : 1 } +## FactorResult +```python +FactorResult( + self, + best: Union[float, int] = -1, + left: Union[float, int] = -1, + right: Union[float, int] = -1, + all_left: Dict[Union[float, int], Union[float, int]] = , + all_right: Dict[Union[float, int], Union[float, int]] = , + variable: Optional[str] = None, + exponent: Optional[float, int] = None, + leftExponent: Optional[float, int] = None, + rightExponent: Optional[float, int] = None, + leftVariable: Optional[str] = None, + rightVariable: Optional[str] = None, +) -> None +``` +FactorResult(best: Union[float, int] = -1, left: Union[float, int] = -1, right: Union[float, int] = -1, all_left: Dict[Union[float, int], Union[float, int]] = , all_right: Dict[Union[float, int], Union[float, int]] = , variable: Optional[str] = None, exponent: Union[float, int, NoneType] = None, leftExponent: Union[float, int, NoneType] = None, rightExponent: Union[float, int, NoneType] = None, leftVariable: Optional[str] = None, rightVariable: Optional[str] = None) +### best +int([x]) -> integer +int(x, base=10) -> integer + +Convert a number or string to an integer, or return 0 if no arguments +are given. If x is a number, return x.__int__(). For floating point +numbers, this truncates towards zero. + +If x is not a number or if base is given, then x must be a string, +bytes, or bytearray instance representing an integer literal in the +given base. The literal can be preceded by '+' or '-' and be surrounded +by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. +Base 0 means to interpret the base from the string as an integer literal. +>>> int('0b100', base=0) +4 +### left +int([x]) -> integer +int(x, base=10) -> integer + +Convert a number or string to an integer, or return 0 if no arguments +are given. If x is a number, return x.__int__(). For floating point +numbers, this truncates towards zero. + +If x is not a number or if base is given, then x must be a string, +bytes, or bytearray instance representing an integer literal in the +given base. The literal can be preceded by '+' or '-' and be surrounded +by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. +Base 0 means to interpret the base from the string as an integer literal. +>>> int('0b100', base=0) +4 +### right +int([x]) -> integer +int(x, base=10) -> integer + +Convert a number or string to an integer, or return 0 if no arguments +are given. If x is a number, return x.__int__(). For floating point +numbers, this truncates towards zero. + +If x is not a number or if base is given, then x must be a string, +bytes, or bytearray instance representing an integer literal in the +given base. The literal can be preceded by '+' or '-' and be surrounded +by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. +Base 0 means to interpret the base from the string as an integer literal. +>>> int('0b100', base=0) +4 +## FractionReductionResult +```python +FractionReductionResult( + self, + numerator: Union[float, int] = 1, + denominator: Union[float, int] = 1, + reduced_variable: Optional[str] = None, + reduced_exponent: Optional[float, int] = None, + common_variable: Optional[str] = None, + common_exponent: Optional[float, int] = None, +) -> None +``` +FractionReductionResult(numerator: Union[float, int] = 1, denominator: Union[float, int] = 1, reduced_variable: Optional[str] = None, reduced_exponent: Union[float, int, NoneType] = None, common_variable: Optional[str] = None, common_exponent: Union[float, int, NoneType] = None) +### denominator +int([x]) -> integer +int(x, base=10) -> integer + +Convert a number or string to an integer, or return 0 if no arguments +are given. If x is a number, return x.__int__(). For floating point +numbers, this truncates towards zero. + +If x is not a number or if base is given, then x must be a string, +bytes, or bytearray instance representing an integer literal in the +given base. The literal can be preceded by '+' or '-' and be surrounded +by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. +Base 0 means to interpret the base from the string as an integer literal. +>>> int('0b100', base=0) +4 +### numerator +int([x]) -> integer +int(x, base=10) -> integer + +Convert a number or string to an integer, or return 0 if no arguments +are given. If x is a number, return x.__int__(). For floating point +numbers, this truncates towards zero. + +If x is not a number or if base is given, then x must be a string, +bytes, or bytearray instance representing an integer literal in the +given base. The literal can be preceded by '+' or '-' and be surrounded +by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. +Base 0 means to interpret the base from the string as an integer literal. +>>> int('0b100', base=0) +4 ## get_term_ex ```python get_term_ex( @@ -140,6 +250,15 @@ __Examples__ - Simple = x^2 * 4 - Complex = 2 * 2x^2 +## make_term +```python +make_term( + coefficient: Union[float, int] = 1, + variable: Optional[str] = None, + exponent: Optional[float, int] = None, +) -> mathy_core.expressions.MathExpression +``` +Create a term node hierarchy from given parameters ## pad_array ```python pad_array(in_list: List[Any], max_length: int, value: Any = 0) -> List[Any] diff --git a/website/mkdocs.yml b/website/mkdocs.yml index 2724ed5..ae41347 100644 --- a/website/mkdocs.yml +++ b/website/mkdocs.yml @@ -5,7 +5,7 @@ site_author: Justin DuJardin site_url: https://core.mathy.ai # Copyright -copyright: Copyright © 2011 - 2024 Justin DuJardin +copyright: Copyright © 2011 - 2025 Justin DuJardin repo_name: mathy/mathy_core repo_url: https://github.com/mathy/mathy_core @@ -34,6 +34,7 @@ nav: - Constants Simplify: api/rules/constants_simplify.md - Distributive Factor Out: api/rules/distributive_factor_out.md - Distributive Multiply Across: api/rules/distributive_multiply_across.md + - Fraction Reduction: api/rules/fraction_reduction.md - Multiplicative Inverse: api/rules/multiplicative_inverse.md - Restate Subtraction: api/rules/restate_subtraction.md - Variable Multiply: api/rules/variable_multiply.md