Force Plate and Inverse Dynamics Workflow

This vignette demonstrates a standard lower-limb kinetics workflow in PhysioMoCap:

  1. Filter force-plate GRF signals.
  2. Compute center of pressure (COP).
  3. Extract loading rate and impulse.
  4. Estimate joint moments and power with planar inverse dynamics.
  5. Run multi-plate selection and 3D inverse dynamics.

1. Simulate force-plate inputs

library(PhysioMoCap)

set.seed(1)
n <- 1000
sr <- 1000

forces <- cbind(
  Fx = rnorm(n, 0, 8),
  Fy = rnorm(n, 0, 8),
  Fz = c(rep(0, 200), abs(sin(seq(0, pi, length.out = 600))) * 900, rep(0, 200))
)

moments <- cbind(
  Mx = rnorm(n, 0, 25),
  My = rnorm(n, 0, 25),
  Mz = rnorm(n, 0, 10)
)

2. Run force-plate analysis

out <- analyzeForcePlate(
  forces = forces,
  moments = moments,
  sampling_rate = sr,
  cutoff = 20,
  threshold = 20,
  filter_method = "moving_average"
)

out$summary
#>   peak_vertical_force max_loading_rate total_impulse n_stances
#> 1            897.3174         4664.171      342.8947         1
head(out$loading_rate)
#>   stance onset peak time_to_peak peak_force loading_rate
#> 1      1   197  500        0.303   897.3174     4664.171
head(out$impulse)
#>   stance onset offset duration_s  impulse
#> 1      1   197    804      0.607 342.8947

3. Build inverse-dynamics inputs

t <- seq(0, (n - 1) / sr, length.out = n)

joints <- data.frame(
  ankle_x = rep(0.00, n),
  ankle_y = rep(0.05, n),
  knee_x = rep(0.00, n),
  knee_y = rep(0.45, n),
  hip_x = rep(0.00, n),
  hip_y = rep(0.85, n)
)

grf_2d <- data.frame(
  fx = out$filtered_forces$force_x,
  fy = out$filtered_forces$force_z,
  cop_x = if (!is.null(out$cop)) out$cop$cop_x else rep(0, n),
  cop_y = if (!is.null(out$cop)) out$cop$cop_y else rep(0, n)
)

angles <- data.frame(
  ankle = 0.20 * sin(2 * pi * 1 * t),
  knee = 0.45 * sin(2 * pi * 1 * t + 0.2),
  hip = 0.35 * sin(2 * pi * 1 * t + 0.4)
)

4. Estimate inertial parameters and compute moments/power

inertial <- estimateSegmentInertia(
  body_mass = 70,
  segment_lengths = c(foot = 0.25, shank = 0.43, thigh = 0.45)
)

id_out <- inverseDynamics2D(
  joints = joints,
  grf = grf_2d,
  sampling_rate = sr,
  angles = angles,
  inertial = inertial
)

head(id_out)
#>    time ankle_moment knee_moment hip_moment ankle_power knee_power hip_power
#> 1 0.000           NA          NA         NA          NA         NA        NA
#> 2 0.001           NA          NA         NA          NA         NA        NA
#> 3 0.002           NA          NA         NA          NA         NA        NA
#> 4 0.003           NA          NA         NA          NA         NA        NA
#> 5 0.004           NA          NA         NA          NA         NA        NA
#> 6 0.005           NA          NA         NA          NA         NA        NA

5. Multi-plate selection and 3D inverse dynamics

force_z <- cbind(
  fp1 = out$filtered_forces$force_z * 0.6,
  fp2 = out$filtered_forces$force_z
)

pe_fp <- PhysioCore::PhysioExperiment(
  assays = S4Vectors::SimpleList(
    force_x = cbind(fp1 = out$filtered_forces$force_x * 0.6,
                    fp2 = out$filtered_forces$force_x),
    force_y = cbind(fp1 = out$filtered_forces$force_y * 0.6,
                    fp2 = out$filtered_forces$force_y),
    force_z = force_z
  ),
  colData = S4Vectors::DataFrame(
    label = c("fp1", "fp2"),
    type = c("forceplate", "forceplate")
  ),
  samplingRate = sr
)

fp_auto <- analyzeForcePlatePE(pe_fp, plate_index = "auto", threshold = 20,
                               cutoff = 20, filter_method = "moving_average")
fp_auto$selected_plate
#> [1] 2
joints_3d <- transform(
  joints,
  ankle_z = ankle_y, knee_z = knee_y, hip_z = hip_y,
  ankle_y = 0, knee_y = 0, hip_y = 0
)

grf_3d <- data.frame(
  fx = out$filtered_forces$force_x,
  fy = out$filtered_forces$force_y,
  fz = out$filtered_forces$force_z,
  cop_x = rep(0, n),
  cop_y = rep(0, n),
  cop_z = rep(0, n)
)

angles_3d <- data.frame(
  ankle_x = angles$ankle, ankle_y = 0 * angles$ankle, ankle_z = 0 * angles$ankle,
  knee_x = angles$knee,   knee_y = 0 * angles$knee,   knee_z = 0 * angles$knee,
  hip_x = angles$hip,     hip_y = 0 * angles$hip,     hip_z = 0 * angles$hip
)

id3 <- inverseDynamics3D(
  joints = joints_3d,
  grf = grf_3d,
  sampling_rate = sr,
  angles = angles_3d
)

head(id3)
#>    time ankle_moment_x ankle_moment_y ankle_moment_z knee_moment_x
#> 1 0.000             NA             NA             NA            NA
#> 2 0.001             NA             NA             NA            NA
#> 3 0.002             NA             NA             NA            NA
#> 4 0.003             NA             NA             NA            NA
#> 5 0.004             NA             NA             NA            NA
#> 6 0.005             NA             NA             NA            NA
#>   knee_moment_y knee_moment_z hip_moment_x hip_moment_y hip_moment_z
#> 1            NA            NA           NA           NA           NA
#> 2            NA            NA           NA           NA           NA
#> 3            NA            NA           NA           NA           NA
#> 4            NA            NA           NA           NA           NA
#> 5            NA            NA           NA           NA           NA
#> 6            NA            NA           NA           NA           NA
#>   ankle_power_total knee_power_total hip_power_total
#> 1                NA               NA              NA
#> 2                NA               NA              NA
#> 3                NA               NA              NA
#> 4                NA               NA              NA
#> 5                NA               NA              NA
#> 6                NA               NA              NA

Notes

  • For publication-grade kinetics, verify sign conventions and coordinate frames from your lab’s force-plate and marker setup.
  • Use subject-specific segment lengths and MVC/body-mass normalization where appropriate.