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
17 changes: 17 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,23 @@ its arguments and answers may change in a minor release. `amcli mcp` no longer l
| `am_classify` on `G*M*m/r^2`, to `amcli mcp` | `solved`, `Newtonian gravitation` first | `failed`: `'am_classify' is preliminary, and is served only when the server is started as 'amcli mcp --preliminary'` |
| the same, to `amcli mcp --preliminary` | `solved` (the flag is ignored) | `solved`, and `"preliminary": true` |

### A cosine of a sum is expanded, as a sine of one is

**Silent: identities in a shifted cosine reduce, and `Expand` opens a cosine of a sum.** The expansion rules had the
sine's addition theorem and not the cosine's, so no shift of a cosine was reduced: `sin(pi - x) - sin(x)` was `0` and
`cos(pi - x) + cos(x)` stayed as written. `cos(a + b)` and `cos(a - b)` are now expanded too. A shift whose reduced
form is shorter comes back reduced. `cos(pi - x)` on its own stays, as `sin(pi + x)` always has, because `-cos(x)`
is rated no shorter ([#2135](https://github.com/asc-community/AngouriMath/issues/2135)).

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"cos(pi/2 - x)".Simplify()` | `cos(pi / 2 - x)` | `sin(x)` |
| `"cos(2*pi - x)".Simplify()` | `cos(2 * pi - x)` | `cos(x)` |
| `"cos(pi - x) + cos(x)".Simplify()` | `cos(pi - x) + cos(x)` | `0` |
| `"cos(s + t) - cos(s)*cos(t) + sin(s)*sin(t)".Simplify()` | unchanged | `0` |
| `"cos(a + b)".Expand()` | `cos(a + b)` | `cos(a) * cos(b) + -sin(a) * sin(b)` |
| `"cos(a - b)".Expand()` | `cos(a - b)` | `cos(a) * cos(b) + sin(a) * sin(b)` |

### A subscript may have a subscript

**Not silent: what was a parse error is a name.** A name took at most one `_`, so `x_1_2` and `X_a_beta` were lexer
Expand Down
3 changes: 3 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Tests/UnitTests/Algebra/SolveTest/DuplicateSystemRowTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

[Tests/UnitTests/Common/CosineOfASumTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

[Tests/UnitTests/Common/LambdaArrowSyntaxTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,39 @@ internal static class MatchedRules
MatchPattern.Node<Sinf>(MatchPattern.Any("b")),
MatchPattern.Node<Cosf>(MatchPattern.Any("a")))),
Soundness.Sound,
description: "sin(a - b) = sin(a)cos(b) - sin(b)cos(a)"));
description: "sin(a - b) = sin(a)cos(b) - sin(b)cos(a)"),

// cos(a + b) -> cos(a)cos(b) - sin(a)sin(b)
// https://github.com/asc-community/AngouriMath/issues/2135
new MatchedRule(
"cosine-of-a-sum",
MatchPattern.Node<Cosf>(
MatchPattern.Node<Sumf>(MatchPattern.Any("a"), MatchPattern.Any("b"))),
MatchPattern.Node<Minusf>(
MatchPattern.Node<Mulf>(
MatchPattern.Node<Cosf>(MatchPattern.Any("a")),
MatchPattern.Node<Cosf>(MatchPattern.Any("b"))),
MatchPattern.Node<Mulf>(
MatchPattern.Node<Sinf>(MatchPattern.Any("a")),
MatchPattern.Node<Sinf>(MatchPattern.Any("b")))),
// The addition theorem again, for every complex a and b.
Soundness.Sound,
description: "cos(a + b) = cos(a)cos(b) - sin(a)sin(b)"),

// cos(a - b) -> cos(a)cos(b) + sin(a)sin(b)
new MatchedRule(
"cosine-of-a-difference",
MatchPattern.Node<Cosf>(
MatchPattern.Node<Minusf>(MatchPattern.Any("a"), MatchPattern.Any("b"))),
MatchPattern.Node<Sumf>(
MatchPattern.Node<Mulf>(
MatchPattern.Node<Cosf>(MatchPattern.Any("a")),
MatchPattern.Node<Cosf>(MatchPattern.Any("b"))),
MatchPattern.Node<Mulf>(
MatchPattern.Node<Sinf>(MatchPattern.Any("a")),
MatchPattern.Node<Sinf>(MatchPattern.Any("b")))),
Soundness.Sound,
description: "cos(a - b) = cos(a)cos(b) + sin(a)sin(b)"));

/// <summary>
/// <see cref="Functions.Patterns.ExpandTrigonometricRules"/>, as data.
Expand Down
6 changes: 3 additions & 3 deletions Sources/AngouriMath/Core/Transformations/Saturation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ 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 324 rules in
/// <see cref="MatchedRules"/>, <b>124 collect, 49 rearrange, 32 expand and 123 are
/// unjudged</b>. A ceiling that refuses the fourth value still refuses 38% of the library,
/// 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
/// 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
/// <see cref="RewriteRuleGrowth.Rearranges"/> ceiling proved two, and
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
**335** rules live there today.
**337** 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 306 distinct rule names are,
So the name has to be a clause that survives being read that way. **All 308 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. **332** rules carry one today; a new rule should.
rule happens to be applied in. **334** 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

**35** of the 328 rules have a pattern on both sides. The rest build their answer in code, and both
**38** of the 337 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 **32** of the 36
- **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,
`{ 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
Expand Down Expand Up @@ -226,7 +226,7 @@ limit, against nothing for handing the node over.
| `SoundUnderAssumptions` | holds given something the rule does not check |
| `Heuristic` | usually right |

**186** of the 335 rules are `Sound` and **149** are conditional. Every one of the thirty registered
**188** of the 337 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
Expand Up @@ -21,6 +21,10 @@ internal static partial class Patterns
{
Sinf(Sumf(var any1, var any2)) => new Sinf(any1) * new Cosf(any2) + new Sinf(any2) * new Cosf(any1),
Sinf(Minusf(var any1, var any2)) => new Sinf(any1) * new Cosf(any2) - new Sinf(any2) * new Cosf(any1),
// The cosine's, which were missing: cos(pi - x) + cos(x) did not reduce to 0 where
// sin(pi - x) - sin(x) did. https://github.com/asc-community/AngouriMath/issues/2135
Cosf(Sumf(var any1, var any2)) => new Cosf(any1) * new Cosf(any2) - new Sinf(any1) * new Sinf(any2),
Cosf(Minusf(var any1, var any2)) => new Cosf(any1) * new Cosf(any2) + new Sinf(any1) * new Sinf(any2),

_ => x
};
Expand Down
51 changes: 51 additions & 0 deletions Sources/Tests/UnitTests/Common/CosineOfASumTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// 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.Number;

namespace AngouriMath.Tests.Common
{
/// <summary>
/// The expansion rules opened a sine of a sum and not a cosine, so no shift of a cosine was
/// reduced: <c>sin(pi - x) - sin(x)</c> was 0 and <c>cos(pi - x) + cos(x)</c> stayed as it
/// was, and so did <c>cos(s + t) - cos(s) cos(t) + sin(s) sin(t)</c>.
/// <a href="https://github.com/asc-community/AngouriMath/issues/2135">#2135</a>
/// </summary>
[Trait("Area", "Common")]
public sealed class CosineOfASumTest
{
[Theory]
[InlineData("cos(pi - x) + cos(x)")]
[InlineData("cos(x - pi) + cos(x)")]
[InlineData("cos(pi/2 + x) + sin(x)")]
[InlineData("cos(s + t) - cos(s) * cos(t) + sin(s) * sin(t)")]
[InlineData("cos(s - t) - cos(s) * cos(t) - sin(s) * sin(t)")]
[InlineData("cos(pi/3 - x) - cos(x) / 2 - sqrt(3) * sin(x) / 2")]
public void TheIdentityIsZero(string identity)
=> Assert.Equal(Integer.Zero, identity.Simplify());

/// <summary>
/// Where the shifted form is longer than what it reduces to, the reduced one is the
/// answer. <c>cos(pi - x)</c> on its own stays, as <c>sin(pi + x)</c> always has:
/// <c>-cos(x)</c> is rated no shorter.
/// </summary>
[Theory]
[InlineData("cos(pi/2 - x)", "sin(x)")]
[InlineData("cos(2 * pi - x)", "cos(x)")]
public void AShiftIsReducedWhereThatIsShorter(string shifted, string reduced)
=> Assert.Equal(reduced.ToEntity(), shifted.Simplify());

[Theory]
[InlineData("cos(a + b)", "cos(a) * cos(b) - sin(a) * sin(b)")]
[InlineData("cos(a - b)", "cos(a) * cos(b) + sin(a) * sin(b)")]
public void ExpandOpensACosineOfASum(string cosine, string expanded)
=> Assert.Equal(Integer.Zero, (cosine.ToEntity().Expand() - expanded).Simplify());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ public void TheRegistryIsAddressableAsFarAsItSaysItIs()
// are thirty-three; NumericNeat's sixteen are eleven, six of them being three rules
// written once per side a negative factor can sit on; and the two factorial sets are
// eight arms each written as three. Every other repointed set is one arm for one rule.
Assert.Equal(332, withRules.Sum(set => set.Rules.Count));
Assert.Equal(334, withRules.Sum(set => set.Rules.Count));
}

/// <summary>
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(335, MatchedRules.All.Sum(set => set.Rules.Count), "the number of rules written as data");
Stated(337, 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(306, names.Count, "the number of distinct rule names");
Stated(308, 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(332,
=> Stated(334,
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(36, rules.Count(rule => rule.Right is not null),
Stated(38, rules.Count(rule => rule.Right is not null),
"how many rules have a pattern on both sides");
Stated(32, rules.Count(rule => rule.Reversed is not null),
Stated(34, 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 @@ -101,7 +101,7 @@ public void ReplacementAPatternOrCode()
"how many rules are declared Collects");
Stated(49, rules.Count(rule => rule.Growth is RewriteRuleGrowth.Rearranges),
"how many rules are declared Rearranges");
Stated(34, rules.Count(rule => rule.Growth is RewriteRuleGrowth.Expands),
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
Expand Down Expand Up @@ -132,7 +132,7 @@ public void ReplacementAPatternOrCode()
public void SoundnessIsPerRule()
{
var rules = MatchedRules.All.SelectMany(set => set.Rules).ToList();
Stated(186, rules.Count(rule => rule.Soundness is Soundness.Sound),
Stated(188, 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(335, rules.Count);
Assert.Equal(186,rules.Count(rule => rule.Soundness is Soundness.Sound));
Assert.Equal(337, rules.Count);
Assert.Equal(188,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 Expand Up @@ -174,7 +174,7 @@ public void EveryRuleOfARepointedSetCarriesItsIdentity()
var repointed = RewriteRules.All
.Where(set => set.Rules.Count > 0 && set.Rules.All(rule => rule.Soundness is not null))
.ToList();
Assert.Equal(332, repointed.Sum(set => set.Rules.Count));
Assert.Equal(334, repointed.Sum(set => set.Rules.Count));
Assert.All(repointed, set => Assert.All(set.Rules, rule => Assert.NotNull(rule.Description)));
}

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(306, names.Count);
Assert.Equal(308, 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