forked from Team254/FRC-2019-Public
-
Notifications
You must be signed in to change notification settings - Fork 0
/
MotionProfile.java
316 lines (288 loc) · 9.58 KB
/
MotionProfile.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
package com.team254.lib.motion;
import static com.team254.lib.util.Util.epsilonEquals;
import static com.team254.lib.motion.MotionUtil.kEpsilon;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
/**
* A motion profile specifies a 1D time-parameterized trajectory. The trajectory is composed of successively coincident
* MotionSegments from which the desired state of motion at any given distance or time can be calculated.
*/
public class MotionProfile {
protected List<MotionSegment> mSegments;
/**
* Create an empty MotionProfile.
*/
public MotionProfile() {
mSegments = new ArrayList<>();
}
/**
* Create a MotionProfile from an existing list of segments (note that validity is not checked).
*
* @param segments The new segments of the profile.
*/
public MotionProfile(List<MotionSegment> segments) {
mSegments = segments;
}
/**
* Checks if the given MotionProfile is valid. This checks that:
* <p>
* 1. All segments are valid.
* <p>
* 2. Successive segments are C1 continuous in position and C0 continuous in velocity.
*
* @return True if the MotionProfile is valid.
*/
public boolean isValid() {
MotionSegment prev_segment = null;
for (MotionSegment s : mSegments) {
if (!s.isValid()) {
return false;
}
if (prev_segment != null && !s.start().coincident(prev_segment.end())) {
// Adjacent segments are not continuous.
System.err.println("Segments not continuous! End: " + prev_segment.end() + ", Start: " + s.start());
return false;
}
prev_segment = s;
}
return true;
}
/**
* Check if the profile is empty.
*
* @return True if there are no segments.
*/
public boolean isEmpty() {
return mSegments.isEmpty();
}
/**
* Get the interpolated MotionState at any given time.
*
* @param t The time to query.
* @return Empty if the time is outside the time bounds of the profile, or the resulting MotionState otherwise.
*/
public Optional<MotionState> stateByTime(double t) {
if (t < startTime() && t + kEpsilon >= startTime()) {
return Optional.of(startState());
}
if (t > endTime() && t - kEpsilon <= endTime()) {
return Optional.of(endState());
}
for (MotionSegment s : mSegments) {
if (s.containsTime(t)) {
return Optional.of(s.start().extrapolate(t));
}
}
return Optional.empty();
}
/**
* Get the interpolated MotionState at any given time, clamping to the endpoints if time is out of bounds.
*
* @param t The time to query.
* @return The MotionState at time t, or closest to it if t is outside the profile.
*/
public MotionState stateByTimeClamped(double t) {
if (t < startTime()) {
return startState();
} else if (t > endTime()) {
return endState();
}
for (MotionSegment s : mSegments) {
if (s.containsTime(t)) {
return s.start().extrapolate(t);
}
}
// Should never get here.
return MotionState.kInvalidState;
}
/**
* Get the interpolated MotionState by distance (the "pos()" field of MotionState). Note that since a profile may
* reverse, this method only returns the *first* instance of this position.
*
* @param pos The position to query.
* @return Empty if the profile never crosses pos or if the profile is invalid, or the resulting MotionState
* otherwise.
*/
public Optional<MotionState> firstStateByPos(double pos) {
for (MotionSegment s : mSegments) {
if (s.containsPos(pos)) {
if (epsilonEquals(s.end().pos(), pos, kEpsilon)) {
return Optional.of(s.end());
}
final double t = Math.min(s.start().nextTimeAtPos(pos), s.end().t());
if (Double.isNaN(t)) {
System.err.println("Error! We should reach 'pos' but we don't");
return Optional.empty();
}
return Optional.of(s.start().extrapolate(t));
}
}
// We never reach pos.
return Optional.empty();
}
/**
* Remove all parts of the profile prior to the query time. This eliminates whole segments and also shortens any
* segments containing t.
*
* @param t The query time.
*/
public void trimBeforeTime(double t) {
for (Iterator<MotionSegment> iterator = mSegments.iterator(); iterator.hasNext(); ) {
MotionSegment s = iterator.next();
if (s.end().t() <= t) {
// Segment is fully before t.
iterator.remove();
continue;
}
if (s.start().t() <= t) {
// Segment begins before t; let's shorten the segment.
s.setStart(s.start().extrapolate(t));
}
break;
}
}
/**
* Remove all segments.
*/
public void clear() {
mSegments.clear();
}
/**
* Remove all segments and initialize to the desired state (actually a segment of length 0 that starts and ends at
* initial_state).
*
* @param initial_state The MotionState to initialize to.
*/
public void reset(MotionState initial_state) {
clear();
mSegments.add(new MotionSegment(initial_state, initial_state));
}
/**
* Remove redundant segments (segments whose start and end states are coincident).
*/
public void consolidate() {
for (Iterator<MotionSegment> iterator = mSegments.iterator(); iterator.hasNext() && mSegments.size() > 1; ) {
MotionSegment s = iterator.next();
if (s.start().coincident(s.end())) {
iterator.remove();
}
}
}
/**
* Add to the profile by applying an acceleration control for a given time. This is appended to the previous last
* state.
*
* @param acc The acceleration to apply.
* @param dt The period of time to apply the given acceleration.
*/
public void appendControl(double acc, double dt) {
if (isEmpty()) {
System.err.println("Error! Trying to append to empty profile");
return;
}
MotionState last_end_state = mSegments.get(mSegments.size() - 1).end();
MotionState new_start_state = new MotionState(last_end_state.t(), last_end_state.pos(), last_end_state.vel(),
acc);
appendSegment(new MotionSegment(new_start_state, new_start_state.extrapolate(new_start_state.t() + dt)));
}
/**
* Add to the profile by inserting a new segment. No validity checking is done.
*
* @param segment The segment to add.
*/
public void appendSegment(MotionSegment segment) {
mSegments.add(segment);
}
/**
* Add to the profile by inserting a new profile after the final state. No validity checking is done.
*
* @param profile The profile to add.
*/
public void appendProfile(MotionProfile profile) {
for (MotionSegment s : profile.segments()) {
appendSegment(s);
}
}
/**
* @return The number of segments.
*/
public int size() {
return mSegments.size();
}
/**
* @return The list of segments.
*/
public List<MotionSegment> segments() {
return mSegments;
}
/**
* @return The first state in the profile (or kInvalidState if empty).
*/
public MotionState startState() {
if (isEmpty()) {
return MotionState.kInvalidState;
}
return mSegments.get(0).start();
}
/**
* @return The time of the first state in the profile (or NaN if empty).
*/
public double startTime() {
return startState().t();
}
/**
* @return The pos of the first state in the profile (or NaN if empty).
*/
public double startPos() {
return startState().pos();
}
/**
* @return The last state in the profile (or kInvalidState if empty).
*/
public MotionState endState() {
if (isEmpty()) {
return MotionState.kInvalidState;
}
return mSegments.get(mSegments.size() - 1).end();
}
/**
* @return The time of the last state in the profile (or NaN if empty).
*/
public double endTime() {
return endState().t();
}
/**
* @return The pos of the last state in the profile (or NaN if empty).
*/
public double endPos() {
return endState().pos();
}
/**
* @return The duration of the entire profile.
*/
public double duration() {
return endTime() - startTime();
}
/**
* @return The total distance covered by the profile. Note that distance is the sum of absolute distances of all
* segments, so a reversing profile will count the distance covered in each direction.
*/
public double length() {
double length = 0.0;
for (MotionSegment s : segments()) {
length += Math.abs(s.end().pos() - s.start().pos());
}
return length;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("Profile:");
for (MotionSegment s : segments()) {
builder.append("\n\t");
builder.append(s);
}
return builder.toString();
}
}