Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ read first.
| **Silent** | `limit(i, i, 0)` | unevaluated | `0` |
| **Silent** | `integral(i, i)` | `-1/2 + C` | `i_1 ^ 2 / 2 + C` |
| | `lambda(i, i + 1)` | `InvalidArgumentParseException` | the lambda |
| **Silent** | `MathS.ToSympyCode` for any set, lambda, piecewise or non-vector matrix | Python that does not run — `NameError`, `SyntaxError`, `TypeError`, or an exception out of the exporter | Python that runs |
| **Silent** | `"{ k : k > 0 }".ToEntity().FreeVariables` — and `Vars`, and `VarsAndConsts` | `{ %1 }`, a name in no expression | `{ }`, `{ k }`, `{ k }` |
| **Silent** | the symbolic determinant of a matrix, substituted where a pivot vanishes — `[[0,1,2],[3,4,5],[6,7,8]]` through `[[a,b,c],[d,e,f],[g,h,i]]` | `NaN` | `0` |
| **Silent** | `((Entity.Matrix)"[[x, 1], [2, y]]".ToEntity()).Determinant.Simplify()` | `x * y - 2 provided not x = 0` | `x * y - 2` |
Expand All @@ -58,6 +59,66 @@ read first.
| **Silent** | `integral(x, [x, y]T)` | `[[C + x ^ 2, C + x * y]]` | `[[x ^ 2 / 2 + C, x * y + C]]` |
| **Silent** | `derivative(e ^ 2, e)`, over a named constant | `0` | `2 * e` |

### `ToSympyCode` emits Python that runs

`MathS.ToSympyCode` is documented as producing code you can run in SymPy. For every set, every
lambda, every `piecewise` and every non-vector matrix, it did not — and the failure was **silent**
in the sense that matters here: the string came back looking like Python.

The preamble is `import sympy` and nothing else, so every SymPy name in the body has to be
qualified. Six exports were not, and a seventh threw before it got that far:

```python
expr = FiniteSet(1, 2) # was: NameError: name 'FiniteSet' is not defined
expr = Interval(0, 1, ...) # was: NameError: name 'Interval' is not defined
expr = Union(FiniteSet(1, 2), FiniteSet(3)) # was: NameError: name 'Union' is not defined
expr = x in S.Reals # was: NameError: name 'S' is not defined
expr = sympy.Lambda(x, ) # was: TypeError: missing 1 required positional argument
```

| input | was | now |
|---|---|---|
| `{ 1, 2 }` | `FiniteSet(1, 2)` | `sympy.FiniteSet(1, 2)` |
| `ZZ` | `S.Integers` | `sympy.S.Integers` |
| `{1,2} \/ {3}` | `Union(FiniteSet(1, 2), FiniteSet(3))` | `sympy.Union(sympy.FiniteSet(1, 2), sympy.FiniteSet(3))` |
| `lambda(x, sin(x) + 1)` | `sympy.Lambda(x, )` | `sympy.Lambda(x, sympy.sin(x) + 1)` |
| `{ x : x > 0 }` | `AngouriBugException` out of the exporter | `sympy.ConditionSet(x, x > 0, sympy.S.UniversalSet)` |
| `[sin(a); b]` | `Interval(sin(a), b, ...)` | `sympy.Interval(sympy.sin(a), b, ...)` |
| `piecewise(sin(x) provided x > 0, …)` | `sympy.Piecewise((sin(x), x > 0), …)` | `sympy.Piecewise((sympy.sin(x), x > 0), …)` |
| `[[sin(a), b], [c, d]]` | `sympy.ImmutableMatrix([[sin(a), b], …])` | `sympy.ImmutableMatrix([[sympy.sin(a), b], …])` |
| `x in RR` | `x in S.Reals` | `(sympy.S.Reals).contains(x)` |

Three separate faults, all of them invisible to the tests that were there:

