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
13 changes: 13 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,19 @@ its rows in the order it finds them, which can differ from the eliminator's
| `MathS.Equations("x*y", "x + y^2 - 1").Solve("x", "y")` | `[[1, 0]]` | `[[1, 0], [0, 1], [0, -1]]` |
| `MathS.Equations("x*y + 3*x - 5*y - 15", "x^2 - 3*x - 4*y - 10").Solve("x", "y")` | `[[1, -3], [2, -3]]` | `[[5, 0], [1, -3], [2, -3]]` |

### A squared sine and cosine with one coefficient are found anywhere in a sum

**Silent: an identity reaches its value however the sum is written.** `k sin(x)^2 + k cos(x)^2` was reduced to `k`
only where the two terms were siblings, so the order a sum was sorted into decided: with the constant `2` sorted
between them, `2 + (-2) cos(x)^2 + (-2) sin(x)^2` stayed as written while `2 - 2 cos(x)^2 - 2 sin(x)^2` was `0`. The
Pythagorean identity with a coefficient is now a rule of its own, found wherever the two terms sit
([#2133](https://github.com/asc-community/AngouriMath/issues/2133)).

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"2 + (-2)*cos(x)^2 + (-2)*sin(x)^2".Simplify()` | `2 + (-2) * cos(x) ^ 2 + (-2) * sin(x) ^ 2` | `0` |
| `"2 + (-2) * cos(2 * x) + (-4) * sin(x) ^ 2".Simplify()` | unchanged | `0` |

### A lambda applied to a vector multiplies by it as a vector

**Silent: a matrix of vectors becomes the product.** Applying a lambda simplified its body first, and a simplified
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,32 @@ internal static class MatchedRules
// "x" is bound by the first part and re-matched by the second, so the two terms
// are required to be about the same argument rather than merely both squared.
MatchPattern.Node<Sumf>(MatchPattern.Exact(Integer.Create(1)), MatchPattern.Any("rest")),
Soundness.Sound));
Soundness.Sound),

// ... + k sin(x)^2 + ... + k cos(x)^2 + ... -> k + (everything else)
// With a coefficient the pair was left to SharedFactor, which sees two siblings
// only, so the sort decided: y + (-2) cos(x)^2 + (-2) sin(x)^2 came out y - 2, and
// 2 + (-2) cos(x)^2 + (-2) sin(x)^2, where the constant sorts between them, stayed.
// "k" is bound by the first part and re-matched by the second, so the coefficients
// are required to be the same, as "x" requires the arguments to be.
// https://github.com/asc-community/AngouriMath/issues/2133
new MatchedRule(
"squared-sine-and-cosine-with-one-coefficient-sum-to-it",
MatchPattern.Gathered<Sumf>(
"rest",
MatchPattern.Commutative<Mulf>(
MatchPattern.Any("k"),
MatchPattern.Node<Powf>(
MatchPattern.Node<Sinf>(MatchPattern.Any("x")),
MatchPattern.Exact(Integer.Create(2)))),
MatchPattern.Commutative<Mulf>(
MatchPattern.Any("k"),
MatchPattern.Node<Powf>(
MatchPattern.Node<Cosf>(MatchPattern.Any("x")),
MatchPattern.Exact(Integer.Create(2))))),
MatchPattern.Node<Sumf>(MatchPattern.Any("k"), MatchPattern.Any("rest")),
Soundness.Sound,
description: "k sin(x)^2 + k cos(x)^2 = k"));

/// <summary>
/// <see cref="Functions.Patterns.InvertNegativePowers"/>, as data.
Expand Down
4 changes: 2 additions & 2 deletions Sources/AngouriMath/Core/Transformations/Saturation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ internal static class Saturation
/// one, and that is a measurement rather than a preference.</b> It reads like the value a
/// ceiling should refuse — it means the rule's growth was not judged, because its
/// replacement is code rather than a written pattern, so admitting it accepts a rewrite
/// nobody measured. But it is where the rules are: of the 337 rules in
/// <see cref="MatchedRules"/>, <b>126 collect, 49 rearrange, 36 expand and 126 are
/// nobody measured. But it is where the rules are: of the 338 rules in
/// <see cref="MatchedRules"/>, <b>127 collect, 49 rearrange, 36 expand and 126 are
/// unjudged</b>. A ceiling that refuses the fourth value still refuses 37% of the library,
/// and what is left could not do much when this was written — over six expression pairs
/// equal only through a larger intermediate form, the
Expand Down
15 changes: 8 additions & 7 deletions Sources/AngouriMath/Docs/Contributing/WritingARule.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ effect". This is how to supply those seven things.
## Where a rule goes

`Core/Transformations/Matching/MatchedRules.cs`, as a value in a `MatchedRuleSet`. **33** sets and
**337** rules live there today.
**338** rules live there today.

The `switch` statements in `Functions/Simplification/Patterns` are the older form. **All thirty
registered sets now run as data and describe what they run**; none executes its `switch` any more.
Expand Down Expand Up @@ -64,7 +64,7 @@ in a name and putting the identity in brackets after it:
2. Tangent is sine over cosine (tan(a) = sin(a) / cos(a)), so tan(x) becomes sin(x) / cos(x).
```

So the name has to be a clause that survives being read that way. **All 308 distinct rule names are,
So the name has to be a clause that survives being read that way. **All 309 distinct rule names are,
and `StepAsASentenceTest` holds them to it** — a name with a capital, a bracket or an underscore
fails that test rather than degrading the prose quietly.

Expand All @@ -91,7 +91,7 @@ debugged for an afternoon.
| `Left.ToString()` | `Divf(var a, Divf(var b, var c))` — how the matcher spells it |

Write the identity with `=`, not `->`: it is an equality, and the arrow belongs to the direction the
rule happens to be applied in. **334** rules carry one today; a new rule should.
rule happens to be applied in. **335** rules carry one today; a new rule should.

## The pattern language

Expand Down Expand Up @@ -123,13 +123,14 @@ matches and silently builds nothing. `BuildableNodeTypesTest` is the list.

## Replacement: a pattern, or code

**38** of the 337 rules have a pattern on both sides. The rest build their answer in code, and both
**39** of the 338 rules have a pattern on both sides. The rest build their answer in code, and both
are legitimate — but the choice costs two things, so make it deliberately.

A pattern replacement gets:

- **a direction.** `MatchedRule.Reversed` is the rule read the other way, and **34** of the 38
two-sided rules have one. The other four forget something: `sin²+cos² = 1` forgets the angle,
- **a direction.** `MatchedRule.Reversed` is the rule read the other way, and **34** of the 39
two-sided rules have one. The other five forget something: `sin²+cos² = 1` and
`k sin² + k cos² = k` forget the angle,
`{ x : x in S } = S` forgets the name the set builder bound, and `c ^ log(c, a) = a` and
`c ^ (k * log(c, a)) = a ^ k` for a constant `c` forget which constant, so none has anything to
read back.
Expand Down Expand Up @@ -226,7 +227,7 @@ limit, against nothing for handing the node over.
| `SoundUnderAssumptions` | holds given something the rule does not check |
| `Heuristic` | usually right |

**188** of the 337 rules are `Sound` and **149** are conditional. Every one of the thirty registered
**189** of the 338 rules are `Sound` and **149** are conditional. Every one of the thirty registered
*sets* declares `SoundUnderAssumptions`, because a set's tier is the **minimum** over its rules — so
the set grain says nothing and the rule grain says everything. A derivation reports the rule's tier
(`RewriteStep.Soundness`), which is why getting it right matters beyond the label.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// Copyright (c) 2019-2026 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
// Website: https://am.angouri.org.
//

using AngouriMath;
using AngouriMath.Extensions;
using Xunit;
using static AngouriMath.Entity;
using static AngouriMath.Entity.Number;

namespace AngouriMath.Tests.Common
{
/// <summary>
/// <c>k sin(x)^2 + k cos(x)^2</c> was reduced only where the two terms were siblings, so the
/// sort decided: <c>y + (-2) cos(x)^2 + (-2) sin(x)^2</c> came out <c>y - 2</c>, and
/// <c>2 + (-2) cos(x)^2 + (-2) sin(x)^2</c>, where the constant sorts between the two, stayed.
/// <a href="https://github.com/asc-community/AngouriMath/issues/2133">#2133</a>
/// </summary>
[Trait("Area", "Common")]
public sealed class PythagoreanPairWithACoefficientTest
{
[Theory]
[InlineData("2 + (-2)*cos(x)^2 + (-2)*sin(x)^2", "0")]
[InlineData("2 + (-2) * cos(2 * x) + (-4) * sin(x) ^ 2", "0")]
[InlineData("a + b*sin(t)^2 + c + b*cos(t)^2", "a + b + c")]
[InlineData("y + (-2)*cos(x)^2 + (-2)*sin(x)^2", "y - 2")]
public void ThePairIsItsCoefficient(string input, string expected)
=> Assert.Equal(Integer.Zero, (input.Simplify() - expected.ToEntity()).Simplify());

/// <summary>
/// Two coefficients, or two arguments, that differ are not the identity.
/// </summary>
[Theory]
[InlineData("2*sin(x)^2 + 3*cos(x)^2")]
[InlineData("k*sin(x)^2 + k*cos(y)^2")]
public void UnequalPartsAreLeft(string input)
=> Assert.Contains("sin", input.Simplify().Stringize());

/// <summary>
/// The first rows of #2133's table, which are rational identities, and are answered by the
/// rational canonical form since
/// <a href="https://github.com/asc-community/AngouriMath/issues/2137">#2137</a>.
/// </summary>
[Theory]
[InlineData("a*b*c*(1/a + 1/b + 1/c) - (a*b + b*c + c*a)")]
[InlineData("(a*b + b*c + c*a)/3 - (a*b*c)*(1/a + 1/b + 1/c)/3")]
public void TheRationalRowsAreZero(string input)
{
var simplified = input.Simplify();
while (simplified is Providedf(var inner, _))
simplified = inner;
Assert.Equal(Integer.Zero, simplified);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ public void EveryDataRuleIsClassifiedAsWritten()
"one-less-a-squared-sine-is-a-squared-cosine: ReplacementIsCode",
"sine-of-a-whole-multiple-of-an-angle: ReplacementIsCode",
"squared-sine-and-cosine-of-one-argument-sum-to-one: ReplacementDropsHoles",
"squared-sine-and-cosine-with-one-coefficient-sum-to-it: ReplacementDropsHoles",
"the-arctangent-of-one-over-root-three: ReplacementIsCode",
"the-arctangent-of-root-three: ReplacementIsCode",
"the-negation-of-a-greater-turns-it-round: ReplacementIsCode",
Expand Down Expand Up @@ -436,9 +437,12 @@ public void AReversedRuleSaysWhatAnExpressionCameFrom()
[Fact]
public void ARuleThatForgetsAHoleHasNoBackwardsReading()
{
var pythagoras = MatchedRules.PythagoreanIdentity.Rules.Single();
Assert.Equal(RuleReversal.ReplacementDropsHoles, pythagoras.Reversal);
Assert.Null(pythagoras.Reversed);
// Both rules of the set, with a coefficient and without, drop the argument.
foreach (var pythagoras in MatchedRules.PythagoreanIdentity.Rules)
{
Assert.Equal(RuleReversal.ReplacementDropsHoles, pythagoras.Reversal);
Assert.Null(pythagoras.Reversed);
}

// And the reversed set is empty rather than absent, which is the difference being
// reported rather than hidden.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ private static void Stated(int stated, int measured, string claim)
public void WhereARuleGoes()
{
Stated(33, MatchedRules.All.Count, "the number of rule sets written as data");
Stated(337, MatchedRules.All.Sum(set => set.Rules.Count), "the number of rules written as data");
Stated(338, MatchedRules.All.Sum(set => set.Rules.Count), "the number of rules written as data");
Stated(30, RewriteRules.All.Count, "the number of registered sets");
// Two families register under one name and run a data set under another --
// CommonDenominator over MatchedRules.CommonDenominator(level), CanonicalOrder over
Expand All @@ -64,7 +64,7 @@ public void TheNameIsASentence()
{
var names = MatchedRules.All.SelectMany(set => set.Rules)
.Select(rule => rule.Name).Distinct(StringComparer.Ordinal).ToList();
Stated(308, names.Count, "the number of distinct rule names");
Stated(309, names.Count, "the number of distinct rule names");

var words = names.Select(name => name.Split('-').Length).ToList();
Stated(4, words.Min(), "the shortest rule name in words");
Expand All @@ -73,7 +73,7 @@ public void TheNameIsASentence()

[Fact]
public void TheIdentityIsNotTheName()
=> Stated(334,
=> Stated(335,
MatchedRules.All.SelectMany(set => set.Rules).Count(rule => rule.Description is not null),
"how many rules carry an identity");

Expand All @@ -85,7 +85,7 @@ public void ThePatternLanguage()
public void ReplacementAPatternOrCode()
{
var rules = MatchedRules.All.SelectMany(set => set.Rules).ToList();
Stated(38, rules.Count(rule => rule.Right is not null),
Stated(39, rules.Count(rule => rule.Right is not null),
"how many rules have a pattern on both sides");
Stated(34, rules.Count(rule => rule.Reversed is not null),
"how many two-sided rules have a direction");
Expand All @@ -97,17 +97,17 @@ public void ReplacementAPatternOrCode()
// rearrange, 9 expand and 270 unjudged", measured before the growth declarations
// went in. The argument survived; the arithmetic did not. A number a remark reasons
// from belongs somewhere that fails when it moves.
Stated(126, rules.Count(rule => rule.Growth is RewriteRuleGrowth.Collects),
Stated(127, rules.Count(rule => rule.Growth is RewriteRuleGrowth.Collects),
"how many rules are declared Collects");
Stated(49, rules.Count(rule => rule.Growth is RewriteRuleGrowth.Rearranges),
"how many rules are declared Rearranges");
Stated(36, rules.Count(rule => rule.Growth is RewriteRuleGrowth.Expands),
"how many rules are declared Expands");

// And the three the document names. By their own names rather than by what the prose
// And the ones the document names. By their own names rather than by what the prose
// calls them: the first is `squared-sine-and-cosine-of-one-argument-sum-to-one` and
// the prose calls it the Pythagorean identity, which is right and is not what to
// match on. All three forget something the backwards direction would have to invent
// match on. All of them forget something the backwards direction would have to invent
// -- the angle, the name the set builder bound, and which constant was raised.
var oneWay = rules
.Where(rule => rule.Right is not null && rule.Reversed is null)
Expand All @@ -121,6 +121,7 @@ public void ReplacementAPatternOrCode()
"a-constant-raised-to-a-logarithm-of-itself-is-the-antilogarithm",
"a-constant-raised-to-a-multiple-of-a-logarithm-of-itself-is-a-power-of-the-antilogarithm",
"squared-sine-and-cosine-of-one-argument-sum-to-one",
"squared-sine-and-cosine-with-one-coefficient-sum-to-it",
},
oneWay);
Assert.All(
Expand All @@ -132,7 +133,7 @@ public void ReplacementAPatternOrCode()
public void SoundnessIsPerRule()
{
var rules = MatchedRules.All.SelectMany(set => set.Rules).ToList();
Stated(188, rules.Count(rule => rule.Soundness is Soundness.Sound),
Stated(189, rules.Count(rule => rule.Soundness is Soundness.Sound),
"how many rules are Sound");
Stated(149, rules.Count(rule => rule.Soundness is Soundness.SoundUnderAssumptions),
"how many rules are SoundUnderAssumptions");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ public void AddressableGrowthIsTheRulesOwnAnswer()
public void SoundnessIsFinerPerRuleThanPerSet()
{
var rules = MatchedRules.All.SelectMany(set => set.Rules).ToList();
Assert.Equal(337, rules.Count);
Assert.Equal(188,rules.Count(rule => rule.Soundness is Soundness.Sound));
Assert.Equal(338, rules.Count);
Assert.Equal(189,rules.Count(rule => rule.Soundness is Soundness.Sound));
Assert.Equal(149, rules.Count(rule => rule.Soundness is Soundness.SoundUnderAssumptions));

// And every set still reports the weakest of them, which is what makes the set grain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public void EveryRuleWrittenAsDataIsNamedInProse()
{
var names = MatchedRules.All.SelectMany(set => set.Rules).Select(rule => rule.Name)
.Distinct(StringComparer.Ordinal).ToList();
Assert.Equal(308, names.Count);
Assert.Equal(309, names.Count);
Assert.All(names, name => Assert.True(Explanation.IsProse(name),
$"'{name}' is a rule name that does not read as a phrase in English"));

Expand Down
Loading