Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/workflows/configure_opensim_env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/bin/bash
#
# Point a conda environment at a locally built OpenSim, so that
# `import opensim` works without manually exporting PYTHONPATH/LD_LIBRARY_PATH
# every time.
#
# It writes conda "activate.d"/"deactivate.d" hooks into the CURRENTLY ACTIVE
# conda environment. Those hooks export PYTHONPATH/LD_LIBRARY_PATH
# automatically every time you `conda activate <env>`, and unset them again
# on `conda deactivate` -- no manual exporting needed after this.
#
# --- Usage ---
#
# 1. Build OpenSim first (only needs to be done once, or again after pulling
# changes to install_opensim.sh), e.g.:
#
# conda activate biobuddy
# bash .github/workflows/install_opensim.sh "$CONDA_PREFIX"
#
# This creates ./opensim/opensim_core_install relative to wherever you ran
# the build command from.
#
# 2. With that SAME conda env still active, run this script from the same
# directory, pointing it at the install directory if it isn't the default:
#
# bash .github/workflows/configure_opensim_env.sh [path/to/opensim_core_install]
#
# (defaults to ./opensim/opensim_core_install if no argument is given)
#
# 3. Re-activate the env so the hook actually runs, then test:
#
# conda deactivate
# conda activate biobuddy
# python -c "import opensim; print(opensim.GetVersion())"
#
# Re-run this script any time OpenSim gets rebuilt or moved to a new path.

set -e

OPENSIM_INSTALL="${1:-$(pwd)/opensim/opensim_core_install}"

if [ -z "$CONDA_PREFIX" ]; then
echo "Error: no conda environment is active. Run 'conda activate <env>' first." >&2
exit 1
fi

if [ ! -d "$OPENSIM_INSTALL" ]; then
echo "Error: OpenSim install directory not found: $OPENSIM_INSTALL" >&2
echo "Pass the path explicitly, e.g.: bash $0 /path/to/opensim_core_install" >&2
exit 1
fi

# Find the "opensim" python package directory (the folder containing
# __init__.py) so we can put its PARENT on PYTHONPATH.
OPENSIM_PY_INIT=$(find "$OPENSIM_INSTALL" -path "*/opensim/__init__.py" | head -n1)
if [ -z "$OPENSIM_PY_INIT" ]; then
echo "Error: could not find an 'opensim/__init__.py' under $OPENSIM_INSTALL" >&2
echo "Did the build finish and install successfully (with BUILD_PYTHON_WRAPPING=on)?" >&2
exit 1
fi
OPENSIM_PY_PKG_PARENT=$(dirname "$(dirname "$OPENSIM_PY_INIT")")

# Collect every directory containing a compiled shared library (OpenSim's own
# libs plus the bundled Simbody ones), so the compiled extension modules can
# resolve them at runtime.
LIB_DIRS=$(find "$OPENSIM_INSTALL" -name "*.so*" -exec dirname {} \; | sort -u | paste -sd:)

ACTIVATE_DIR="$CONDA_PREFIX/etc/conda/activate.d"
DEACTIVATE_DIR="$CONDA_PREFIX/etc/conda/deactivate.d"
mkdir -p "$ACTIVATE_DIR" "$DEACTIVATE_DIR"

cat > "$ACTIVATE_DIR/opensim.sh" <<EOF
# Auto-generated by configure_opensim_env.sh -- do not edit by hand,
# re-run the script instead if OpenSim gets rebuilt or moved.
export _OLD_PYTHONPATH="\$PYTHONPATH"
export _OLD_LD_LIBRARY_PATH="\$LD_LIBRARY_PATH"
export PYTHONPATH="$OPENSIM_PY_PKG_PARENT:\$PYTHONPATH"
export LD_LIBRARY_PATH="$LIB_DIRS:\$CONDA_PREFIX/lib:\$LD_LIBRARY_PATH"
EOF

cat > "$DEACTIVATE_DIR/opensim.sh" <<'EOF'
# Auto-generated by configure_opensim_env.sh
export PYTHONPATH="$_OLD_PYTHONPATH"
export LD_LIBRARY_PATH="$_OLD_LD_LIBRARY_PATH"
unset _OLD_PYTHONPATH _OLD_LD_LIBRARY_PATH
EOF