- **Unqualified names.** `import sympy` binds `sympy`, not `FiniteSet` or `S`.
- **Parts interpolated rather than exported.** `Interval`, `Piecewise` and the non-vector `Matrix`
wrote their children with `{Left}` instead of `{Left.ToSymPy()}`. A bare variable spells the same
in both languages, so it only shows once the part is a function — `sin(a)`, which Python does not
have.
- **A set builder threw.** `ConditionalSet.Codomain` is `Domain.Any`, which `SpecialSet.Create` has
no member for, so the exporter's cast raised `AngouriBugException` — "please report about it to
the official repository", which is [#985](https://github.com/asc-community/AngouriMath/issues/985).
SymPy names that set: `S.UniversalSet`, which is the one it prints `ConditionSet` *without* a
third argument for.

`x in RR` also changes shape rather than just qualification. Python's `in` coerces its result to a
`bool`, and a membership that is not decided is not one: `x in sympy.S.Reals` raises
`TypeError: did not evaluate to a bool: (-oo < x) & (x < oo)`. `.contains` answers with the
condition, and still answers `True` or `False` where it can.

**Why the tests passed.** They asserted substring containment — `Assert.Contains("Piecewise((a, b),
(c, d))")` — which holds whether the parts were exported or interpolated, and holds on a program
that does not run at all. The new cases pin the whole emitted expression, and one of them asserts
every SymPy name carries its qualifier.

