From 3438fa872f6b31fce18b183142341ba140db2d36 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 16 Aug 2026 03:23:36 +0000 Subject: [PATCH] Give F# a way to solve a system of equations (#562) The wrapper had `solutions x expr` for one equation and nothing at all for a system, so the only route was dropping into the C# API. solveSystem ["x"; "y"] ["x + y - 3"; "x - y - 1"] // [[2, 1]] That is Version 2 of the two shapes on the issue, chosen because it reads like the `solutions x expr` already there -- subject first, then the thing -- rather than introducing an EquationSystem type a caller has to learn before solving anything. `equationSystem` is exposed as well for anyone who does want to build one and pass it around, which is Version 1's use without Version 1's cost. Two decisions the tests pin: An entry may be written either way. `x + y = 3` and `x + y - 3` mean the same system, and an equality is moved to one side rather than passed on as a node -- handing an Equalsf to EquationSystem makes the solver try to invert an equality and throw NotSufficientlySupportedException, which is what the first version of this did. No solution answers None rather than a null Matrix, since "there is no solution" and "the solver gave up" arrive the same way here and neither is a matrix. @Happypig375 asked on the issue for someone to pick the redesign up. F# wrapper tests 134 passed, 0 failed. --- .../FSharpWrapperUnitTests.fsproj | 1 + .../Functions.Systems.fs | 38 +++++++++++++++++++ .../Wrappers/AngouriMath.FSharp/Functions.fs | 23 +++++++++++ 3 files changed, 62 insertions(+) create mode 100644 Sources/Tests/FSharpWrapperUnitTests/Functions.Systems.fs diff --git a/Sources/Tests/FSharpWrapperUnitTests/FSharpWrapperUnitTests.fsproj b/Sources/Tests/FSharpWrapperUnitTests/FSharpWrapperUnitTests.fsproj index f6a42a3e7..f987569cf 100644 --- a/Sources/Tests/FSharpWrapperUnitTests/FSharpWrapperUnitTests.fsproj +++ b/Sources/Tests/FSharpWrapperUnitTests/FSharpWrapperUnitTests.fsproj @@ -17,6 +17,7 @@ + diff --git a/Sources/Tests/FSharpWrapperUnitTests/Functions.Systems.fs b/Sources/Tests/FSharpWrapperUnitTests/Functions.Systems.fs new file mode 100644 index 000000000..a6a7fd4f3 --- /dev/null +++ b/Sources/Tests/FSharpWrapperUnitTests/Functions.Systems.fs @@ -0,0 +1,38 @@ +module AngouriMath.FSharp.Tests.Systems + +open AngouriMath.FSharp.Core +open AngouriMath.FSharp.Functions +open Xunit + +// https://github.com/asc-community/AngouriMath/issues/562. Version 2 of the two shapes proposed +// there: the variables first and the equations second, which is how `solutions x expr` already +// reads, rather than a separate EquationSystem type the caller has to know about. `equationSystem` +// is still exposed for anyone who wants to build one and pass it around. + +[] +let ``A linear system is solved over its variables`` () = + match solveSystem ["x"; "y"] ["x + y - 3"; "x - y - 1"] with + | Some solutions -> + Assert.Equal(1, solutions.RowCount) + Assert.Equal(parsed "2", solutions.[0, 0]) // columns follow the order of vars + Assert.Equal(parsed "1", solutions.[0, 1]) + | None -> failwith "the system has a solution and should not have answered None" + +[] +let ``An equality may be written out rather than moved to one side`` () = + let asEquality = solveSystem ["x"; "y"] ["x + y = 3"; "x - y = 1"] + let asExpression = solveSystem ["x"; "y"] ["x + y - 3"; "x - y - 1"] + Assert.Equal(asExpression.IsSome, asEquality.IsSome) + Assert.Equal(asExpression.Value.[0, 0], asEquality.Value.[0, 0]) + Assert.Equal(asExpression.Value.[0, 1], asEquality.Value.[0, 1]) + +/// An inconsistent system has no solution, and None is that rather than an empty matrix. +[] +let ``An inconsistent system answers None`` () = + Assert.True((solveSystem ["x"; "y"] ["x + y - 1"; "x + y - 2"]).IsNone) + +[] +let ``A system can be built and passed around`` () = + let system = equationSystem ["x + y - 3"; "x - y - 1"] + Assert.NotNull(system) + Assert.True((system.Solve(symbol "x", symbol "y")) <> null) diff --git a/Sources/Wrappers/AngouriMath.FSharp/Functions.fs b/Sources/Wrappers/AngouriMath.FSharp/Functions.fs index b4ea3ab05..a8d294e00 100644 --- a/Sources/Wrappers/AngouriMath.FSharp/Functions.fs +++ b/Sources/Wrappers/AngouriMath.FSharp/Functions.fs @@ -250,3 +250,26 @@ let solutions x expr = match parsed expr with | :? Entity.Statement as statement -> statement.Solve(symbol x).InnerSimplified :?> Entity.Set | func -> (equality func 0).Solve(symbol x).InnerSimplified :?> Entity.Set + +/// Returns a system of equations, built from a list of equalities or expressions. +/// An expression that is not an equality is read as being equal to zero, exactly as +/// `solutions` reads a single one. +let equationSystem (equations : 'T list) = + MathS.Equations(equations |> List.map (fun eq -> + match parsed eq with + // EquationSystem reads each entry as being equal to zero, so an equality written out + // is moved to one side rather than handed over as a node -- passing the node makes the + // solver try to invert an equality, which it cannot do. + | :? Entity.Equalsf as equation -> equation.Left - equation.Right + | func -> func)) + +/// Solves a system of equations over the given variables. +/// +/// Each row of the resulting matrix is one solution and its columns follow the order of +/// `vars`, so `solveSystem ["x"; "y"] ["x + y - 3"; "x - y - 1"]` answers `[[2, 1]]`: +/// one solution, with x = 2 and y = 1. Returns None where none was found, rather than an +/// empty matrix, since "no solution" and "the solver gave up" are the same answer here and +/// neither is a matrix. +let solveSystem (vars : 'T list) (equations : 'U list) = + (equationSystem equations).Solve(vars |> List.map symbol |> Array.ofList) + |> Option.ofObj