Skip to content

Commit

Permalink
Merge pull request #45 from pie-framework/feat/equivalence-between-in…
Browse files Browse the repository at this point in the history
…equalities-with-variables

feat/equivalence-between-inequalities-with-variables
  • Loading branch information
CarlaCostea authored Oct 29, 2021
2 parents 6311ece + 77514d2 commit c495c52
Show file tree
Hide file tree
Showing 15 changed files with 682 additions and 179 deletions.
Binary file modified .DS_Store
Binary file not shown.
40 changes: 35 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- an api
- a large set of test data
- aims to determine if two mathematical expressions are equal, either having the same form or not
- have a public website where you can add some math and check if it works - also the link should be shareable


Expand All @@ -13,6 +14,7 @@ yarn demo

Then go to `http://localhost:$PORT/demo`


## tests

```shell
Expand All @@ -27,9 +29,9 @@ There is one test that runs fixture data located here: `src/fixtures/latex-equal
yarn jest src/__tests__/latex-equal.spec.ts -t src/fixtures/latex-equal/7119.ts --reporters default
```


## Next Steps

* Check that we have a test case for any outstanding jira tickets relating to math-validation.
* Check that api we expose will support what is needed.
* Do triage on the test failures, add a note to failing test so we can build a picture of the work needed

Expand All @@ -52,27 +54,55 @@ yarn jest src/__tests__/latex-equal.spec.ts -t src/fixtures/latex-equal/7119.ts
* more advanced literal validation (todo)
* block input that is clearly too large/unrelated (eg: a user can type in gobbledy-gook - we should just abort if we see that)


## Capabilities

It can determine mathematical equivalence between:

- linear equations in one variable
- linear equations in two variables
- 2-way inequalities in one or two variables
- compound inequalities in one variable
- trigonometric identities and functions
- inverse trigonometric functions
- similar notation for logarithms and based logarithms

It can also handle degrees, radians and gradians


### things that'd be great (but we may have to park until we have more time)

* a faster latex parser
* faster math evaluation


## modes

There are 2 modes - literal and symbolic

Literal: needs to more advanced than the legacy literal implementation which was essentially a string check.
Literal: is at its most basic a tuned version of a string validation

By default - ignores spaces and parentheses as long as they do not change the meaning of operations (ex. “a+7 +b” will validate against “ ((a) + (7))+b ”)
- ignores leading zeros: “0.1” will validate against “.1”
- accepts commas for decimal marks. For example “1,000” will be equivalent with 1000

Literal Validation offers two configuration options that can be used to validate some variety of forms for an expression:

Ignore trailing zeros option; allows the evaluation to accept zeros to the right of the decimal place “4.5” will validate against “4.50000000000000”
Ignore order option; makes validation indifferent to the variables order, as long as it does not change operations meaning. In this case “a+7 +b*c” will validate against “7 + a+bc”, but not against “ac+7+b”; without it “a+7 +b” will not validate against “7 + a+b”

Symbolic: attempts to decide if expressions are mathematically equivalent or not

By default, it offers all configurations presented for literal validation, exceeding it by quite a lot
In order to check equivalence between 2 expressions, we have to reduce both expressions to the simplest one. Then distribute all coefficients, combine any like terms on each side of the expression, and arrange them in the same order.

Symbolic:

### Notes

* `@babel/runtime` is a devDependency if you ever need to link this repo to another package for testing

## TODO
* strip logs on compile
* set up api that is compatible w/ ui component options
* start going through the tests, build up literal + symbolic a bit att the start
* derivatives kind of work and kind of not - how to use?

### CI
Expand Down
34 changes: 34 additions & 0 deletions src/__tests__/compare-compound-inequations.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { AstToMathJs } from "../conversion/ast-to-mathjs";
import { LatexToAst } from "../conversion/latex-to-ast";

import { splitInequality } from "../symbolic/compare-compound-inequations";

const lta = new LatexToAst();
const atm = new AstToMathJs();

describe("splitInequality", () => {
it.each`
compoundInequality | leftPart | rightPart
${"20>x>7"} | ${"20>x"} | ${"x>7"}
${" 5 ≥ -4 + x > -1 - 1"} | ${" 5 ≥ -4 + x"} | ${"-4 + x > -1 - 1"}
${"x/x<x+y≤1.5*2"} | ${"x/x<x+y"} | ${"x+y≤1.5*2"}
${"2 < 4x > 20"} | ${"2<4x"} | ${"4x>20"}
${"-3 < 2x+5 < 17"} | ${"-3<2x+5"} | ${"2x+5 < 17"}
${"-3 = 6x = -3"} | ${"-3 = 6x"} | ${"6x = -3"}
${"a≥b≥c "} | ${"a≥b"} | ${"b≥c"}
${"a+2≥b-10≥c-100 "} | ${"a+2≥b-10"} | ${"b-10≥c-100"}
${"a≠b≠c "} | ${"a≠b"} | ${"b≠c"}
`(
"$compoundInequality => $leftPart, $rightPart",
({ compoundInequality, leftPart, rightPart }) => {
const inequality = atm.convert(lta.convert(compoundInequality));
const broken = splitInequality(inequality);

const leftSide = atm.convert(lta.convert(leftPart));
const rightSide = atm.convert(lta.convert(rightPart));

expect(broken.left).toEqual(leftSide);
expect(broken.right).toEqual(rightSide);
}
);
});
Original file line number Diff line number Diff line change
@@ -1,18 +1,77 @@
import { AstToMathJs } from "../conversion/ast-to-mathjs";
import { LatexToAst } from "../conversion/latex-to-ast";
import { simplify } from "../symbolic";

import {
getUnknowns,
getVariables,
getCoefficients,
setXToOne,
solveLinearEquation,
} from "../symbolic/compare-equations";
expressionsCanBeCompared,
transformEqualityInExpression,
} from "../symbolic/utils";

const lta = new LatexToAst();
const atm = new AstToMathJs();

describe("getUnknowns", () => {
describe("expressionsCanBeCompared", () => {
it('equations: "x = x" and "2=2" - should return false: equations can not be compared because second equation does not have a variable', () => {
const firstEquation = atm.convert(lta.convert("x=x"));
const secondEquation = atm.convert(lta.convert("2=2"));
const result = expressionsCanBeCompared(firstEquation, secondEquation);

expect(result).toEqual(false);
});

it('equations: "x = x" and "\\log x=2" - should return false: equations can not be compared because second equation contains a function', () => {
const firstEquation = atm.convert(lta.convert("x=x"));
const secondEquation = atm.convert(lta.convert("\\log x=2"));
const result = expressionsCanBeCompared(firstEquation, secondEquation);

expect(result).toEqual(false);
});

it('equations: "5z = 0" and "2y+3=m" - should return true: both equations have variables and does not contain functions', () => {
const firstEquation = atm.convert(lta.convert("x=x"));
const secondEquation = atm.convert(lta.convert("2y+3=m"));
const result = expressionsCanBeCompared(firstEquation, secondEquation);

expect(result).toEqual(true);
});

it('equations: "x" and "y" - should return true: both expressions have variables and does not contain functions', () => {
const firstEquation = atm.convert(lta.convert("x"));
const secondEquation = atm.convert(lta.convert("y"));
const result = expressionsCanBeCompared(firstEquation, secondEquation);

expect(result).toEqual(true);
});
});

describe("transformEqualityInExpression", () => {
it.each`
expression | unknowns
equation | transformedExpression
${"x+5= 2x+3"} | ${"2-x"}
${"5-2(3-m)= 4m+10"} | ${"-5-4m-2(3-m)"}
${"a=2b+3"} | ${"a-2b-3"}
`(
"$equation => $transformedExpression",
({ equation, transformedExpression }) => {
const equationToTransform = atm.convert(lta.convert(equation));
const expression = simplify(
atm.convert(lta.convert(transformedExpression))
);

const result = transformEqualityInExpression(equationToTransform);

expect(result.equals(expression)).toEqual(true);
}
);
});

describe("getVariables", () => {
it.each`
expression | variables
${"x"} | ${["x"]}
${"x +1"} | ${["x"]}
${"((x^2 + x) / x) - 1"} | ${["x"]}
Expand All @@ -21,30 +80,31 @@ describe("getUnknowns", () => {
${"((y^2 + z) / x) - 1"} | ${["x", "y", "z"]}
${"109h"} | ${["h"]}
${"m+n+10"} | ${["m", "n"]}
`("$expression => $unknowns", ({ expression, unknowns }) => {
`("$expression => $variables", ({ expression, variables }) => {
const equation = atm.convert(lta.convert(expression));
const unknownsName = getUnknowns(equation);
const variablesName = getVariables(equation);

expect(unknownsName).toEqual(unknowns);
expect(variablesName).toEqual(variables);
});
});

