Skip to content

Commit f50dd6c

Browse files
committed
V1.5.1
1 parent 41aa042 commit f50dd6c

File tree

10 files changed

+753
-559
lines changed

10 files changed

+753
-559
lines changed

CHANGELOG.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,59 @@
11
# Changelog for AbsoluteLib V2
22

33

4-
# 1.3.6
4+
## 1.5.1
5+
6+
### Dual-Mode Solver — Constraint + Improved Sweep
7+
- **Both solver strategies now available** via `TrajectorySolver.SolveMode` enum:
8+
- **`CONSTRAINT`** (default): Two-constraint algebraic solver. Computes exact (pitch, velocity) from target geometry + rim clearance. Fastest and most deterministic.
9+
- **`SWEEP`**: Improved angle sweep. Tests every pitch from min→max at 0.5° steps, calculates the exact velocity needed per angle, simulates with full RK4 physics, refines with binary-search velocity correction, and picks the angle with the **smallest miss distance**. No scoring, no weights — pure accuracy selection.
10+
- **`CONSTRAINT` mode automatically falls back to `SWEEP`** if the constraint solver fails (e.g., no valid clearance found within +3.0m). The sweep acts as a robust fallback.
11+
- **`SWEEP` mode does NOT fall back** — it runs the sweep only.
12+
13+
### New API
14+
- **`solver.setSolveMode(TrajectorySolver.SolveMode.CONSTRAINT)`** — switch between modes at runtime
15+
- **`solver.getSolveMode()`** — query current mode
16+
- Both modes share the same validation pipeline: collision, clearance, arc height, hit detection, flyover, entry angle, and velocity refinement.
17+
18+
## 1.5.0
19+
20+
### Constraint-Based Solver
21+
- **Replaced the angle sweep with a two-constraint algebraic solver**. Instead of testing 70+ pitch angles and picking the best, the solver now computes the **single exact (pitch, velocity) pair** from two physical constraints:
22+
1. Ball passes through target center at `(distance, heightDiff)`
23+
2. Ball clears the rim edge at `(distance − radius, heightDiff + clearance)`
24+
- This eliminates scoring, searching, and miss-distance comparison entirely. One deterministic solution per solve — no ambiguity.
25+
- **Full physics verification**: The vacuum analytical solution is then simulated with RK4 (air drag, Magnus spin, game piece properties) and refined with 8-iteration binary-search velocity correction. Same physics fidelity, dramatically simpler selection.
26+
- **Obstacle avoidance via clearance escalation**: If the analytical trajectory collides with an obstacle or fails clearance checks, the solver increases `rimClearance` in 0.25m steps (up to +3.0m) until the arc is high enough to clear. No sweep fallback needed.
27+
- **Moving target refinement**: For moving robots, the solver iterates twice with actual time-of-flight to refine the lead prediction — same concept as TurretCalculator's `iterativeMovingShotFromFunnelClearance`.
28+
- **`solveForCurrentRpm()` also updated**: Uses the high-arc angle formula `θ = atan((v² + √(v⁴ − g(gd² + 2hv²))) / (gd))` instead of sweeping. Direct analytical solution for fixed-velocity scenarios.
29+
30+
### New Configuration
31+
- **`SolverConstants.rimClearanceMeters`** (default 0.15m): Height above the target rim the ball must achieve at the rim edge. Controls arc steepness — larger = steeper entry, smaller = flatter. One ball radius (~0.075m) is the physical minimum.
32+
- **`computeConstraintSolution(d, h, r, c)`**: New static method exposing the two-constraint parabolic solver. Returns `[pitchRadians, velocityMps]` for any geometry.
33+
34+
## 1.4.1
35+
36+
### Scoring Removed — Pure Accuracy Selection
37+
- **Removed all scoring**: The solver no longer scores candidates. Instead, it filters valid trajectories (collision-free, on-target, correct entry angle, no flyover) and picks the one **closest to dead center** by miss distance. No points, no weights, no tuning — just the most accurate shot.
38+
- **Selection by miss distance**: Among all valid candidates, the solver selects the trajectory with the smallest horizontal distance from the target center at the rim-crossing plane. A hit 2cm off-center always beats a hit 5cm off-center, regardless of entry angle, height, or flight time.
39+
- **`ScoringWeights` deprecated**: The class and `setScoringWeights()`/`getScoringWeights()` are `@Deprecated`. The solver ignores them entirely.
40+
- **`scoreCandidate()` removed**: The internal scoring method is gone. No replacement — selection is a simple `missDistance < bestMissDistance` comparison.
41+
42+
### Debug Info Updated
43+
- `SolveDebugInfo` now tracks **miss distance** (meters from center) instead of score points.
44+
- New `getBestMissDistance()` and `CandidateInfo.getMissDistance()` methods.
45+
- `getBestScore()` and `getScore()` are `@Deprecated` (return miss distance for compatibility).
46+
- Debug tables and summaries show `miss=X.XXXXm` instead of `score=X.X`.
47+
48+
### Tighter Hit Detection
49+
- **Default tolerance multipliers reduced to 1.0** (`hoopToleranceMultiplier`, `basketDescentToleranceMultiplier`). The ball must land within the actual target radius — no more inflated acceptance zones.
50+
- **Rim-plane crossing always detected**: Simulation stops when the ball descends through targetZ regardless of horizontal distance.
51+
- **Velocity binary-search refinement**: 8-iteration binary search finds the lowest valid velocity for the steepest possible entry angle.
52+
53+
### Other
54+
- Updated `ExampleShooter`: removed all weight configuration, tightened target radius to 0.45m, debug output uses miss distance instead of score.
55+
56+
# 1.3.6 - 1.3.9
557
- Scoring update
658
- Re-made the Website
759

