Skip to content

Commit 5cf2dac

Browse files
authored
Merge pull request #4039 from opensim-org/path_fitter_segfault_fix
Fix bugs in `PolynomialPathFitter` related to the number of input coordinate samples
2 parents aa40605 + d9a5cca commit 5cf2dac

File tree

6 files changed

+76
-9
lines changed

6 files changed

+76
-9
lines changed

Bindings/Java/Matlab/examples/PolynomialPathFitter/examplePolynomialPathFitter.m

+22-3
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,35 @@
5050
%
5151
% The fitter will randomly sample around the coordinate values provided in the
5252
% table to generate model configurations for which to compute path lengths and
53-
% moment arms. This table has many more rows than are needed for the fitter to
54-
% generate a good fit, so we will remove some of the rows to speed up the
55-
% fitting process.
53+
% moment arms. In general, it is up to the user to decide how many sample points
54+
% are needed to adequately cover the range of motion of the model's coordinates.
55+
% In this case, the table has many more rows than are needed for the fitter to
56+
% generate a good fit, so we will remove some of the rows to speed up the fitting
57+
% process.
5658
values = TimeSeriesTable('coordinates.sto');
5759
times = values.getIndependentColumn();
5860
for i = times.size():-1:1
5961
if mod(i, 10) ~= 0
6062
values.removeRowAtIndex(i);
6163
end
6264
end
65+
66+
% Add columns for the toe joints. Use a sine function to generate the
67+
% coordinate values. An amplitude of 0.5 keeps the values within the coordinate
68+
% range of the MTP joints in the model.
69+
amplitude = 0.5;
70+
angular_frequency = 10.0; % rad/s
71+
phase = 0.0;
72+
sine = Sine(amplitude, angular_frequency, phase);
73+
mtp_values = Vector(values.getNumRows(), 0.0);
74+
time = Vector(1, 0.0);
75+
for i = 0:values.getNumRows()-1
76+
time.set(0, times.get(i));
77+
mtp_values.set(i, sine.calcValue(time));
78+
end
79+
80+
values.appendColumn('/jointset/mtp_r/mtp_angle_r/value', mtp_values);
81+
values.appendColumn('/jointset/mtp_l/mtp_angle_l/value', mtp_values);
6382
fitter.setCoordinateValues(TableProcessor(values));
6483

6584
% Configure optional settings

Bindings/Python/examples/PolynomialPathFitter/examplePolynomialPathFitter.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,33 @@
5050
#
5151
# The fitter will randomly sample around the coordinate values provided in the
5252
# table to generate model configurations for which to compute path lengths and
53-
# moment arms. This table has many more rows than are needed for the fitter to
54-
# generate a good fit, so we will remove some of the rows to speed up the
55-
# fitting process.
53+
# moment arms. In general, it is up to the user to decide how many sample points
54+
# are needed to adequately cover the range of motion of the model's coordinates.
55+
# In this case, the table has many more rows than are needed for the fitter to
56+
# generate a good fit, so we will remove some of the rows to speed up the fitting
57+
# process.
5658
values = osim.TimeSeriesTable('coordinates.sto')
5759
times = values.getIndependentColumn()
5860
for i in range(len(times)):
5961
if i % 10 != 0:
6062
values.removeRow(times[i])
6163

64+
# Add columns for the toe joints. Use a sine function to generate the
65+
# coordinate values. An amplitude of 0.5 keeps the values within the coordinate
66+
# range of the MTP joints in the model.
67+
amplitude = 0.5
68+
angular_frequency = 10.0 # rad/s
69+
phase = 0.0
70+
sine = osim.Sine(amplitude, angular_frequency, phase)
71+
mtp_values = osim.Vector(values.getNumRows(), 0.0)
72+
times = values.getIndependentColumn()
73+
time = osim.Vector(1, 0.0)
74+
for i in range(values.getNumRows()):
75+
time.set(0, times[i])
76+
mtp_values.set(i, sine.calcValue(time))
77+
78+
values.appendColumn('/jointset/mtp_r/mtp_angle_r/value', mtp_values)
79+
values.appendColumn('/jointset/mtp_l/mtp_angle_l/value', mtp_values)
6280
fitter.setCoordinateValues(osim.TableProcessor(values))
6381

