Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rules): add MultiplicativeInverse rule #19

Merged
merged 10 commits into from
Feb 6, 2024
1 change: 1 addition & 0 deletions mathy_core/problems.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

Utility functions for helping generate input problems.
"""

import random
from dataclasses import dataclass
from typing import Any, List, Optional, Set, Tuple, TypeVar, Union, cast
Expand Down
2 changes: 2 additions & 0 deletions mathy_core/rules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 .multiplicative_inverse import MultiplicativeInverseRule # noqa
from .restate_subtraction import RestateSubtractionRule # noqa
from .variable_multiply import VariableMultiplyRule # noqa

Expand All @@ -14,6 +15,7 @@
"ConstantsSimplifyRule",
"DistributiveFactorOutRule",
"DistributiveMultiplyRule",
"MultiplicativeInverseRule",
"RestateSubtractionRule",
"VariableMultiplyRule",
)
1 change: 1 addition & 0 deletions mathy_core/rules/commutative_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class CommutativeSwapRule(BaseRule):
/ \ / \
a b b a
"""

preferred: bool

def __init__(self, preferred: bool = True):
Expand Down
1 change: 1 addition & 0 deletions mathy_core/rules/distributive_factor_out.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class DistributiveFactorOutRule(BaseRule):
/ \ / \ / \
a b a c b c
"""

constants: bool

def __init__(self, constants: bool = False):
Expand Down
17 changes: 17 additions & 0 deletions mathy_core/rules/multiplicative_inverse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
The `Multiplicative Inverse` rule converts division operations into multiplication by the reciprocal. This transformation can simplify the structure of mathematical expressions and prepare them for further simplification.

This rule is expressed with the equation `a / b = a * (1 / b)`

**Convert Division to Multiplication by Reciprocal**

This handles the `a / b` conversion to `a * (1 / b)`.

**Handle Division by a Negative Denominator**

When the denominator is negative, the rule handles it by negating the numerator and converting the division into multiplication by the positive reciprocal of the denominator.

This handles the `4 / -(2 + 3)` conversion to `4 * -1 / (2 + 3)`

### Examples

`rule_tests:multiplicative_inverse`
77 changes: 77 additions & 0 deletions mathy_core/rules/multiplicative_inverse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from typing import Optional

from ..expressions import (
ConstantExpression,
DivideExpression,
MathExpression,
MultiplyExpression,
NegateExpression,
)
from ..rule import BaseRule, ExpressionChangeRule

_OP_DIVISION_EXPRESSION = "division-expression"
_OP_DIVISION_NEGATIVE_DENOMINATOR = "division-negative-denominator"


class MultiplicativeInverseRule(BaseRule):
"""Convert division operations to multiplication by the reciprocal."""

@property
def name(self) -> str:
return "Multiplicative Inverse"

@property
def code(self) -> str:
return "MI"

def get_type(self, node: MathExpression) -> Optional[str]:
"""Determine the configuration of the tree for this transformation.