absolutelib.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"fileName": "absolutelib.json",
33
"name": "absolutelib",
4-
"version": "1.4.0",
4+
"version": "1.5.1",
55
"frcYear": "2026",
66
"uuid": "a604c6de-3573-4ba6-ae4f-32778238b9de",
77
"mavenUrls": [
@@ -13,7 +13,7 @@
1313
{
1414
"groupId": "ca.team4308",
1515
"artifactId": "absolutelib-java",
16-
"version": "1.4.0"
16+
"version": "1.5.1"
1717
}
1818
],
1919
"requires": [

example/example-2026-Imported/src/main/java/frc/robot/subsystems/ExampleShooter.java

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -78,31 +78,23 @@ public ExampleShooter() {
7878
.addEntry(8.0, 48.0, 3300);
7979

8080
GamePiece gamePiece = GamePieces.REBUILT_2026_BALL;
81-
SolverConstants.setHoopToleranceMultiplier(1);
82-
SolverConstants.setBasketDescentToleranceMultiplier(1);
8381
SolverConstants.setMinTargetDistanceMeters(0.05);
8482
SolverConstants.setVelocityBufferMultiplier(1.2);
83+
SolverConstants.setRimClearanceMeters(0.15);
8584
TrajectorySolver.SolverConfig solverConfig = TrajectorySolver.SolverConfig.defaults()
8685
.toBuilder()
87-
.hoopToleranceMultiplier(1.5)
8886
.minPitchDegrees(47.5)
8987
.maxPitchDegrees(82.5)
9088
.build();
9189
solver = new TrajectorySolver(gamePiece, solverConfig);
90+
91+
// CONSTRAINT or SWEEP
92+
solver.setSolveMode(TrajectorySolver.SolveMode.CONSTRAINT);
9293

9394
shooterSystem = new ShooterSystem(config, table, solver);
9495
shooterSystem.setMode(ShotMode.SOLVER_ONLY);
9596
shooterSystem.setFallbackShot(60.0, 3000);
9697

97-
solver.setScoringWeights(
98-
ScoringWeights.builder()
99-
.lowArcWeight(.10)
100-
.optimalAngleDegrees(50.0)
101-
.accuracyWeight(1.2)
102-
.speedWeight(5.0)
103-
.stabilityWeight(0.3)
104-
.clearanceWeight(0.5)
105-
.build());
10698
solver.setDebugEnabled(true);
10799
}
108100

@@ -219,24 +211,24 @@ private void logTrajectoryDebug() {
219211
recordOutput("Debug/RejectedClearance", debug.getRejectedClearanceCount());
220212
recordOutput("Debug/RejectedMiss", debug.getRejectedMissCount());
221213
recordOutput("Debug/RejectedFlyover", debug.getRejectedFlyoverCount());
222-
recordOutput("Debug/BestScore", debug.getBestScore());
214+
recordOutput("Debug/BestMissDistance", debug.getBestMissDistance());
223215
recordOutput("Debug/BestPitchDeg", debug.getBestPitchDegrees());
224216
recordOutput("Debug/Summary", debug.getSummary());
225217
recordOutput("Debug/DetailedTable", debug.getDetailedTable());
226218

227219
List<SolveDebugInfo.CandidateInfo> accepted = debug.getAcceptedCandidates();
228220
double[] accPitch = new double[accepted.size()];
229-
double[] accScore = new double[accepted.size()];
221+
double[] accMiss = new double[accepted.size()];
230222
double[] accTOF = new double[accepted.size()];
231223
double[] accMaxH = new double[accepted.size()];
232224
for (int i = 0; i < accepted.size(); i++) {
233225
accPitch[i] = accepted.get(i).getPitchDegrees();
234-
accScore[i] = accepted.get(i).getScore();
226+
accMiss[i] = accepted.get(i).getMissDistance();
235227
accTOF[i] = accepted.get(i).getTimeOfFlight();
236228
accMaxH[i] = accepted.get(i).getMaxHeight();
237229
}
238230
recordOutput("Debug/AcceptedPitches", accPitch);
239-
recordOutput("Debug/AcceptedScores", accScore);
231+
recordOutput("Debug/AcceptedMissDistance", accMiss);
240232
recordOutput("Debug/AcceptedTOF", accTOF);
241233
recordOutput("Debug/AcceptedMaxHeight", accMaxH);
242234

@@ -290,7 +282,7 @@ private void updateShot() {
290282
.shooterPositionMeters(shooterX, shooterY, shooterHeightMeters)
291283
.shooterYawRadians(yawRad)
292284
.targetPositionMeters(targetPosition.getX(), targetPosition.getY(), targetPosition.getZ())
293-
.targetRadiusMeters(0.53)
285+
.targetRadiusMeters(0.45)
294286
.includeAirResistance(true)
295287
.robotVelocity(vx, vy)
296288
);

example/example-2026-Imported/vendordeps/absolutelib.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"fileName": "absolutelib.json",
33
"name": "absolutelib",
4-
"version": "1.3.9",
4+
"version": "1.5.1",
55
"frcYear": "2026",
66
"uuid": "a604c6de-3573-4ba6-ae4f-32778238b9de",
77
"mavenUrls": [
@@ -13,7 +13,7 @@
1313
{
1414
"groupId": "ca.team4308",
1515
"artifactId": "absolutelib-java",
16-
"version": "1.3.9"
16+
"version": "1.5.1"
1717
}
1818
],
1919
"requires": [],

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
org.gradle.workers.max=1
22

33
group=ca.team4308
4-
version=1.4.0
4+
version=1.5.1
55

66
org.gradle.console=plain
77
org.gradle.logging.level=info

src/main/java/ca/team4308/absolutelib/math/trajectories/ScoringWeights.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
* }</pre>
4343
*
4444
* @see TrajectorySolver#setScoringWeights(ScoringWeights)
45-
*/
45+
* @deprecated Since 1.4.1 — the solver now uses a deterministic accuracy-first\n * scoring system. This class is retained for API compatibility only.\n */
46+
@Deprecated
4647
public final class ScoringWeights {
4748

4849
private final double accuracyWeight;

src/main/java/ca/team4308/absolutelib/math/trajectories/SolveDebugInfo.java

Lines changed: 26 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,10 @@
66
import ca.team4308.absolutelib.math.trajectories.physics.ProjectileMotion;
77

88
/**
9-
* Debug information from a solver run. Tracks why each candidate angle
10-
* was accepted or rejected and stores all candidate trajectory paths
11-
* for visualization.
9+
* Debug info from a solver run. Tracks accepted/rejected candidates
10+
* and their trajectory paths for visualization.
1211
*
13-
* <p>Enable debug mode on the solver with {@link TrajectorySolver#setDebugEnabled(boolean)}.
14-
* When enabled, the solver records every tested angle, its rejection reason (if any),
15-
* its score, and the full simulated trajectory. This data can be published to
16-
* NetworkTables for real-time visualization in AdvantageScope.</p>
17-
*
18-
* <pre>
19-
* solver.setDebugEnabled(true);
20-
* TrajectoryResult result = solver.solve(input);
21-
* SolveDebugInfo debug = result.getDebugInfo();
22-
* if (debug != null) {
23-
* System.out.println(debug.getSummary());
24-
* // Get all candidate trajectories for visualization
25-
* List&lt;SolveDebugInfo.CandidateInfo&gt; candidates = debug.getCandidates();
26-
* }
27-
* </pre>
12+
* Enable with {@link TrajectorySolver#setDebugEnabled(boolean)}.
2813
*/
2914
public class SolveDebugInfo {
3015

@@ -52,19 +37,19 @@ public enum RejectionReason {
5237
public static class CandidateInfo {
5338
private final double pitchDegrees;
5439
private final RejectionReason rejection;
55-
private final double score;
40+
private final double missDistance;
5641
private final double closestApproach;
5742
private final double maxHeight;
5843
private final double timeOfFlight;
5944
private final boolean hitTarget;
6045
private final ProjectileMotion.TrajectoryState[] trajectory;
6146

62-
public CandidateInfo(double pitchDegrees, RejectionReason rejection, double score,
47+
public CandidateInfo(double pitchDegrees, RejectionReason rejection, double missDistance,
6348
double closestApproach, double maxHeight, double timeOfFlight,
6449
boolean hitTarget, ProjectileMotion.TrajectoryState[] trajectory) {
6550
this.pitchDegrees = pitchDegrees;
6651
this.rejection = rejection;
67-
this.score = score;
52+
this.missDistance = missDistance;
6853
this.closestApproach = closestApproach;
6954
this.maxHeight = maxHeight;
7055
this.timeOfFlight = timeOfFlight;
@@ -75,7 +60,11 @@ public CandidateInfo(double pitchDegrees, RejectionReason rejection, double scor
7560
public double getPitchDegrees() { return pitchDegrees; }
7661
public RejectionReason getRejection() { return rejection; }
7762
public boolean isAccepted() { return rejection == RejectionReason.NONE; }
78-
public double getScore() { return score; }
63+
/** @deprecated Use {@link #getMissDistance()} instead. */
64+
@Deprecated
65+
public double getScore() { return missDistance; }
66+
/** Horizontal distance from target center (meters). 0 = dead center. */
67+
public double getMissDistance() { return missDistance; }
7968
public double getClosestApproach() { return closestApproach; }
8069
public double getMaxHeight() { return maxHeight; }
8170
public double getTimeOfFlight() { return timeOfFlight; }
@@ -85,8 +74,8 @@ public CandidateInfo(double pitchDegrees, RejectionReason rejection, double scor
8574
@Override
8675
public String toString() {
8776
if (rejection == RejectionReason.NONE) {
88-
return String.format(" %5.1f° ACCEPTED score=%.1f closest=%.3fm TOF=%.3fs maxH=%.2fm",
89-
pitchDegrees, score, closestApproach, timeOfFlight, maxHeight);
77+
return String.format(" %5.1f° ACCEPTED miss=%.4fm closest=%.3fm TOF=%.3fs maxH=%.2fm",
78+
pitchDegrees, missDistance, closestApproach, timeOfFlight, maxHeight);
9079
} else {
9180
return String.format(" %5.1f° %-18s closest=%.3fm TOF=%.3fs maxH=%.2fm",
9281
pitchDegrees, rejection, closestApproach, timeOfFlight, maxHeight);
@@ -102,22 +91,22 @@ public String toString() {
10291
private int rejectedMiss = 0;
10392
private int rejectedFlyover = 0;
10493
private int accepted = 0;
105-
private double bestScore = Double.NEGATIVE_INFINITY;
94+
private double bestMissDistance = Double.POSITIVE_INFINITY;
10695
private double bestPitchDegrees = Double.NaN;
10796

10897
/**
10998
* Records a candidate that was accepted (passed all filters).
11099
*/
111-
public void recordAccepted(double pitchDeg, double score, double closestApproach,
100+
public void recordAccepted(double pitchDeg, double missDistance, double closestApproach,
112101
double maxHeight, double tof, boolean hitTarget,
113102
ProjectileMotion.TrajectoryState[] trajectory) {
114103
totalTested++;
115104
accepted++;
116-
if (score > bestScore) {
117-
bestScore = score;
105+
if (missDistance < bestMissDistance) {
106+
bestMissDistance = missDistance;
118107
bestPitchDegrees = pitchDeg;
119108
}
120-
candidates.add(new CandidateInfo(pitchDeg, RejectionReason.NONE, score,
109+
candidates.add(new CandidateInfo(pitchDeg, RejectionReason.NONE, missDistance,
121110
closestApproach, maxHeight, tof, hitTarget, trajectory));
122111
}
123112

@@ -168,7 +157,11 @@ public List<CandidateInfo> getRejectedCandidates() {
168157
public int getRejectedMissCount() { return rejectedMiss; }
169158
public int getRejectedFlyoverCount() { return rejectedFlyover; }
170159
public int getTotalRejected() { return totalTested - accepted; }
171-
public double getBestScore() { return bestScore; }
160+
/** @deprecated Use {@link #getBestMissDistance()} instead. */
161+
@Deprecated
162+
public double getBestScore() { return bestMissDistance; }
163+
/** Horizontal distance from target center for the best accepted candidate (meters). 0 = dead center. */
164+
public double getBestMissDistance() { return bestMissDistance; }
172165
public double getBestPitchDegrees() { return bestPitchDegrees; }
173166

174167
/**
@@ -181,7 +174,7 @@ public String getSummary() {
181174
sb.append(String.format(" Rejected by: collision=%d, arcTooLow=%d, clearance=%d, miss=%d, flyover=%d\n",
182175
rejectedCollision, rejectedArcTooLow, rejectedClearance, rejectedMiss, rejectedFlyover));
183176
if (!Double.isNaN(bestPitchDegrees)) {
184-
sb.append(String.format(" Best: %.1f° (score=%.1f)\n", bestPitchDegrees, bestScore));
177+
sb.append(String.format(" Best: %.1f° (miss=%.4fm)\n", bestPitchDegrees, bestMissDistance));
185178
} else {
186179
sb.append(" No valid solution found.\n");
187180
}
@@ -193,8 +186,8 @@ public String getSummary() {
193186
*/
194187
public String getDetailedTable() {
195188
StringBuilder sb = new StringBuilder();
196-
sb.append("Pitch° Status Score Closest TOF MaxHeight\n");
197-
sb.append("------ ------ ----- ------- --- ---------\n");
189+
sb.append("Pitch° Status Miss(m) Closest TOF MaxHeight\n");
190+
sb.append("------ ------ ------- ------- --- ---------\n");
198191
for (CandidateInfo c : candidates) {
199192
sb.append(c.toString()).append('\n');
200193
}

0 commit comments

Comments
 (0)