Skip to content

Do not name a root that denies how it was found (#964) - #1025

Merged
Rafael-SOWNet merged 3 commits into
masterfrom
fix/964-derivative-in-equation
Aug 23, 2026
Merged

Rafael-SOWNet merged 3 commits into
masterfrom
fix/964-derivative-in-equation

Conversation

@Rafael-SOWNet

@Rafael-SOWNet Rafael-SOWNet commented Aug 23, 2026 •

Copy link
Copy Markdown
Member

Closes #964. Roadmap #746 item 3 — a live wrong answer.

What was wrong

"derivative(y, x) + y - x".SolveEquation("y") answered { x }. Substituting that back — with this library's own Substitute — gives derivative(x, x) + x - x, which is 1. The set named a member that is not a root.

The derivative went to zero because y is not x, and every calculus operator reads a name it does not bind the same way: limit(y, x, 0) is y, integral(y, x) is x * y + C, sum(y, k, 1, 3) is 3 * y. Each is a decision about the name y. The root the solver then returns says that name stands for an expression in x, so the answer denies the step that produced it.

Measured on 6b93b401, the merge base:

was
"derivative(y, x) + y - x".SolveEquation("y") { x }
("derivative(y, x) + y - x = 0").Solve("y") { x }
"derivative(y, x) + y ^ 2 - x".SolveEquation("y") { sqrt(x), -sqrt(x) }, leaving x ^ (-1/2) / 2 and -1/2 * x ^ (-1/2)
"integral(y, x) + y - x".SolveEquation("y") { -(C + -x) / (x + 1) }
"limit(y, x, 0) + y - x".SolveEquation("y") { x / 2 }
"sum(y, k, 1, 3) + y - k".SolveEquation("y") { k / 4 }
"{ y : derivative(y, x) + y - x = 0 }".ToEntity().Simplify() { y : y - x = 0 }

So it is not one equation and not one operator: every binder in the language, and a quadratic in the unknown as readily as a linear one.

Why declining, and which spelling

Solving these needs a differential-equation solver — #746 item 48 — and this is not that. So the answer has to be a refusal, and a refusal has two spellings that are not equally honest:

  • the empty set says there is no such y, which is false;
  • the condition itself says I could not settle this, which is true.

All six now come back as ConditionalSet — { y : derivative(y, x) + y - x = 0 } and so on. AnalyticalEquationSolver already answers this way for #278, so it is the established spelling here rather than a new one.

A root that does not mention the name the operator is taken over denies nothing, and is returned as before. Nothing that answered correctly stops answering:

  • "derivative(y * x, x) + y - 1".SolveEquation("y") is { 1/2 }
  • "derivative(y ^ 2, y) - 2".SolveEquation("y") is { 1 } — there the unknown is the name the derivative binds
  • "limit(y * x, x, 2) - 4".SolveEquation("y") is { 2 }
  • "sum(y, k, 1, 3) - 6".SolveEquation("y") is { 2 }

The set builder had it too

{ y : derivative(y, x) + y - x = 0 } simplified to { y : y - x = 0 }, which is { x } — the same wrong answer, and the thing the honest answer above would have collapsed into on its way out through Entity.Solve. A binder over y makes y range over values, and expressions in x are among them, so that reading is not available under it. ConditionalSet.InnerSimplify now leaves such a predicate alone.

That is one shared predicate, CalculusOperator.NamesAssumedFreeOf, used at both sites.

What is deliberately not touched

The equality half of <= and >= goes through the same solver, but the inequality half is answered by AnalyticalInequalitySolver under the same assumption — (derivative(y, x) + y - x <= 0).Solve("y") is { x } \/ (-oo; -x / (-1)), and both halves are wrong. Guarding one of the two would give a set that is honest in half of itself, so it needs its own change rather than half of this one.

Measured

  • Suite 7549 passed, 0 failed, 14 skipped — the 16 added here on top of the 7533 of the merge base. No existing test changed.
  • F# wrapper 134 passed, 0 failed.
  • Every value in the tables above measured on a build of each side, 6b93b401 and this tree.
  • BREAKING-CHANGES.md carries all of it under a new Unreleased section.

The new tests are in Sources/Tests/UnitTests/Algebra/SolveTest/CalculusEquationTest.cs. The first of them is the property, not the form: every member of a finite answer is substituted back and required to simplify to zero. It is what fails on the merge base, with x is not a root of derivative(y, x) + y - x; it leaves 1.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bjumi5K7fg8yx6UK1mZTQd

Rafael-SOWNet and others added 2 commits August 23, 2026 12:21
"derivative(y, x) + y - x".SolveEquation("y") answered { x }. Substituting
that back, with this library's own Substitute, gives derivative(x, x) + x - x,
which is 1: the set named a member that is not a root.

The derivative went to zero because y is not x, and every calculus operator
reads a name it does not bind the same way -- limit(y, x, 0) is y,
integral(y, x) is x * y + C, sum(y, k, 1, 3) is 3 * y. That is a decision
about the name, and the root the solver then returns says the name stands
for an expression in x. The answer denies the step that produced it.

    "derivative(y, x) + y - x".SolveEquation("y")      { x }
    "derivative(y, x) + y ^ 2 - x".SolveEquation("y")  { sqrt(x), -sqrt(x) }
    "integral(y, x) + y - x".SolveEquation("y")        { -(C + -x) / (x + 1) }
    "limit(y, x, 0) + y - x".SolveEquation("y")        { x / 2 }
    "sum(y, k, 1, 3) + y - k".SolveEquation("y")       { k / 4 }

All five now come back as the condition itself. The empty set would say
there is no such y, which is a second false claim in place of the first;
the condition says only that this was not settled, which is true. Solving
it is #746 item 48, a differential-equation solver, and is not here.

A root free of the name the operator is taken over denies nothing and is
returned as before: derivative(y * x, x) + y - 1 is still { 1/2 }, and
derivative(y ^ 2, y) - 2 is { 1 }, where the unknown is the name the
derivative binds and no independence is claimed of it.

The set builder had the same reading of its own bound name:
{ y : derivative(y, x) + y - x = 0 } simplified to { y : y - x = 0 }, which
is { x }. A binder over y makes y range over values, and expressions in x
are among them, so that simplification settled a condition written to stay
open -- and it is what the answer above would have collapsed into. Guarded
where the predicate is simplified.

Not touched: the equality half of `<=` and `>=` goes through the same
solver, but the inequality half is answered by AnalyticalInequalitySolver
under the same assumption, so guarding one of the two would give a set that
is honest in half of itself. That needs its own change.

Suite 7549 passed, 0 failed, 14 skipped -- the 16 added here on top of the
7533 of the merge base. F# wrapper 134 passed. Both sides of every value
above measured on a build: 6b93b40 and this tree.

Claude-Session: https://claude.ai/code/session_01Bjumi5K7fg8yx6UK1mZTQd

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sources/.editorconfig pins the header per directory rather than globally --
bumping the default would make IDE0073 fail on the 367 files that carry the
2022 one -- so a new file needs its own scope or it ships with a copyright
four years out of date.

The two other branches that add files append their scopes to the same place,
so this costs whichever of the three merges later one append conflict, and
all three entries survive it.
BREAKING-CHANGES.md and Sources/.editorconfig are both append-only in practice,
so the merge keeps both sides -- except that a section heading is not an append:
unioning left two '## Unreleased' headings, and the entries now sit under one.
@Rafael-SOWNet
Rafael-SOWNet merged commit 0e68b16 into master Aug 23, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Solving an equation that contains the unknown's derivative returns a non-solution

1 participant