Skip to content

Commit 81f0c39

Browse files
committed
diff-eqn engine ready with 5 methods: rk4, rk45, euler, implicit_euler and bdf32
1 parent 047b8b5 commit 81f0c39

12 files changed

Lines changed: 601 additions & 221 deletions

File tree

‎parser-ng/DIFF_ENGINE.md‎

Lines changed: 266 additions & 0 deletions
Large diffs are not rendered by default.

‎parser-ng/src/main/java/com/github/gbenroscience/math/differentialcalculus/equations/coeffextractor/clext/common/DiffEqnArgParser.java‎

Lines changed: 110 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@
1010
import java.util.List;
1111

1212
/**
13-
* Reads the calling-convention arguments (t0, y0, tEnd, h, method, points) off
14-
* a diffeqn/diffeqnPath/diffeqnHO/diffeqnPathHO call — t0/tEnd/h/method/ points
15-
* from {@link Token#getRawArgs()} text (the pragmatic path settled on over
16-
* re-deriving the same information structurally, per the explicit "if too
17-
* difficult, get it from token.getRawArgs()" fallback), but y0 from the real
18-
* compiled {@code Token}, since a bracketed vector literal like
19-
* {@code (1, 0, 0, 0, 0)} compiles to a single {@code MATRIX}-kind token named
20-
* {@code anonN} rather than staying literal text — re-splitting rawArgs text on
21-
* commas breaks the moment y0 contains any expression with its own nested comma
22-
* (a function call, another vector), and doesn't reflect how ParserNG actually
13+
* Reads the calling-convention arguments (t0, y0, tEnd, h, method, points,
14+
* presentationStrategy) off a diffeqn/diffeqnPath/diffeqnHO/diffeqnPathHO
15+
* call — t0/tEnd/h/method/points/presentationStrategy from {@link
16+
* Token#getRawArgs()} text (the pragmatic path settled on over re-deriving
17+
* the same information structurally, per the explicit "if too difficult, get
18+
* it from token.getRawArgs()" fallback), but y0 from the real compiled
19+
* {@code Token}, since a bracketed vector literal like {@code (1, 0, 0, 0,
20+
* 0)} compiles to a single {@code MATRIX}-kind token named {@code anonN}
21+
* rather than staying literal text — re-splitting rawArgs text on commas
22+
* breaks the moment y0 contains any expression with its own nested comma (a
23+
* function call, another vector), and doesn't reflect how ParserNG actually
2324
* represents it. The real values are read via
2425
* {@code FunctionManager.lookUp(name).getMatrix().getFlatArray()}. Bracket
2526
* text-splitting is kept only as a last-resort fallback for a shape that
@@ -36,13 +37,30 @@
3637
* <h2>Positional argument layout</h2>
3738
* <pre>
3839
* diffeqn: [equation, t0, y0, tEnd, h?, method?]
39-
* diffeqnPath: [equation, t0, y0, tEnd, h?, method?, points?]
40-
* diffeqnHO: [equation, t0, y0, tEnd, h?, method?] (y0 always a vector here)
41-
* diffeqnPathHO: [equation, t0, y0, tEnd, h?, method?, points?]
42-
* </pre> h and method are optional per the calling convention; when omitted
43-
* this class applies a documented default ({@link #DEFAULT_H}, {@link
44-
* #DEFAULT_METHOD}) rather than silently guessing something else — a caller who
45-
* cares about the exact solver behavior should always pass both.
40+
* diffeqnPath: [equation, t0, y0, tEnd, h?, method?, points?, presentationStrategy?]
41+
* diffeqnHO: [equation, t0, y0, tEnd, h?, method?] (y0 always a vector here)
42+
* diffeqnPathHO: [equation, t0, y0, tEnd, h?, method?, points?, presentationStrategy?]
43+
* </pre>
44+
* h and method are optional for every kind; points and presentationStrategy
45+
* are additionally available on the *_PATH kinds only, and each is
46+
* INDEPENDENTLY optional — a call may supply neither, just points, just
47+
* presentationStrategy, or both. Because of that, the trailing argument at
48+
* index 6 is disambiguated by content rather than assumed to always be
49+
* points: if it parses as a number it's points (and index 7, if present, is
50+
* then presentationStrategy); if it doesn't parse as a number it's read as
51+
* presentationStrategy directly, and points is left at its default. This is
52+
* what lets {@code diffeqnPathHO(eqn, t0, y0, tEnd, h, method, "state")} work
53+
* — points omitted, presentationStrategy supplied — without the parser
54+
* trying (and failing) to read "state" as a number.
55+
*
56+
* All optional arguments fall back to a documented default ({@link
57+
* #DEFAULT_H}, {@link #DEFAULT_METHOD}, {@link
58+
* #DEFAULT_PRESENTATION_STRATEGY}) when omitted, rather than silently
59+
* guessing something else — a caller who cares about the exact solver
60+
* behavior should always pass them all explicitly. presentationStrategy is
61+
* currently only consumed downstream by diffeqnPathHO; diffeqnPath accepts
62+
* and parses it for forward compatibility, but nothing reads it yet for that
63+
* kind.
4664
*/
4765
public final class DiffEqnArgParser {
4866

@@ -57,6 +75,12 @@ public final class DiffEqnArgParser {
5775
public static final ODESolverMethod DEFAULT_METHOD
5876
= ODESolverMethod.RK4;
5977

78+
/**
79+
* Applied when the call omits the optional presentationStrategy argument.
80+
*/
81+
public static final PresentationStrategy DEFAULT_PRESENTATION_STRATEGY
82+
= PresentationStrategy.TRAJECTORY;
83+
6084
private DiffEqnArgParser() {
6185
}
6286

@@ -107,14 +131,41 @@ public static DiffEqnCall parse(Token[] fullCallPostfix) {
107131
ODESolverMethod method = raw.length > 5 && !raw[5].isEmpty()
108132
? parseMethod(raw[5]) : DEFAULT_METHOD;
109133

110-
int points = -1;
111134
boolean pathVariant = kind == DiffEqnCall.Kind.DIFFEQN_PATH || kind == DiffEqnCall.Kind.DIFFEQN_PATH_HO;
112-
if (pathVariant && raw.length > 6 && !raw[6].isEmpty()) {
113-
points = (int) parseDouble(raw[6], "points");
135+
136+
int points = -1;
137+
PresentationStrategy presentationStrategy = DEFAULT_PRESENTATION_STRATEGY;
138+
139+
if (pathVariant) {
140+
String arg6 = raw.length > 6 ? raw[6] : "";
141+
String arg7 = raw.length > 7 ? raw[7] : "";
142+
143+
if (!arg6.isEmpty()) {
144+
Double maybePoints = tryParseDouble(arg6);
145+
if (maybePoints != null) {
146+
// arg6 is numeric -> points. presentationStrategy, if present, is arg7.
147+
points = maybePoints.intValue();
148+
if (!arg7.isEmpty()) {
149+
presentationStrategy = parsePresentationStrategy(arg7);
150+
}
151+
} else {
152+
// arg6 isn't numeric -> it's presentationStrategy, and points was omitted.
153+
presentationStrategy = parsePresentationStrategy(arg6);
154+
if (!arg7.isEmpty()) {
155+
throw new IllegalArgumentException(
156+
"Unexpected 8th argument '" + arg7 + "' — the 7th argument ('" + arg6
157+
+ "') was already read as the presentation strategy, so no further "
158+
+ "trailing arguments are expected. If you meant to supply points, "
159+
+ "put it before the presentation strategy.");
160+
}
161+
}
162+
} else if (!arg7.isEmpty()) {
163+
// points slot explicitly left empty, but presentationStrategy was still supplied.
164+
presentationStrategy = parsePresentationStrategy(arg7);
165+
}
114166
}
115167

116-
117-
return new DiffEqnCall(kind, rhsText, t0, y0, tEnd, h, method, points);
168+
return new DiffEqnCall(kind, rhsText, t0, y0, tEnd, h, method, points, presentationStrategy);
118169
}
119170

120171
private static double parseDouble(String raw, String argName) {
@@ -125,6 +176,20 @@ private static double parseDouble(String raw, String argName) {
125176
}
126177
}
127178

179+
/**
180+
* Like {@link #parseDouble}, but never throws — returns null on failure
181+
* instead. Used to disambiguate whether a trailing optional argument is
182+
* numeric (points) or not (presentationStrategy) without relying on a
183+
* fixed position, since the two are independently optional.
184+
*/
185+
private static Double tryParseDouble(String raw) {
186+
try {
187+
return Double.parseDouble(raw.trim());
188+
} catch (NumberFormatException e) {
189+
return null;
190+
}
191+
}
192+
128193
/**
129194
* Resolves y0 (argument index 2) from its real, isolated {@code Token}
130195
* rather than raw text where possible — see class javadoc for why. A
@@ -202,4 +267,26 @@ public static ODESolverMethod parseMethod(String raw) {
202267
+ "\"euler\", \"rk4\", \"rk45\", \"implicit_euler\", \"bdf2\".");
203268
}
204269
}
205-
}
270+
271+
/**
272+
* Parses the optional presentationStrategy argument. Accepts an
273+
* unquoted or quoted "state"/"trajectory" (case-insensitive), matching
274+
* {@link #parseMethod}'s convention for string-valued arguments.
275+
*/
276+
public static PresentationStrategy parsePresentationStrategy(String raw) {
277+
String cleaned = raw.trim();
278+
if (cleaned.length() >= 2 && cleaned.startsWith("\"") && cleaned.endsWith("\"")) {
279+
cleaned = cleaned.substring(1, cleaned.length() - 1);
280+
}
281+
switch (cleaned.toLowerCase()) {
282+
case "trajectory":
283+
return PresentationStrategy.TRAJECTORY;
284+
case "state":
285+
return PresentationStrategy.STATE;
286+
default:
287+
throw new IllegalArgumentException(
288+
"Unrecognized presentation strategy '" + raw + "' — expected one of "
289+
+ "\"trajectory\" or \"state\".");
290+
}
291+
}
292+
}

‎parser-ng/src/main/java/com/github/gbenroscience/math/differentialcalculus/equations/coeffextractor/clext/common/DiffEqnCall.java‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,16 @@ public enum Kind {
5050
public final ODESolverMethod method;
5151
/** Only meaningful for the *_PATH kinds; <= 0 means "solver's natural steps, no resampling". */
5252
public final int points;
53+
/**
54+
* {@link PresentationStrategy} field
55+
* Determines whether to show a matrix of t, y, y', y''(when this field equals {@link PresentationStrategy#STATE} ) etc
56+
* or just t, y (when this field equals {@link PresentationStrategy#TRAJECTORY} ).
57+
* The default is {@link PresentationStrategy#TRAJECTORY}
58+
*/
59+
public final PresentationStrategy presentationStrategy;
5360

5461
public DiffEqnCall(Kind kind, String rhsRawText, double t0, double[] y0, double tEnd,
55-
double h, ODESolverMethod method, int points) {
62+
double h, ODESolverMethod method, int points, PresentationStrategy presentationStrategy) {
5663
this.kind = kind;
5764
this.rhsRawText = rhsRawText;
5865
this.t0 = t0;
@@ -61,6 +68,7 @@ public DiffEqnCall(Kind kind, String rhsRawText, double t0, double[] y0, double
6168
this.h = h;
6269
this.method = method;
6370
this.points = points;
71+
this.presentationStrategy = presentationStrategy;
6472
}
6573

6674
public boolean isPathVariant() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2026 GBEMIRO.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.github.gbenroscience.math.differentialcalculus.equations.coeffextractor.clext.common;
17+
18+
/**
19+
*
20+
* @author GBEMIRO
21+
*/
22+
public enum PresentationStrategy {
23+
TRAJECTORY, STATE
24+
}

0 commit comments

Comments
 (0)