describe("getCoefficients", () => {
it.each`
expression | coefficients
${"x+0"} | ${[0, 1]}
${"2x^2 = 2x"} | ${[1, 0]}
${"x +1"} | ${[1, 1]}
${"((x^2 + x) / x) - 1"} | ${[0, 0, 1]}
${"1+2"} | ${[1, 0]}
${"a +1+c"} | ${[1, 0]}
${"1+2"} | ${[]}
${"a +1+c"} | ${[]}
${"y^2+5y - 1"} | ${[-1, 5, 1]}
${"2y^2+4y"} | ${[0, 4, 2]}
${"109h"} | ${[0, 109]}
${"m+n+10"} | ${[1, 0]}
${"m+n+10"} | ${[]}
${"x-x"} | ${[0, 0]}
${"x + 5 - 3 + x - 6 - x + 2"} | ${[-2, 1]}
${"2x-x"} | ${[0, 1]}
${"x - x - 2"} | ${[1, 0]}
${"x - x - 2"} | ${[]}
`("$expression => $coefficients", ({ expression, coefficients }) => {
const equation = atm.convert(lta.convert(expression));
const coefficientsList = getCoefficients(equation);
Expand All @@ -61,11 +121,11 @@ describe("getCoefficients", () => {
expect(coefficientsList).toEqual([0, 0]);
});

it('equation: "1 = -2" - if equation has no coefficient for x it will return coefficients [1, 0]', () => {
it('equation: "1 = -2" - if equation has no coefficient for x but can be rationalized it will return an empty array', () => {
const equation = atm.convert(lta.convert("1+2"));
const coefficientsList = getCoefficients(equation);

expect(coefficientsList).toEqual([1, 0]);
expect(coefficientsList).toEqual([]);
});

it('equation: "m + n = - 2" - if equation has more than one variable, will return coefficients [1, 0]', () => {
Expand Down Expand Up @@ -148,7 +208,7 @@ describe("solveLinearEquation", () => {
expect(result).toEqual(-Infinity);
});

it('equation: "2x^2 = 2x" - has no solution', () => {
it('equation: "2y^2+4y" - solution is -2', () => {
const coefficients = [0, 4, 2];
const result = solveLinearEquation(coefficients);

Expand Down
11 changes: 9 additions & 2 deletions src/conversion/latex-to-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,10 +298,10 @@ export const latex_rules = [
["\\\\lnot(?![a-zA-Z])", "NOT"],

["=", "="],
["≠", "NE"],
["\\\\neq(?![a-zA-Z])", "NE"],
["\\\\ne(?![a-zA-Z])", "NE"],
["\\\\not\\s*=", "NE"],
["≠", "NE"],
["\\\\leq(?![a-zA-Z])", "LE"],
["\\\\le(?![a-zA-Z])", "LE"],
["\\\\geq(?![a-zA-Z])", "GE"],
Expand Down Expand Up @@ -689,7 +689,11 @@ export class LatexToAst {
var lhs = this.expression(params);

let relationalToken = (token) =>
token === "<" || token === "LE" || token === ">" || token === "GE";
token === "<" ||
token === "LE" ||
token === ">" ||
token === "GE" ||
token === "NE";

while (
this.token.token_type === "=" ||
Expand Down Expand Up @@ -733,6 +737,9 @@ export class LatexToAst {
case "GE":
case "ge":
return "largerEq";
case "NE":
case "ne":
return "unequal";
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/difference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const log = debug("difference");
export const differenceIsTooGreat = (a: RawAst, b: RawAst) => {
const smallest = Math.min(a.toString().length, b.toString().length);
const biggest = Math.max(a.toString().length, b.toString().length);
const errorAcceptance = 6;
const errorAcceptance = 17;
const limit = (1 / smallest) * 100 + 10 + errorAcceptance;
const diff = biggest - smallest;

Expand Down
Loading

0 comments on commit c495c52

Please sign in to comment.