Measured with `work/sympycheck`, which executes the generated program rather than reading it: of 45
emitted programs the corpus now covers, **43 run** and 0 return an inexact value. The two that do
not are the set builders, whose preamble still declares the placeholder that
[#989](https://github.com/asc-community/AngouriMath/issues/989) is about — `%1 = sympy.Symbol('%1')`
is a `SyntaxError` whatever the body says. Their `expr` line is correct here, and with #989's fix
in as well, 45 of 45 run. Suite 7385 passed, 0 failed; corpus unchanged at 116/119 with 0 wrong.
[#985](https://github.com/asc-community/AngouriMath/issues/985).

### A set builder's internal placeholder no longer escapes into `Vars` or `FreeVariables`

`ConditionalSet.DirectChildren` is its predicate with the bound name renamed to a fresh one, so that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,45 +16,54 @@ partial record Set
partial record FiniteSet
{
internal override string ToSymPy()
=> $"FiniteSet({string.Join(", ", Elements.Select(c => c.ToSymPy()))})";
=> $"sympy.FiniteSet({string.Join(", ", Elements.Select(c => c.ToSymPy()))})";
}

partial record Interval
{
internal override string ToSymPy()
=> $"Interval({Left}, {Right}, left_open={((Boolean)!LeftClosed).ToSymPy()}, right_open={((Boolean)!RightClosed).ToSymPy()})";
=> $"sympy.Interval({Left.ToSymPy()}, {Right.ToSymPy()}, left_open={((Boolean)!LeftClosed).ToSymPy()}, right_open={((Boolean)!RightClosed).ToSymPy()})";
}

partial record ConditionalSet
{
// A set builder's Codomain is Domain.Any, which SpecialSet.Create has no
// member for and throws on -- so every set builder threw an AngouriBugException
// out of the exporter. SymPy names that set: S.UniversalSet is the one it
// prints ConditionSet without a third argument for, which is what "no
// restriction beyond the predicate" means here too.
// https://github.com/asc-community/AngouriMath/issues/985
internal override string ToSymPy()
=> $"ConditionSet({Var.ToSymPy()}, {Predicate.ToSymPy()}, {((SpecialSet)Codomain).ToSymPy()})";
=> $"sympy.ConditionSet({Var.ToSymPy()}, {Predicate.ToSymPy()}, {CodomainToSymPy()})";

private string CodomainToSymPy()
=> Codomain is Domain.Any ? "sympy.S.UniversalSet" : SpecialSet.Create(Codomain).ToSymPy();
}

partial record SpecialSet
{
partial record Integers
{
internal override string ToSymPy()
=> "S.Integers";
=> "sympy.S.Integers";
}

partial record Rationals
{
internal override string ToSymPy()
=> "S.Rationals";
=> "sympy.S.Rationals";
}

partial record Reals
{
internal override string ToSymPy()
=> "S.Reals";
=> "sympy.S.Reals";
}

partial record Complexes
{
internal override string ToSymPy()
=> "S.Complexes";
=> "sympy.S.Complexes";
}

internal override string ToSymPy()
Expand All @@ -64,25 +73,30 @@ internal override string ToSymPy()
partial record Unionf
{
internal override string ToSymPy()
=> $"Union({Left.ToSymPy()}, {Right.ToSymPy()})";
=> $"sympy.Union({Left.ToSymPy()}, {Right.ToSymPy()})";
}

partial record Intersectionf
{
internal override string ToSymPy()
=> $"Intersection({Left.ToSymPy()}, {Right.ToSymPy()})";
=> $"sympy.Intersection({Left.ToSymPy()}, {Right.ToSymPy()})";
}

partial record SetMinusf
{
internal override string ToSymPy()
=> $"Complement({Left.ToSymPy()}, {Right.ToSymPy()})";
=> $"sympy.Complement({Left.ToSymPy()}, {Right.ToSymPy()})";
}

partial record Inf
{
// Python's `in` forces its result to a bool, and a membership that is not
// decided is not one: `x in sympy.S.Reals` raises "did not evaluate to a
// bool: (-oo < x) & (x < oo)". `.contains` is the form that answers with the
// condition instead, and it still answers True or False when it can.
// https://github.com/asc-community/AngouriMath/issues/985
internal override string ToSymPy()
=> $"{Element.ToSymPy(Element.Priority < Priority)} in {SupSet.ToSymPy(SupSet.Priority < Priority)}";
=> $"({SupSet.ToSymPy()}).contains({Element.ToSymPy()})";
}
}

Expand All @@ -96,7 +110,7 @@ partial record Providedf

partial record Piecewise
{
internal override string ToSymPy() => $"sympy.Piecewise({string.Join(", ", Cases.Select(c => $"({c.Expression}, {c.Predicate})"))})";
internal override string ToSymPy() => $"sympy.Piecewise({string.Join(", ", Cases.Select(c => $"({c.Expression.ToSymPy()}, {c.Predicate.ToSymPy()})"))})";
}

partial record Matrix
Expand All @@ -107,7 +121,7 @@ internal override string ToSymPy()
IsVector switch
{
true => this.Select(c => c.ToSymPy()),
false => this.Select(c => $"[{string.Join(", ", ((Matrix)c).T)}]"),
false => this.Select(c => $"[{string.Join(", ", ((Matrix)c).T.Select(e => e.ToSymPy()))}]"),
}) +
"])";
}
Expand All @@ -123,7 +137,7 @@ internal override string ToSymPy()
partial record Lambda
{
internal override string ToSymPy()
=> $"sympy.Lambda({Parameter.ToSymPy()}, )";
=> $"sympy.Lambda({Parameter.ToSymPy()}, {Body.ToSymPy()})";
}
}
}
72 changes: 69 additions & 3 deletions Sources/Tests/UnitTests/Convenience/ToSymPyTEst.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ public class ToSymPyTest
[InlineData(@"A \/ B", "Union(A, B)")]
[InlineData(@"A /\ B", "Intersection(A, B)")]
[InlineData(@"A \ B", "Complement(A, B)")]
[InlineData(@"a in B", "a in B")]
[InlineData("domain({ x : x > 0 }, RR)", "ConditionSet(x, x > 0, S.Reals)")]
// both of these recorded the old output, and neither of those outputs ran:
// `a in B` raises rather than answering, and `ConditionSet`/`S` are unqualified.
// https://github.com/asc-community/AngouriMath/issues/985
[InlineData(@"a in B", "(B).contains(a)")]
[InlineData("domain({ x : x > 0 }, RR)", "sympy.ConditionSet(x, x > 0, sympy.S.Reals)")]
[InlineData("sec(x)", "sympy.sec(x)")]
[InlineData("csc(x)", "sympy.csc(x)")]
[InlineData("arcsec(x)", "sympy.asec(x)")]
Expand All @@ -91,6 +94,69 @@ public void TestSymPy(string expression, string expectedToBeIn, bool contains =
else
Assert.DoesNotContain(expectedToBeIn, MathS.ToSympyCode(ent));
}