echo "Done."
echo " OpenSim python package dir: $OPENSIM_PY_PKG_PARENT"
echo " Library dirs: $LIB_DIRS"
echo ""
echo "Now run this to make it take effect:"
echo " conda deactivate && conda activate $(basename "$CONDA_PREFIX")"
echo ' python -c "import opensim; print(opensim.GetVersion())"'
62 changes: 0 additions & 62 deletions .github/workflows/copilot-setup-steps.yml

This file was deleted.

77 changes: 77 additions & 0 deletions .github/workflows/install_opensim.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/bin/bash

# Exit when an error happens instead of continue.
set -e

# Default values for flags.
DEBUG_TYPE="Release"
NUM_JOBS=${OPENSIM_BUILD_JOBS:-24}
MOCO="off"
ORG="nickbianco"
BRANCH="dc7e1f0a18905fcd17cbbee9f923a70b4cb9de99"
GENERATOR="Ninja"
PYTHON_ROOT_DIR=$1
WORKING_DIR="$(pwd)/opensim"
if [ -d "$WORKING_DIR" ]; then
rm -rf "$WORKING_DIR"
fi
mkdir -p "$WORKING_DIR"

# Get opensim-core. Fetch the pinned commit directly instead of cloning the
# whole repo and checking it out afterwards: if that commit is later dropped
# from whatever branch it was on (rebase/branch deletion upstream), a normal
# clone stops including it and `git checkout $BRANCH` fails with "unable to
# read tree", even though the commit object itself still exists and remains
# directly fetchable by SHA.
mkdir -p "$WORKING_DIR/opensim-core"
cd "$WORKING_DIR/opensim-core"
git init -q
git remote add origin https://github.com/$ORG/opensim-core.git
git fetch --depth 1 origin $BRANCH
git checkout -q FETCH_HEAD

# Build opensim-core dependencies.
mkdir -p "$WORKING_DIR/opensim-core/dependencies/build"
cd "$WORKING_DIR/opensim-core/dependencies/build"
cmake "$WORKING_DIR/opensim-core/dependencies" -G"$GENERATOR" -DCMAKE_BUILD_TYPE=$DEBUG_TYPE -DCMAKE_INSTALL_PREFIX="$WORKING_DIR/opensim_dependencies_install/" -DSUPERBUILD_ezc3d=off -DOPENSIM_WITH_CASADI=$MOCO -DBUILD_PYTHON_WRAPPING=on -DPython3_ROOT_DIR="$PYTHON_ROOT_DIR"
cmake . -LAH

# opensim-core's own dependencies/CMakeLists.txt pins Simbody to a raw commit
# on nickbianco's fork. That commit can become orphaned (unreachable from any
# branch) if the fork gets rebased/cleaned up upstream, which makes
# ExternalProject_Add's normal git clone fail with "reference is not a tree" /
# "unable to read tree", even though the commit object itself still exists
# and remains directly fetchable by SHA (same class of issue as the
# opensim-core checkout above). Work around it the same way: fetch Simbody by
# SHA ourselves into the exact source dir ExternalProject expects, then mark
# its git-clone step as already done via the stamp file it checks, so the
# build skips straight to configuring/building the pre-fetched source.
SIMBODY_GIT_URL="https://github.com/nickbianco/simbody.git"
SIMBODY_GIT_TAG="6cd18d8b8466b3e574377a6b5acd942af78c7a88"
SIMBODY_SOURCE_DIR="$WORKING_DIR/opensim-core/dependencies/simbody"
SIMBODY_STAMP_DIR="$WORKING_DIR/opensim-core/dependencies/build/simbody/stamp"

rm -rf "$SIMBODY_SOURCE_DIR"
mkdir -p "$SIMBODY_SOURCE_DIR"
(
cd "$SIMBODY_SOURCE_DIR"
git init -q
git remote add origin "$SIMBODY_GIT_URL"
git fetch --depth 1 origin "$SIMBODY_GIT_TAG"
git checkout -q FETCH_HEAD
if [ -f .gitmodules ]; then
git submodule update --init --recursive
fi
)
cp "$SIMBODY_STAMP_DIR/simbody-gitinfo.txt" "$SIMBODY_STAMP_DIR/simbody-gitclone-lastrun.txt"
touch "$SIMBODY_STAMP_DIR/simbody-gitclone-lastrun.txt"

cmake --build . --config $DEBUG_TYPE -j$NUM_JOBS