6482
# Configure optional settings

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ v4.6
8888
other path-based force elements dependent on a user-provided expression. (#4035)
8989
- Fixed a bug where `DeGrooteFregly2016Muscle::getBoundsNormalizedFiberLength()` was returning
9090
tendon force bounds rather than fiber length bounds. (#4040)
91+
- Fixed bugs in `PolynomialPathFitter` when too few coordinate samples were provided. (#4039)
9192

9293

9394
v4.5.1

OpenSim/Actuators/PolynomialPathFitter.cpp

+8-1
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@ void PolynomialPathFitter::run() {
170170
"Expected 'num_parallel_threads' to be between 1 and {}, but "
171171
"received {}.", std::thread::hardware_concurrency(),
172172
get_num_parallel_threads())
173+
OPENSIM_THROW_IF_FRMOBJ(
174+
static_cast<int>(values.getNumRows()) < get_num_parallel_threads(),
175+
Exception, "Expected the number of time points in the coordinate "
176+
"values table to be greater than 'num_parallel_threads', but "
177+
"received {} and {}, respectively.",
178+
values.getNumRows(), get_num_parallel_threads())
173179
log_info("Number of parallel threads = {}", get_num_parallel_threads());
174180

175181
// Number of samples per frame.
@@ -662,7 +668,8 @@ TimeSeriesTable PolynomialPathFitter::sampleCoordinateValues(
662668
int timeIdx = 0;
663669
const auto& times = values.getIndependentColumn();
664670
TimeSeriesTable valuesSampled;
665-
double dt = (times[1] - times[0]) / (get_num_samples_per_frame() + 2);
671+
double dt = (times.size() < 2) ? 0.01 :
672+
(times[1] - times[0]) / (get_num_samples_per_frame() + 2);
666673
for (int i = 0; i < get_num_parallel_threads(); ++i) {
667674
int numTimeIndexes = outputs[i].nrow() / get_num_samples_per_frame();
668675
for (int j = 0; j < numTimeIndexes; ++j) {

OpenSim/Actuators/PolynomialPathFitter.h

+7
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,13 @@ class OSIMACTUATORS_API PolynomialPathFitterBounds : public Object {
151151
* for the fitting process, or if the fit for a particular path did not meet the
152152
* specified tolerances.
153153
*
154+
* In general, it is up to the user to decide how many sample points are needed
155+
* to adequately cover the range of motion of the model's coordinates. As the
156+
* complexity of a muscle path increases, more sample points over a larger
157+
* dimension of coordinate values are needed to achieve a good fit. Users may
158+
* consider manually creating the coordinates value table to ensure that the
159+
* sampling covers the full range of motion for the model.
160+
*
154161
* It is highly recommended to use the files printed to the output directory to
155162
* evaluate the quality of the fitted paths (see `setOutputDirectory()` for more
156163
* details). Depending on the quality of the original model, it may not be

OpenSim/Actuators/Test/testPolynomialPathFitter.cpp

+17-2
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ namespace {
5757
return processor;
5858
}
5959

60-
TableProcessor createCoordinatesTable(bool correctLabel, bool addMetaData) {
61-
SimTK::Vector column = SimTK::Test::randVector(100);
60+
TableProcessor createCoordinatesTable(bool correctLabel, bool addMetaData,
61+
int numRows = 100) {
62+
SimTK::Vector column = SimTK::Test::randVector(numRows);
6263
std::vector<double> times;
6364
times.reserve(column.size());
6465
for (int i = 0; i < column.size(); ++i) {
@@ -130,3 +131,17 @@ TEST_CASE("Invalid configurations") {
130131
"Expected 'maximum_polynomial_order' to be at most 9"));
131132
}
132133
}
134+
135+
TEST_CASE("Number of rows less than the number of threads") {
136+
int numAvailableThreads = std::thread::hardware_concurrency();
137+
int numRows = (numAvailableThreads == 1) ? 1 : numAvailableThreads - 1;
138+
CAPTURE(numAvailableThreads);
139+
CAPTURE(numRows);
140+
141+
PolynomialPathFitter fitter;
142+
fitter.setModel(createHangingMuscleModel());
143+
fitter.setCoordinateValues(createCoordinatesTable(true, true, numRows));
144+
fitter.setNumParallelThreads(numAvailableThreads);
145+
REQUIRE_THROWS_WITH(fitter.run(), ContainsSubstring(
146+
"Expected the number of time points in the coordinate values table"));
147+
}

0 commit comments

Comments
 (0)