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 @@ -200,6 +200,19 @@ for `u` made a matrix of vectors. A lambda written in place is now applied befor
| `apply(lambda(u, [[0, -1], [1, 0]] * u), [p, q])` | `[[0, [-p, -q]], [[p, q], 0]]` | `[-q, p]` |
| `apply(lambda(u, [1, 1]T * u), [p, q])` | `[[[p, q], [p, q]]]` | `p + q` |

### One and a squared tangent are found anywhere in a sum

**Silent: an identity reaches its value whichever way the sum is sorted.** `1 + tan(x)^2 = sec(x)^2` and
`1 + cot(x)^2 = csc(x)^2` were rules for two siblings, and secant sorts before tangent, so `1 + tan(x)^2 - sec(x)^2`
became `1 - sec(x)^2 + tan(x)^2` and stayed, while its cotangent twin, sorted the other way, was `0`. Both identities
are now also found wherever the `1` and the square sit in a sum, under the condition the tangent's domain carries
([#2142](https://github.com/asc-community/AngouriMath/issues/2142)).

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"1 + tan(x)^2 - sec(x)^2".Simplify()` | `1 + tan(x) ^ 2 - sec(x) ^ 2` | `0 provided not cos(x) = 0` |
| `"a + 1 + tan(y)^2 - sec(y)^2".Simplify()` | `a + 1 + tan(y) ^ 2 - sec(y) ^ 2` | `a provided not cos(y) = 0` |

### A square system is not answered with rows that are no solution

**Silent: a row that is no solution is gone, and a system with a row that fails for some values is `null`.**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,46 @@ internal static class MatchedRules
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"));
description: "k sin(x)^2 + k cos(x)^2 = k"),

// ... + 1 + ... + tan(x)^2 + ... -> sec(x)^2 + (everything else), and the same with
// the cotangent. The identity divided through by cos^2 and by sin^2 is also in
// TrigonometricRules, but for two siblings only, so the sort decided again: secant
// sorts before tangent, and 1 + tan(x)^2 - sec(x)^2 became 1 - sec(x)^2 + tan(x)^2
// and stayed, while cotangent sorts before cosecant and its twin was 0.
// SoundUnderAssumptions as there: each side is defined only where cos (sin) is not 0.
// https://github.com/asc-community/AngouriMath/issues/2142
new MatchedRule(
"one-and-a-squared-tangent-anywhere-in-a-sum-make-a-squared-secant",
MatchPattern.Gathered<Sumf>(
"rest",
MatchPattern.Exact(Integer.Create(1)),
MatchPattern.Node<Powf>(
MatchPattern.Node<Tanf>(MatchPattern.Any("x")),
MatchPattern.Exact(Integer.Create(2)))),
MatchPattern.Node<Sumf>(
MatchPattern.Node<Powf>(
MatchPattern.Node<Secantf>(MatchPattern.Any("x")),
MatchPattern.Exact(Integer.Create(2))),
MatchPattern.Any("rest")),
Soundness.SoundUnderAssumptions,
description: "1 + tan(x)^2 = sec(x)^2"),

new MatchedRule(
"one-and-a-squared-cotangent-anywhere-in-a-sum-make-a-squared-cosecant",
MatchPattern.Gathered<Sumf>(
"rest",
MatchPattern.Exact(Integer.Create(1)),
MatchPattern.Node<Powf>(
MatchPattern.Node<Cotanf>(MatchPattern.Any("x")),
MatchPattern.Exact(Integer.Create(2)))),
MatchPattern.Node<Sumf>(
MatchPattern.Node<Powf>(
MatchPattern.Node<Cosecantf>(MatchPattern.Any("x")),
MatchPattern.Exact(Integer.Create(2))),
MatchPattern.Any("rest")),
Soundness.SoundUnderAssumptions,
description: "1 + cotan(x)^2 = cosec(x)^2"));

/// <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 338 rules in
/// <see cref="MatchedRules"/>, <b>127 collect, 49 rearrange, 36 expand and 126 are
/// nobody measured. But it is where the rules are: of the 340 rules in
/// <see cref="MatchedRules"/>, <b>129 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
12 changes: 6 additions & 6 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
**338** rules live there today.
**340** 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 309 distinct rule names are,
So the name has to be a clause that survives being read that way. **All 311 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. **335** rules carry one today; a new rule should.
rule happens to be applied in. **337** rules carry one today; a new rule should.

## The pattern language

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

## Replacement: a pattern, or code

**39** of the 338 rules have a pattern on both sides. The rest build their answer in code, and both
**41** of the 340 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 39
- **a direction.** `MatchedRule.Reversed` is the rule read the other way, and **36** of the 41
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
Expand Down Expand Up @@ -227,7 +227,7 @@ limit, against nothing for handing the node over.
| `SoundUnderAssumptions` | holds given something the rule does not check |
| `Heuristic` | usually right |

**189** of the 338 rules are `Sound` and **149** are conditional. Every one of the thirty registered
**189** of the 340 rules are `Sound` and **151** 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
45 changes: 45 additions & 0 deletions Sources/Tests/UnitTests/Common/TangentSecantIdentityTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// 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>1 + tan(x)^2 = sec(x)^2</c> was found for two siblings only, and secant sorts before
/// tangent, so <c>1 + tan(x)^2 - sec(x)^2</c> became <c>1 - sec(x)^2 + tan(x)^2</c> and stayed,
/// while its cotangent twin, where cotangent sorts first, was <c>0</c>.
/// <a href="https://github.com/asc-community/AngouriMath/issues/2142">#2142</a>
/// </summary>
[Trait("Area", "Common")]
public sealed class TangentSecantIdentityTest
{
[Theory]
[InlineData("1 + tan(x)^2 - sec(x)^2", "0")]
[InlineData("1 + cot(x)^2 - cosec(x)^2", "0")]
[InlineData("a + 1 + tan(y)^2 - sec(y)^2", "a")]
[InlineData("tan(x)^2 - sec(x)^2 + 1", "0")]
public void TheIdentityHolds(string input, string expected)
{
var difference = (input.Simplify() - expected.ToEntity()).Simplify();
while (difference is Providedf(var inner, _))
difference = inner;
Assert.Equal(Integer.Zero, difference);
}

/// <summary>
/// Where the tangent is undefined the answer is too, as the input is.
/// </summary>
[Fact]
public void TheAnswerKeepsTheTangentsDomain()
=> Assert.Contains("cos(x) = 0", "1 + tan(x)^2 - sec(x)^2".Simplify().Stringize());
}
}
22 changes: 15 additions & 7 deletions Sources/Tests/UnitTests/Core/Transformations/ReversibleRuleTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -437,16 +437,24 @@ public void AReversedRuleSaysWhatAnExpressionCameFrom()
[Fact]
public void ARuleThatForgetsAHoleHasNoBackwardsReading()
{
// Both rules of the set, with a coefficient and without, drop the argument.
foreach (var pythagoras in MatchedRules.PythagoreanIdentity.Rules)
// Both Pythagorean rules of the set, with a coefficient and without, drop the
// argument. The set's other two, 1 + tan^2 = sec^2 and its cotangent twin, keep it,
// and they are the only ones its reversal holds.
var pythagoras = MatchedRules.PythagoreanIdentity.Rules
.Where(rule => rule.Name.StartsWith("squared-sine-and-cosine", StringComparison.Ordinal))
.ToList();
Assert.Equal(2, pythagoras.Count);
foreach (var rule in pythagoras)
{
Assert.Equal(RuleReversal.ReplacementDropsHoles, pythagoras.Reversal);
Assert.Null(pythagoras.Reversed);
Assert.Equal(RuleReversal.ReplacementDropsHoles, rule.Reversal);
Assert.Null(rule.Reversed);
}

// And the reversed set is empty rather than absent, which is the difference being
// reported rather than hidden.
Assert.Empty(MatchedRules.PythagoreanIdentity.Reversed.Rules);
// And what the reversed set holds is the reversals of the others only, rather than
// anything standing for the two that have none.
Assert.Equal(
MatchedRules.PythagoreanIdentity.Rules.Count(rule => rule.Reversed is not null),
MatchedRules.PythagoreanIdentity.Reversed.Rules.Count);
Assert.Equal(
"1 + a".ToEntity(),
MatchedRules.PythagoreanIdentity.Reversed.ApplyHere("1 + a".ToEntity()));
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(338, MatchedRules.All.Sum(set => set.Rules.Count), "the number of rules written as data");
Stated(340, 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(309, names.Count, "the number of distinct rule names");
Stated(311, 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(335,
=> Stated(337,
MatchedRules.All.SelectMany(set => set.Rules).Count(rule => rule.Description is not null),
"how many rules carry an identity");

Expand All @@ -85,9 +85,9 @@ public void ThePatternLanguage()
public void ReplacementAPatternOrCode()
{
var rules = MatchedRules.All.SelectMany(set => set.Rules).ToList();
Stated(39, rules.Count(rule => rule.Right is not null),
Stated(41, 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),
Stated(36, rules.Count(rule => rule.Reversed is not null),
"how many two-sided rules have a direction");
Stated(126, rules.Count(rule => rule.Growth is RewriteRuleGrowth.Unknown),
"how many rules sit at Unknown growth");
Expand All @@ -97,7 +97,7 @@ 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(127, rules.Count(rule => rule.Growth is RewriteRuleGrowth.Collects),
Stated(129, 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");
Expand Down Expand Up @@ -135,7 +135,7 @@ public void SoundnessIsPerRule()
var rules = MatchedRules.All.SelectMany(set => set.Rules).ToList();
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),
Stated(151, rules.Count(rule => rule.Soundness is Soundness.SoundUnderAssumptions),
"how many rules are SoundUnderAssumptions");
Assert.All(RewriteRules.All,
set => Assert.Equal(Soundness.SoundUnderAssumptions, set.Soundness));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ public void AddressableGrowthIsTheRulesOwnAnswer()
public void SoundnessIsFinerPerRuleThanPerSet()
{
var rules = MatchedRules.All.SelectMany(set => set.Rules).ToList();
Assert.Equal(338, rules.Count);
Assert.Equal(340, rules.Count);
Assert.Equal(189,rules.Count(rule => rule.Soundness is Soundness.Sound));
Assert.Equal(149, rules.Count(rule => rule.Soundness is Soundness.SoundUnderAssumptions));
Assert.Equal(151, rules.Count(rule => rule.Soundness is Soundness.SoundUnderAssumptions));

// And every set still reports the weakest of them, which is what makes the set grain
// uninformative rather than wrong.
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(309, names.Count);
Assert.Equal(311, 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