# Build and install opensim-core.
mkdir -p "$WORKING_DIR/opensim-core/build"
cd "$WORKING_DIR/opensim-core/build"
cmake "$WORKING_DIR/opensim-core" -G"$GENERATOR" -DCMAKE_BUILD_TYPE=$DEBUG_TYPE -DOPENSIM_DEPENDENCIES_DIR="$WORKING_DIR/opensim_dependencies_install/" -DOPENSIM_C3D_PARSER=None -DBUILD_TESTING=off -DCMAKE_INSTALL_PREFIX="$WORKING_DIR/opensim_core_install" -DOPENSIM_INSTALL_UNIX_FHS=off -DOPENSIM_WITH_CASADI=$MOCO -DBUILD_PYTHON_WRAPPING=on -DPython3_ROOT_DIR="$PYTHON_ROOT_DIR"
cmake --build . --config $DEBUG_TYPE -j$NUM_JOBS
cmake --install .
46 changes: 42 additions & 4 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,63 @@ jobs:
submodules: recursive
fetch-depth: 0

- name: Free disk space
run: |
# Building OpenSim from source (Simbody + the full OpenSim/Moco
# tree) plus conda/biorbd needs more disk than the ~14GB free by
# default on GitHub runners. Remove large preinstalled toolchains
# we don't use to make room, otherwise the build (or even the
# earlier `git clone` of opensim-core) can silently run out of
# space and fail with an unrelated-looking error.
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force || true
df -h /

- name: Setup conda
uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest

- name: Set up Python
run: |
conda install -cconda-forge python=3.11.11 pip
conda install -cconda-forge python=3.11.11 pip

- name: Install system dependencies for building OpenSim
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential cmake ninja-build \
libblas-dev liblapack-dev \
freeglut3-dev libxi-dev libxmu-dev \
pkg-config
pip install swig

- name: Install dependencies
run: |
pip install . && pip uninstall -y biobuddy
pip install scipy==1.17.0 numpy==2.4.1 lxml ezc3d matplotlib plotly
conda install biorbd=1.11.2=py311h9439bbc_1 graphviz -cconda-forge
conda install biorbd graphviz -cconda-forge

- name: Install test dependencies
run: |
pip install pytest pytest-cov codecov
conda install -c conda-forge deepdiff pinocchio pandas
conda install -c opensim-org opensim=4.5.1

- name: Build and install OpenSim from source
run: |
# Cap parallel compile jobs to the runner's core count: the script
# defaults to 24 jobs, which OOM-kills the compiler on standard
# (4-core) GitHub runners partway through the build.
export OPENSIM_BUILD_JOBS=$(nproc)
bash .github/workflows/install_opensim.sh "$CONDA_PREFIX"

OPENSIM_INSTALL="$(pwd)/opensim/opensim_core_install"
OPENSIM_PY_INIT=$(find "$OPENSIM_INSTALL" -path "*/opensim/__init__.py" | head -n1)
OPENSIM_PY_PKG_PARENT=$(dirname "$(dirname "$OPENSIM_PY_INIT")")
LIB_DIRS=$(find "$OPENSIM_INSTALL" -name "*.so*" -exec dirname {} \; | sort -u | paste -sd:)

echo "PYTHONPATH=$OPENSIM_PY_PKG_PARENT:$PYTHONPATH" >> "$GITHUB_ENV"
echo "LD_LIBRARY_PATH=$LIB_DIRS:$LD_LIBRARY_PATH" >> "$GITHUB_ENV"

- name: List conda packages
run: conda list
Expand Down Expand Up @@ -102,4 +139,5 @@ jobs:
pip install .
cd
python -c "import biobuddy"
cd $BASE_FOLDER
cd $BASE_FOLDER

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,5 @@ examples/data/hide_and_seek/
examples/models/hide_and_seek/

examples/data/arm26_allbiceps_1dof.png

opensim/
43 changes: 23 additions & 20 deletions biobuddy/components/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,24 +114,27 @@ def _calculate_coefficients(self):

# Handle the case with only 2 points (linear interpolation)
if self.nb_nodes == 2:
t = self.safe_max(self.x_points[1] - self.x_points[0])
self.b[0] = self.b[1] = (self.y_points[1] - self.y_points[0]) / t
self.c[0] = self.c[1] = 0.0
self.d[0] = self.d[1] = 0.0
t = (self.safe_max(self.x_points[1] - self.x_points[0])).reshape(-1)[0]
self.b[0] = ((self.y_points[1] - self.y_points[0]) / t).reshape(-1)[0]
self.b[1] = self.b[0]
self.c[0] = 0.0
self.c[1] = 0.0
self.d[0] = 0.0
self.d[1] = 0.0
return