Support different types of tree configurations based on the division operation:
- DivisionExpression restated as multiplication by reciprocal
- DivisionNegativeDenominator is a division by a negative term
"""
is_division = isinstance(node, DivideExpression)
if not is_division:
return None

# Division where the denominator is negative (e.g., (2 + 3z) / -z)
if isinstance(node.right, NegateExpression):
return _OP_DIVISION_NEGATIVE_DENOMINATOR

# If none of the above, it's a general division expression
return _OP_DIVISION_EXPRESSION

def can_apply_to(self, node: MathExpression) -> bool:
tree_type = self.get_type(node)
return tree_type is not None

def apply_to(self, node: MathExpression) -> ExpressionChangeRule:
change = super().apply_to(node)
tree_type = self.get_type(node)
assert tree_type is not None, "call can_apply_to before applying a rule"
change.save_parent() # connect result to node.parent

assert node.left is not None, "Division must have a left child"
assert node.right is not None, "Division must have a right child"

# For negative denominator, negate the numerator and use the positive reciprocal
if tree_type == _OP_DIVISION_NEGATIVE_DENOMINATOR:
assert isinstance(
node.right, NegateExpression
), "Right child must be a NegateExpression"
child = node.right.get_child()
assert child is not None, "NegateExpression must have a child"
result = MultiplyExpression(
node.left.clone(),
DivideExpression(ConstantExpression(-1), child.clone()),
)
# Multiply the numerator by the reciprocal of the denominator
else:
result = MultiplyExpression(
node.left.clone(),
DivideExpression(ConstantExpression(1), node.right.clone()),
)

result.set_changed() # mark this node as changed for visualization
return change.done(result)
25 changes: 25 additions & 0 deletions mathy_core/rules/multiplicative_inverse.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"valid": [
{
"input": "4 / -(2 + 3)",
"output": "4 * -1 / (2 + 3)"
},
{
"input": "(21x^3 - 35x^2) / 7x",
"output": "(21x^3 - 35x^2) * 1 / 7x"
},
{
"input": "(x^2 + 4x + 4) / (2x - 2)",
"output": "(x^2 + 4x + 4) * 1 / (2x - 2)"
},
{
"input": "(2 + 3x) / 2x",
"output": "(2 + 3x) * 1 / 2x"
},
{
"input": "((x + 1) / -(y + 2))",
"output": "(x + 1) * -1 / (y + 2)"
}
],
"invalid": []
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ The `Variable Multiplication` rule restates `x^b * x^d` as `x^(b + d)`, which is

!!! note

This rule can only be applied when the nodes have matching variable bases. This means that `x * y` cannot be combined, but `x * x` can be.
This rule can only be applied when the nodes have matching variable bases. This means that `x * y` cannot be combined, but `x * x` can be.

### Transformations

Both implicit and explicit variable powers are recognized in this transformation.

!!! info "Help Wanted"

The current variable multiply rule leaves out a case where there is a power
raised to another power, they can be combined by multiplying the exponents
together.
The current variable multiply rule leaves out a case where there is a power
raised to another power, they can be combined by multiplying the exponents
together.

For example: `x^(2^2) = x^4`

For example: `x^(2^2) = x^4`

If you would like to help out with by updating this rule [open an issue here](https://github.com/justindujardin/mathy/issues/new?title=VariableMultiplyRaisePowerToPower){target=\_blank}
If you would like to help out with by updating this rule [open an issue here](https://github.com/justindujardin/mathy/issues/new?title=VariableMultiplyRaisePowerToPower){target=\_blank}

#### Explicit powers

Expand Down
2 changes: 2 additions & 0 deletions mathy_core/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ def run_rule_tests(
if callback is not None:
callback(ex)
rule = init_rule_for_test(ex, rule_class)
assert rule.name is not None, "Rule must have a name"
assert rule.code is not None, "Rule must have a code"
expression = parser.parse(ex["input"]).clone()
before = expression.clone().get_root()
print(ex)
Expand Down
8 changes: 8 additions & 0 deletions tests/test_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
ConstantsSimplifyRule,
DistributiveFactorOutRule,
DistributiveMultiplyRule,
MultiplicativeInverseRule,
RestateSubtractionRule,
VariableMultiplyRule,
)
Expand Down Expand Up @@ -54,6 +55,13 @@ def debug(ex):
run_rule_tests("restate_subtraction", RestateSubtractionRule, debug)


def test_rules_multiplicative_inverse():
def debug(ex):
pass

run_rule_tests("multiplicative_inverse", MultiplicativeInverseRule, debug)


def test_rules_variable_multiply():
def debug(ex):
pass
Expand Down
2 changes: 2 additions & 0 deletions website/docs/api/rules/distributive_factor_out.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ DistributiveFactorOutRule.get_type(
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
Expand All @@ -82,6 +83,7 @@ Support the three types of tree configurations:
of the child add node is the target.

Structure:

- Simple
* node(add),node.left(term),node.right(term)
- Chained Left
Expand Down
44 changes: 44 additions & 0 deletions website/docs/api/rules/multiplicative_inverse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
```python

import mathy_core.rules.multiplicative_inverse
```
The `Multiplicative Inverse` rule converts division operations into multiplication by the reciprocal. This transformation can simplify the structure of mathematical expressions and prepare them for further simplification.

This rule is expressed with the equation `a / b = a * (1 / b)`

**Convert Division to Multiplication by Reciprocal**

This handles the `a / b` conversion to `a * (1 / b)`.

**Handle Division by a Negative Denominator**

When the denominator is negative, the rule handles it by negating the numerator and converting the division into multiplication by the positive reciprocal of the denominator.

This handles the `4 / -(2 + 3)` conversion to `4 * -1 / (2 + 3)`

### Examples

`rule_tests:multiplicative_inverse`


## API


## MultiplicativeInverseRule
```python
MultiplicativeInverseRule(self, args, kwargs)
```
Convert division operations to multiplication by the reciprocal.
### get_type
```python
MultiplicativeInverseRule.get_type(
self,
node: mathy_core.expressions.MathExpression,
) -> Optional[str]
```
Determine the configuration of the tree for this transformation.

Support different types of tree configurations based on the division operation:
- DivisionExpression restated as multiplication by reciprocal
- DivisionNegativeDenominator is a division by a negative term

66 changes: 66 additions & 0 deletions website/docs/api/rules/variable_multiply.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,71 @@

import mathy_core.rules.variable_multiply
```
The `Variable Multiplication` rule restates `x^b * x^d` as `x^(b + d)`, which isolates the exponents attached to the variables so they can be combined.

!!! note

This rule can only be applied when the nodes have matching variable bases. This means that `x * y` cannot be combined, but `x * x` can be.

### Transformations

Both implicit and explicit variable powers are recognized in this transformation.

!!! info "Help Wanted"

The current variable multiply rule leaves out a case where there is a power
raised to another power, they can be combined by multiplying the exponents
together.

For example: `x^(2^2) = x^4`

If you would like to help out with by updating this rule [open an issue here](https://github.com/justindujardin/mathy/issues/new?title=VariableMultiplyRaisePowerToPower){target=\_blank}

#### Explicit powers

In the simplest case, both variables have explicit exponents.

Examples: `x^b * x^d = x^(b+d)`

- `42x^2 * x^3` becomes `42x^(2 + 3)`
- `x^1 * x^7` becomes `x^(1 + 8)`

```
*
/ \
/ \ ^
/ \ = / \
^ ^ x +
/ \ / \ / \
x b x d b d
```

#### Implicit powers

When not explicitly stated, a variable has an implicit power of being raised to the 1, and this form is identified.

Examples: `x * x^d = x^(1 + d)`

- `42x * x^3` becomes `42x^(1 + 3)`
- `x * x` becomes `x^(1 + 1)`

```
*
/ \
/ \ ^
/ \ = / \
x ^ x +
/ \ / \
x d 1 d
```

### Examples

`rule_tests:variable_multiply`


## API


## VariableMultiplyRule
```python
Expand Down Expand Up @@ -63,3 +128,4 @@ Support two types of tree configurations:
Structure:
- Simple node(mult),node.left(term),node.right(term)
- Chained node(mult),node.left(term),node.right(mult),node.right.left(term)

Loading
Loading