// Substring containment is what let these ship. `Assert.Contains("Piecewise((a, b),
// (c, d))")` passes whether the parts were exported or merely interpolated, because a
// bare variable spells the same in both languages -- and it passes on a program that
// does not run at all, because `import sympy` alone binds neither `FiniteSet` nor `S`.
// These pin the whole emitted expression instead. The harness that actually executes
// it is `work/sympycheck` in the analysis workspace.
// https://github.com/asc-community/AngouriMath/issues/985
[Theory]
// named without the qualifier the preamble's lone `import sympy` requires: NameError
[InlineData("{ 1, 2 }", "sympy.FiniteSet(1, 2)")]
[InlineData("[0; 1]", "sympy.Interval(0, 1, left_open=False, right_open=False)")]
[InlineData("ZZ", "sympy.S.Integers")]
[InlineData(@"{1,2} \/ {3}", "sympy.Union(sympy.FiniteSet(1, 2), sympy.FiniteSet(3))")]
[InlineData(@"{1,2} /\ {2}", "sympy.Intersection(sympy.FiniteSet(1, 2), sympy.FiniteSet(2))")]
[InlineData(@"{1,2} \ {2}", "sympy.Complement(sympy.FiniteSet(1, 2), sympy.FiniteSet(2))")]
// a body that was never emitted: sympy.Lambda(x, ) is a SyntaxError
[InlineData("lambda(x, sin(x) + 1)", "sympy.Lambda(x, sympy.sin(x) + 1)")]
// a set builder threw AngouriBugException out of the exporter, because its codomain is
// Domain.Any and SpecialSet.Create has no member for it
[InlineData("{ x : x > 0 }", "sympy.ConditionSet(x, x > 0, sympy.S.UniversalSet)")]
// parts interpolated as this library spells them rather than as SymPy does -- visible
// only once the part is a function, since a bare name spells the same either way
[InlineData("[sin(a); b]", "sympy.Interval(sympy.sin(a), b, left_open=False, right_open=False)")]
[InlineData("piecewise(sin(x) provided x > 0, 1 provided x < 0)",
"sympy.Piecewise((sympy.sin(x), x > 0), (1, x < 0))")]
[InlineData("[[sin(a), b], [c, d]]", "sympy.ImmutableMatrix([[sympy.sin(a), b], [c, d]])")]
// Python's `in` forces a bool, so `x in sympy.S.Reals` raises rather than answering
// with the condition
[InlineData("x in RR", "(sympy.S.Reals).contains(x)")]
public void TheWholeEmittedExpressionIsWhatItShouldBe(string expression, string expected)
=> Assert.Equal(expected, EmittedExpressionOf(MathS.FromString(expression)));

private static string EmittedExpressionOf(Entity entity)
{
var code = MathS.ToSympyCode(entity);
var at = code.LastIndexOf("expr = ", System.StringComparison.Ordinal);
return code.Substring(at + "expr = ".Length).Trim();
}

// The preamble is `import sympy` and nothing else, so every SymPy name in the body has
// to carry the qualifier. Nothing was checking that, and six exports did not.
[Theory]
[InlineData("{ 1, 2 }")]
[InlineData("[0; 1]")]
[InlineData("RR")]
[InlineData(@"{1,2} \/ {3}")]
[InlineData("x in RR")]
[InlineData("lambda(x, sin(x))")]
[InlineData("piecewise(sin(x) provided x > 0, 1 provided x < 0)")]
public void EverySymPyNameIsQualified(string expression)
{
var code = EmittedExpressionOf(MathS.FromString(expression));
foreach (var name in new[]
{ "FiniteSet", "Interval", "Union", "Intersection", "Complement",
"ConditionSet", "Lambda", "Piecewise", "ImmutableMatrix", "S.", "sin" })
foreach (System.Text.RegularExpressions.Match match in
System.Text.RegularExpressions.Regex.Matches(
code, System.Text.RegularExpressions.Regex.Escape(name)))
Assert.True(
match.Index >= "sympy.".Length
&& code.Substring(match.Index - "sympy.".Length, "sympy.".Length) == "sympy.",
$"`{name}` is unqualified in `{code}`");
}
}
}

Loading