nm1 = self.nb_nodes - 1
nm2 = self.nb_nodes - 2

# Set up tridiagonal system:
# b = diagonal, d = offdiagonal, c = right-hand side
self.d[0] = self.safe_max(self.x_points[1] - self.x_points[0])
self.c[1] = (self.y_points[1] - self.y_points[0]) / self.d[0]
self.d[0] = (self.safe_max(self.x_points[1] - self.x_points[0])).reshape(-1)[0]
self.c[1] = ((self.y_points[1] - self.y_points[0]) / self.d[0]).reshape(-1)[0]

for i in range(1, nm1):
self.d[i] = self.safe_max(self.x_points[i + 1] - self.x_points[i])
self.d[i] = (self.safe_max(self.x_points[i + 1] - self.x_points[i])).reshape(-1)[0]
self.b[i] = 2.0 * (self.d[i - 1] + self.d[i])
self.c[i + 1] = (self.y_points[i + 1] - self.y_points[i]) / self.d[i]
self.c[i + 1] = ((self.y_points[i + 1] - self.y_points[i]) / self.d[i]).reshape(-1)[0]
self.c[i] = self.c[i + 1] - self.c[i]

# End conditions. Third derivatives at x[0] and x[self.nb_nodes-1]
Expand All @@ -142,12 +145,12 @@ def _calculate_coefficients(self):
self.c[nm1] = 0.0

if self.nb_nodes > 3:
d31 = self.safe_max(self.x_points[3] - self.x_points[1])
d20 = self.safe_max(self.x_points[2] - self.x_points[0])
d1 = self.safe_max(self.x_points[nm1] - self.x_points[self.nb_nodes - 3])
d2 = self.safe_max(self.x_points[nm2] - self.x_points[self.nb_nodes - 4])
d30 = self.safe_max(self.x_points[3] - self.x_points[0])
d3 = self.safe_max(self.x_points[nm1] - self.x_points[self.nb_nodes - 4])
d31 = (self.safe_max(self.x_points[3] - self.x_points[1])).reshape(-1)[0]
d20 = (self.safe_max(self.x_points[2] - self.x_points[0])).reshape(-1)[0]
d1 = (self.safe_max(self.x_points[nm1] - self.x_points[self.nb_nodes - 3])).reshape(-1)[0]
d2 = (self.safe_max(self.x_points[nm2] - self.x_points[self.nb_nodes - 4])).reshape(-1)[0]
d30 = (self.safe_max(self.x_points[3] - self.x_points[0])).reshape(-1)[0]
d3 = (self.safe_max(self.x_points[nm1] - self.x_points[self.nb_nodes - 4])).reshape(-1)[0]

self.c[0] = self.c[2] / d31 - self.c[1] / d20
self.c[nm1] = self.c[nm2] / d1 - self.c[self.nb_nodes - 3] / d2
Expand All @@ -167,14 +170,14 @@ def _calculate_coefficients(self):
self.c[i] = (self.c[i] - self.d[i] * self.c[i + 1]) / self.b[i]

# Compute polynomial coefficients
self.b[nm1] = (self.y_points[nm1] - self.y_points[nm2]) / self.d[nm2] + self.d[nm2] * (
self.c[nm2] + 2.0 * self.c[nm1]
)
self.b[nm1] = (
(self.y_points[nm1] - self.y_points[nm2]) / self.d[nm2] + self.d[nm2] * (self.c[nm2] + 2.0 * self.c[nm1])
).reshape(-1)[0]

for i in range(nm1):
self.b[i] = (self.y_points[i + 1] - self.y_points[i]) / self.d[i] - self.d[i] * (
self.c[i + 1] + 2.0 * self.c[i]
)
self.b[i] = (
(self.y_points[i + 1] - self.y_points[i]) / self.d[i] - self.d[i] * (self.c[i + 1] + 2.0 * self.c[i])
).reshape(-1)[0]
self.d[i] = (self.c[i + 1] - self.c[i]) / self.d[i]
self.c[i] *= 3.0

Expand Down
Loading
Loading