Skip to content

Commit b7d60bb

Browse files
Huoleitfarbod-farshidian
authored andcommitted
Merged in fix/includeFinalEvent (pull request #615)
fix trajectory spreading - final time can be post-event Approved-by: Farbod Farshidian
2 parents 54744b0 + ea4c76a commit b7d60bb

File tree

9 files changed

+148
-77
lines changed

9 files changed

+148
-77
lines changed

ocs2_core/src/Types.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ std::string checkSize(int stateDim, int inputDim, const ScalarFunctionQuadraticA
189189
if (data.dfdux.rows() != inputDim) {
190190
errorDescription << dataName << ".dfdux.rows() != " << inputDim << "\n";
191191
}
192-
if (data.dfdux.cols() != stateDim) {
192+
if (data.dfdux.cols() != stateDim && inputDim > 0) {
193193
errorDescription << dataName << ".dfdux.cols() != " << stateDim << "\n";
194194
}
195195

ocs2_ddp/include/ocs2_ddp/DDP_HelperFunctions.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,26 @@ void retrieveActiveNormalizedTime(const std::pair<int, int>& partitionInterval,
135135
const size_array_t& postEventIndices, scalar_array_t& normalizedTimeTrajectory,
136136
size_array_t& normalizedPostEventIndices);
137137

138+
/**
139+
* Get the Partition Intervals From Time Trajectory. Intervals are defined as [start, end).
140+
*
141+
* Pay attention, the rightmost index of the end partition is (..., timeArray.size() - 1) , as the last value function is filled manually.
142+
* The reason is though we don’t write to the end index, we do have to read it. Adding the last index to the final partition will
143+
* cause a segmentation fault. There is no trivial method to distinguish the final partition from other partitions because, by design,
144+
* partitions should be treated equally.
145+
*
146+
* Every time point that is equal or larger to the desiredPartitionPoint should be included in that partition. This logic here is the same
147+
* as the event times.
148+
*
149+
* The last time of desiredPartitionPoints is filled manually. There is no round-off error involved. So it is safe to use == for
150+
* floating-point numbers. The last time point is naturally included by using std::lower_bound.
151+
*
152+
* @param [in] timeTrajectory: time trajectory that will be divided
153+
* @param [in] numWorkers: number of worker i.e. number of partitions
154+
* @return array of index pairs indicating the start and end of each partition
155+
*/
156+
std::vector<std::pair<int, int>> computePartitionIntervals(const scalar_array_t& timeTrajectory, int numWorkers);
157+
138158
/**
139159
* Gets a reference to the linear controller from the given primal solution.
140160
*/

ocs2_ddp/include/ocs2_ddp/GaussNewtonDDP.h

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -229,26 +229,6 @@ class GaussNewtonDDP : public SolverBase {
229229
return getValueFunctionImpl(time, state, cachedPrimalData_.primalSolution, cachedDualData_.valueFunctionTrajectory);
230230
}
231231

232-
/**
233-
* Get the Partition Intervals From Time Trajectory. Intervals are defined as [start, end).
234-
*
235-
* Pay attention, the rightmost index of the end partition is (..., timeArray.size() - 1) , as the last value function is filled manually.
236-
* The reason is though we don’t write to the end index, we do have to read it. Adding the last index to the final partition will
237-
* cause a segmentation fault. There is no trivial method to distinguish the final partition from other partitions because, by design,
238-
* partitions should be treated equally.
239-
*
240-
* Every time point that is equal or larger to the desiredPartitionPoint should be included in that partition. This logic here is the same
241-
* as the event times.
242-
*
243-
* The last time of desiredPartitionPoints is filled manually. There is no round-off error involved. So it is safe to use == for
244-
* floating-point numbers. The last time point is naturally included by using std::lower_bound.
245-
*
246-
* @param [in] timeTrajectory: time trajectory that will be divided
247-
* @param [in] numWorkers: number of worker i.e. number of partitions
248-
* @return array of index pairs indicating the start and end of each partition
249-
*/
250-
std::vector<std::pair<int, int>> getPartitionIntervalsFromTimeTrajectory(const scalar_array_t& timeTrajectory, int numWorkers);
251-
252232
/**
253233
* Forward integrate the system dynamics with given controller in primalSolution and operating trajectories. In general, it uses
254234
* the given control policies and initial state, to integrate the system dynamics in the time period [initTime, finalTime].

ocs2_ddp/src/DDP_HelperFunctions.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,4 +300,33 @@ void retrieveActiveNormalizedTime(const std::pair<int, int>& partitionInterval,
300300
[N, &partitionInterval](size_t i) -> size_t { return N - i + partitionInterval.first; });
301301
}
302302

303+
/******************************************************************************************************/
304+
/******************************************************************************************************/
305+
/******************************************************************************************************/
306+
std::vector<std::pair<int, int>> computePartitionIntervals(const scalar_array_t& timeTrajectory, int numWorkers) {
307+
const scalar_t increment = (timeTrajectory.back() - timeTrajectory.front()) / static_cast<scalar_t>(numWorkers);
308+
309+
scalar_array_t desiredPartitionPoints(numWorkers + 1);
310+
desiredPartitionPoints.front() = timeTrajectory.front();
311+
for (size_t i = 1u; i < desiredPartitionPoints.size() - 1; i++) {
312+
desiredPartitionPoints[i] = desiredPartitionPoints[i - 1] + increment;
313+
}
314+
desiredPartitionPoints.back() = timeTrajectory.back();
315+
316+
std::vector<std::pair<int, int>> partitionIntervals;
317+
partitionIntervals.reserve(desiredPartitionPoints.size());
318+
319+
int endPos, startPos = 0;
320+
for (size_t i = 1u; i < desiredPartitionPoints.size(); i++) {
321+
const auto itr = std::upper_bound(timeTrajectory.begin(), timeTrajectory.end(), desiredPartitionPoints[i]);
322+
endPos = (itr != timeTrajectory.end()) ? std::distance(timeTrajectory.begin(), itr) : (timeTrajectory.size() - 1);
323+
if (endPos != startPos) {
324+
partitionIntervals.emplace_back(startPos, endPos);
325+
startPos = endPos;
326+
}
327+
}
328+
329+
return partitionIntervals;
330+
}
331+
303332
} // namespace ocs2

ocs2_ddp/src/GaussNewtonDDP.cpp

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -369,35 +369,6 @@ vector_t GaussNewtonDDP::getStateInputEqualityConstraintLagrangianImpl(scalar_t
369369
return DmDagger.transpose() * temp;
370370
}
371371

372-
/******************************************************************************************************/
373-
/******************************************************************************************************/
374-
/******************************************************************************************************/
375-
std::vector<std::pair<int, int>> GaussNewtonDDP::getPartitionIntervalsFromTimeTrajectory(const scalar_array_t& timeTrajectory,
376-
int numWorkers) {
377-
scalar_array_t desiredPartitionPoints(numWorkers + 1);
378-
desiredPartitionPoints.front() = timeTrajectory.front();
379-
380-
const scalar_t increment = (timeTrajectory.back() - timeTrajectory.front()) / static_cast<scalar_t>(numWorkers);
381-
for (size_t i = 1u; i < desiredPartitionPoints.size() - 1; i++) {
382-
desiredPartitionPoints[i] = desiredPartitionPoints[i - 1] + increment;
383-
}
384-
desiredPartitionPoints.back() = timeTrajectory.back();
385-
386-
std::vector<std::pair<int, int>> partitionIntervals;
387-
partitionIntervals.reserve(desiredPartitionPoints.size());
388-
389-
int endPos, startPos = 0;
390-
for (size_t i = 1u; i < desiredPartitionPoints.size(); i++) {
391-
const scalar_t& time = desiredPartitionPoints[i];
392-
endPos = std::distance(timeTrajectory.begin(), std::lower_bound(timeTrajectory.begin(), timeTrajectory.end(), time));
393-
if (endPos != startPos) {
394-
partitionIntervals.emplace_back(startPos, endPos);
395-
startPos = endPos;
396-
}
397-
}
398-
399-
return partitionIntervals;
400-
}
401372
/******************************************************************************************************/
402373
/******************************************************************************************************/
403374
/******************************************************************************************************/
@@ -534,8 +505,7 @@ scalar_t GaussNewtonDDP::solveSequentialRiccatiEquationsImpl(const ScalarFunctio
534505
riccatiEquationsWorker(0, partitionInterval, finalValueFunction);
535506
} else { // solve it in parallel
536507
// do equal-time partitions based on available thread resource
537-
std::vector<std::pair<int, int>> partitionIntervals =
538-
getPartitionIntervalsFromTimeTrajectory(nominalPrimalData_.primalSolution.timeTrajectory_, ddpSettings_.nThreads_);
508+
const auto partitionIntervals = computePartitionIntervals(nominalPrimalData_.primalSolution.timeTrajectory_, ddpSettings_.nThreads_);
539509

540510
// hold the final value function of each partition
541511
std::vector<ScalarFunctionQuadraticApproximation> finalValueFunctionOfEachPartition(partitionIntervals.size());
@@ -559,7 +529,14 @@ scalar_t GaussNewtonDDP::solveSequentialRiccatiEquationsImpl(const ScalarFunctio
559529
if (ddpSettings_.checkNumericalStability_) {
560530
const int N = nominalPrimalData_.primalSolution.timeTrajectory_.size();
561531
for (int k = N - 1; k >= 0; k--) {
562-
const auto errorDescription = checkBeingPSD(nominalDualData_.valueFunctionTrajectory[k], "ValueFunction");
532+
// check size
533+
auto errorDescription = checkSize(nominalPrimalData_.primalSolution.stateTrajectory_[k].size(), 0,
534+
nominalDualData_.valueFunctionTrajectory[k], "ValueFunction");
535+
if (!errorDescription.empty()) {
536+
throw std::runtime_error(errorDescription);
537+
}
538+
// check PSD
539+
errorDescription = checkBeingPSD(nominalDualData_.valueFunctionTrajectory[k], "ValueFunction");
563540
if (!errorDescription.empty()) {
564541
std::stringstream throwMsg;
565542
throwMsg << "at time " << nominalPrimalData_.primalSolution.timeTrajectory_[k] << ":\n";

ocs2_ddp/src/SLQ.cpp

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,6 @@ void SLQ::riccatiEquationsWorker(size_t workerIndex, const std::pair<int, int>&
239239
* SsNormalized = [-10.0, ..., -2.0, -1.0, -0.0]
240240
*/
241241
vector_array_t& allSsTrajectory = allSsTrajectoryStock_[workerIndex];
242-
allSsTrajectory.clear();
243242
integrateRiccatiEquationNominalTime(*riccatiIntegratorPtrStock_[workerIndex], *riccatiEquationsPtrStock_[workerIndex], partitionInterval,
244243
nominalTimeTrajectory, nominalEventsPastTheEndIndices, std::move(allSsFinal), SsNormalizedTime,
245244
SsNormalizedPostEventIndices, allSsTrajectory);
@@ -266,7 +265,6 @@ void SLQ::integrateRiccatiEquationNominalTime(IntegratorBase& riccatiIntegrator,
266265
const int nominalTimeSize = SsNormalizedTime.size();
267266
const int numEvents = SsNormalizedPostEventIndices.size();
268267
auto partitionDuration = nominalTimeTrajectory[partitionInterval.second] - nominalTimeTrajectory[partitionInterval.first];
269-
const auto numTimeSteps = static_cast<size_t>(settings().maxNumStepsPerSecond_ * std::max(1.0, partitionDuration));
270268

271269
// Normalized switching time indices, start and end of the partition are added at the beginning and end
272270
using iterator_t = scalar_array_t::const_iterator;
@@ -281,15 +279,16 @@ void SLQ::integrateRiccatiEquationNominalTime(IntegratorBase& riccatiIntegrator,
281279

282280
// integrating the Riccati equations
283281
allSsTrajectory.clear();
284-
allSsTrajectory.reserve(numTimeSteps);
282+
allSsTrajectory.reserve(nominalTimeSize);
285283
for (int i = 0; i <= numEvents; i++) {
286284
iterator_t beginTimeItr = SsNormalizedSwitchingTimesIndices[i].first;
287285
iterator_t endTimeItr = SsNormalizedSwitchingTimesIndices[i].second;
288286

289-
Observer observer(&allSsTrajectory);
290287
// solve Riccati equations
288+
Observer observer(&allSsTrajectory);
289+
const auto maxNumTimeSteps = static_cast<size_t>(settings().maxNumStepsPerSecond_ * std::max(1.0, partitionDuration));
291290
riccatiIntegrator.integrateTimes(riccatiEquation, observer, allSsFinal, beginTimeItr, endTimeItr, settings().timeStep_,
292-
settings().absTolODE_, settings().relTolODE_, numTimeSteps);
291+
settings().absTolODE_, settings().relTolODE_, maxNumTimeSteps);
293292

294293
if (i < numEvents) {
295294
allSsFinal = riccatiEquation.computeJumpMap(*endTimeItr, allSsTrajectory.back());

ocs2_oc/include/ocs2_oc/trajectory_adjustment/TrajectorySpreading.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,13 @@ class TrajectorySpreading final {
123123
*/
124124
size_array_t findPostEventIndices(const scalar_array_t& eventTimes, const scalar_array_t& timeTrajectory) const {
125125
size_array_t postEventIndices(eventTimes.size());
126-
std::transform(eventTimes.cbegin(), eventTimes.cend(), postEventIndices.begin(),
127-
[this, &timeTrajectory](scalar_t time) -> int { return upperBoundIndex(timeTrajectory, time); });
126+
for (std::size_t i = 0; i < eventTimes.size(); i++) {
127+
if (i == eventTimes.size() - 1 && eventTimes[i] == timeTrajectory.back()) {
128+
postEventIndices[i] = timeTrajectory.size() - 1;
129+
} else {
130+
postEventIndices[i] = upperBoundIndex(timeTrajectory, eventTimes[i]);
131+
}
132+
}
128133
return postEventIndices;
129134
}
130135

ocs2_oc/src/trajectory_adjustment/TrajectorySpreading.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ auto TrajectorySpreading::set(const ModeSchedule& oldModeSchedule, const ModeSch
170170
// if last mode of the new mode sequence is NOT matched
171171
else if (!isLastActiveModeOfNewModeSequenceMatched) {
172172
const auto mismatchEventTime = newModeSchedule.eventTimes[newStartIndexOfMatchedSequence + w - 1];
173-
eraseFromIndex_ = upperBoundIndex(oldTimeTrajectory, mismatchEventTime);
173+
eraseFromIndex_ = lowerBoundIndex(oldTimeTrajectory, mismatchEventTime);
174174
}
175175

176176
// computes the index of the spreading values and intervals

ocs2_oc/test/trajectory_adjustment/TrajectorySpreadingTest.cpp

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ struct Result {
5252
ocs2::size_array_t preEventModeTrajectory;
5353
};
5454

55+
std::size_t modeAtTime(const ocs2::ModeSchedule& modeSchedule, ocs2::scalar_t time, bool isFinalTime) {
56+
if (isFinalTime) {
57+
auto timeItr = std::upper_bound(modeSchedule.eventTimes.begin(), modeSchedule.eventTimes.end(), time);
58+
int modeIndex = std::distance(modeSchedule.eventTimes.begin(), timeItr);
59+
return modeSchedule.modeSequence[modeIndex];
60+
} else {
61+
return modeSchedule.modeAtTime(time);
62+
}
63+
}
64+
5565
class TrajectorySpreadingTest : public testing::Test {
5666
protected:
5767
static constexpr size_t STATE_DIM = 2;
@@ -99,8 +109,9 @@ class TrajectorySpreadingTest : public testing::Test {
99109
[&out](size_t eventIndex) { return out.stateTrajectory[eventIndex - 1]; });
100110

101111
out.modeTrajectory.resize(out.timeTrajectory.size());
102-
std::transform(out.timeTrajectory.begin(), out.timeTrajectory.end(), out.modeTrajectory.begin(),
103-
[&modeSchedule](ocs2::scalar_t time) { return modeSchedule.modeAtTime(time); });
112+
for (int i = 0; i < out.modeTrajectory.size(); i++) {
113+
out.modeTrajectory[i] = modeAtTime(modeSchedule, out.timeTrajectory[i], i == out.modeTrajectory.size() - 1 ? true : false);
114+
}
104115

105116
out.preEventModeTrajectory.resize(out.postEventsIndeces.size());
106117
std::transform(out.postEventsIndeces.begin(), out.postEventsIndeces.end(), out.preEventModeTrajectory.begin(),
@@ -194,17 +205,22 @@ class TrajectorySpreadingTest : public testing::Test {
194205
const ocs2::scalar_t finalTime = spreadResult.timeTrajectory.back();
195206

196207
const auto startEventItr = std::upper_bound(updatedModeSchedule.eventTimes.begin(), updatedModeSchedule.eventTimes.end(), initTime);
197-
// If a time point is aligned with(the same as) an event time, it belongs to the pre-mode.
198-
const auto endEventItr = std::lower_bound(updatedModeSchedule.eventTimes.begin(), updatedModeSchedule.eventTimes.end(), finalTime);
199-
200-
ocs2::size_array_t postEventIndice(endEventItr - startEventItr);
201-
std::transform(startEventItr, endEventItr, postEventIndice.begin(), [&spreadResult](ocs2::scalar_t time) {
202-
auto timeItr = std::upper_bound(spreadResult.timeTrajectory.begin(), spreadResult.timeTrajectory.end(), time);
203-
return std::distance(spreadResult.timeTrajectory.begin(), timeItr);
204-
});
208+
// If the final time is aligned with(the same as) an event time, it belongs to the post-mode.
209+
const auto endEventItr = std::upper_bound(updatedModeSchedule.eventTimes.begin(), updatedModeSchedule.eventTimes.end(), finalTime);
210+
211+
ocs2::size_array_t postEventIndices(endEventItr - startEventItr);
212+
for (auto itr = startEventItr; itr != endEventItr; itr++) {
213+
int index = std::distance(startEventItr, itr);
214+
if (itr == endEventItr - 1 && *itr == spreadResult.timeTrajectory.back()) {
215+
postEventIndices[index] = spreadResult.timeTrajectory.size() - 1;
216+
} else {
217+
auto timeItr = std::upper_bound(spreadResult.timeTrajectory.begin(), spreadResult.timeTrajectory.end(), *itr);
218+
postEventIndices[index] = std::distance(spreadResult.timeTrajectory.begin(), timeItr);
219+
}
220+
}
205221

206-
EXPECT_TRUE(spreadResult.postEventsIndeces.size() == postEventIndice.size());
207-
EXPECT_TRUE(std::equal(postEventIndice.begin(), postEventIndice.end(), spreadResult.postEventsIndeces.begin()));
222+
EXPECT_TRUE(spreadResult.postEventsIndeces.size() == postEventIndices.size());
223+
EXPECT_TRUE(std::equal(postEventIndices.begin(), postEventIndices.end(), spreadResult.postEventsIndeces.begin()));
208224

209225
} else {
210226
EXPECT_EQ(spreadResult.postEventsIndeces.size(), 0);
@@ -220,9 +236,10 @@ class TrajectorySpreadingTest : public testing::Test {
220236
auto eventIndexActualItr = spreadResult.postEventsIndeces.begin();
221237
auto eventTimeReferenceInd = ocs2::lookup::findIndexInTimeArray(updatedModeSchedule.eventTimes, period.first);
222238
for (size_t k = 0; k < spreadResult.timeTrajectory.size(); k++) {
223-
// time should be monotonic sequence
224-
if (k > 0) {
225-
EXPECT_TRUE(spreadResult.timeTrajectory[k - 1] < spreadResult.timeTrajectory[k]);
239+
// Time should be monotonic sequence except the final time. It is possible that the last two time points have the same time, but one
240+
// stands for pre-mode and the other stands for post-mode.
241+
if (k > 0 && k < spreadResult.timeTrajectory.size() - 1) {
242+
EXPECT_TRUE(spreadResult.timeTrajectory[k - 1] < spreadResult.timeTrajectory[k]) << "TimeIndex: " << k;
226243
}
227244

228245
// Pre-event time should be equal to the event time
@@ -238,7 +255,9 @@ class TrajectorySpreadingTest : public testing::Test {
238255
eventIndexActualItr++;
239256
}
240257
// mode should match the given modeSchedule
241-
EXPECT_TRUE(updatedModeSchedule.modeAtTime(spreadResult.timeTrajectory[k]) == spreadResult.modeTrajectory[k]);
258+
auto updatedMode =
259+
modeAtTime(updatedModeSchedule, spreadResult.timeTrajectory[k], k == spreadResult.timeTrajectory.size() - 1 ? true : false);
260+
EXPECT_TRUE(updatedMode == spreadResult.modeTrajectory[k]);
242261
} // end of k loop
243262

244263
// test postEventsIndeces
@@ -319,6 +338,48 @@ TEST_F(TrajectorySpreadingTest, partially_matching_modes) {
319338
EXPECT_TRUE(status.willPerformTrajectorySpreading);
320339
}
321340

341+
TEST_F(TrajectorySpreadingTest, final_time_is_the_same_as_event_time_1) {
342+
const ocs2::scalar_array_t eventTimes{1.1, 1.3};
343+
const ocs2::size_array_t modeSequence{0, 1, 2};
344+
345+
const ocs2::scalar_array_t updatedEventTimes{1.1, 2.1};
346+
const ocs2::size_array_t updatedModeSequence{0, 1, 2};
347+
348+
const std::pair<ocs2::scalar_t, ocs2::scalar_t> period{0.2, 2.1};
349+
const auto status = checkResults({eventTimes, modeSequence}, {updatedEventTimes, updatedModeSequence}, period);
350+
351+
EXPECT_FALSE(status.willTruncate);
352+
EXPECT_TRUE(status.willPerformTrajectorySpreading);
353+
}
354+
355+
TEST_F(TrajectorySpreadingTest, final_time_is_the_same_as_event_time_2) {
356+
const ocs2::scalar_array_t eventTimes{1.1, 2.1};
357+
const ocs2::size_array_t modeSequence{0, 1, 2};
358+
359+
const ocs2::scalar_array_t updatedEventTimes{1.1, 1.3};
360+
const ocs2::size_array_t updatedModeSequence{0, 1, 2};
361+
362+
const std::pair<ocs2::scalar_t, ocs2::scalar_t> period{0.2, 2.1};
363+
const auto status = checkResults({eventTimes, modeSequence}, {updatedEventTimes, updatedModeSequence}, period);
364+
365+
EXPECT_FALSE(status.willTruncate);
366+
EXPECT_TRUE(status.willPerformTrajectorySpreading);
367+
}
368+
369+
TEST_F(TrajectorySpreadingTest, erase_trajectory) {
370+
const ocs2::scalar_array_t eventTimes{1.1, 1.3};
371+
const ocs2::size_array_t modeSequence{0, 1, 2};
372+
373+
const ocs2::scalar_array_t updatedEventTimes{1.1, 1.3};
374+
const ocs2::size_array_t updatedModeSequence{0, 1, 3};
375+
376+
const std::pair<ocs2::scalar_t, ocs2::scalar_t> period{0.2, 2.1};
377+
const auto status = checkResults({eventTimes, modeSequence}, {updatedEventTimes, updatedModeSequence}, period);
378+
379+
EXPECT_TRUE(status.willTruncate);
380+
EXPECT_FALSE(status.willPerformTrajectorySpreading);
381+
}
382+
322383
TEST_F(TrajectorySpreadingTest, fully_matched_modes) {
323384
const ocs2::scalar_array_t eventTimes{1.1, 1.3};
324385
const ocs2::size_array_t modeSequence{0, 1, 2};

0 commit comments

Comments
 (0)