Skip to content

Commit 77f41fc

Browse files
committed
Add GitHub Actions workflows for building and validating GTSAM Docker images
- Introduced a new workflow for building and validating Docker images on multiple platforms (linux/amd64 and linux/arm64). - Updated the release workflow to include validation steps for the Docker images. - Added example scripts for validating GTSAM functionality within the container. - Created a shell script to facilitate running validation examples inside the Docker container.
1 parent c349e76 commit 77f41fc

5 files changed

Lines changed: 246 additions & 7 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Build and Validate
2+
3+
on:
4+
pull_request:
5+
branches: [main, development]
6+
7+
jobs:
8+
build-and-validate:
9+
strategy:
10+
fail-fast: false
11+
matrix:
12+
include:
13+
- platform: linux/amd64
14+
runner: ubuntu-latest
15+
- platform: linux/arm64
16+
runner: ubuntu-24.04-arm
17+
runs-on: ${{ matrix.runner }}
18+
permissions:
19+
contents: read
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v6
23+
24+
- name: Set up Docker Buildx
25+
uses: docker/setup-buildx-action@v3
26+
27+
- name: Build image (${{ matrix.platform }})
28+
run: docker build --platform ${{ matrix.platform }} -t gtsam_docker:latest .
29+
30+
- name: Validate image (PlanarSLAM example)
31+
run: |
32+
chmod +x ./scripts/validate_container.sh
33+
./scripts/validate_container.sh gtsam_docker:latest /examples/PlanarSLAMExample.py

.github/workflows/release.yaml

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ jobs:
2121
build:
2222
strategy:
2323
matrix:
24-
platform: [ "linux/amd64" ]
25-
# Use GitHub-hosted runner for amd64 and the arm64 partner runner for arm64
24+
platform: [ "linux/amd64", "linux/arm64" ]
25+
# Use GitHub-hosted runner for amd64; arm64 uses partner runner (ensure ubuntu-24.04-arm is enabled for the repo/org)
2626
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
2727
permissions:
2828
contents: read
@@ -62,8 +62,37 @@ jobs:
6262
tags: ${{ steps.meta.outputs.tags }}
6363
labels: ${{ steps.meta.outputs.labels }}
6464

65-
manifest:
65+
validate:
6666
needs: build
67+
strategy:
68+
fail-fast: false
69+
matrix:
70+
arch: [ amd64, arm64 ]
71+
runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
72+
permissions:
73+
contents: read
74+
packages: read
75+
steps:
76+
- name: Checkout
77+
uses: actions/checkout@v6
78+
79+
- name: Log into registry ${{ env.REGISTRY }}
80+
uses: docker/login-action@v3
81+
with:
82+
registry: ${{ env.REGISTRY }}
83+
username: ${{ github.actor }}
84+
password: ${{ secrets.GITHUB_TOKEN }}
85+
86+
- name: Pull and validate image (${{ matrix.arch }})
87+
run: |
88+
LOWER_IMAGE_NAME=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')
89+
docker pull ${{ env.REGISTRY }}/${LOWER_IMAGE_NAME}:${{ inputs.version }}-${{ matrix.arch }}
90+
docker tag ${{ env.REGISTRY }}/${LOWER_IMAGE_NAME}:${{ inputs.version }}-${{ matrix.arch }} gtsam_docker:latest
91+
chmod +x ./scripts/validate_container.sh
92+
./scripts/validate_container.sh gtsam_docker:latest /examples/PlanarSLAMExample.py
93+
94+
manifest:
95+
needs: validate
6796
runs-on: ubuntu-latest
6897
permissions:
6998
contents: read
@@ -78,18 +107,18 @@ jobs:
78107

79108
- name: Create multi-arch manifest for latest
80109
run: |
81-
# Convert IMAGE_NAME to lowercase
82110
LOWER_IMAGE_NAME=$(echo "${IMAGE_NAME}" | tr '[:upper:]' '[:lower:]')
83111
docker manifest create $REGISTRY/${LOWER_IMAGE_NAME}:latest \
84-
$REGISTRY/${LOWER_IMAGE_NAME}:latest-amd64
112+
$REGISTRY/${LOWER_IMAGE_NAME}:latest-amd64 \
113+
$REGISTRY/${LOWER_IMAGE_NAME}:latest-arm64
85114
docker manifest push $REGISTRY/${LOWER_IMAGE_NAME}:latest
86115
87116
- name: Create multi-arch manifest for version tag
88117
run: |
89-
# Convert IMAGE_NAME to lowercase
90118
LOWER_IMAGE_NAME=$(echo "${IMAGE_NAME}" | tr '[:upper:]' '[:lower:]')
91119
docker manifest create $REGISTRY/${LOWER_IMAGE_NAME}:${{ inputs.version }} \
92-
$REGISTRY/${LOWER_IMAGE_NAME}:${{ inputs.version }}-amd64
120+
$REGISTRY/${LOWER_IMAGE_NAME}:${{ inputs.version }}-amd64 \
121+
$REGISTRY/${LOWER_IMAGE_NAME}:${{ inputs.version }}-arm64
93122
docker manifest push $REGISTRY/${LOWER_IMAGE_NAME}:${{ inputs.version }}
94123
95124
release:

examples/PlanarSLAMExample.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""
2+
GTSAM Copyright 2010-2018, Georgia Tech Research Corporation,
3+
Atlanta, Georgia 30332-0415
4+
All Rights Reserved
5+
Authors: Frank Dellaert, et al. (see THANKS for the full author list)
6+
See LICENSE for the license information
7+
8+
Simple robotics example using odometry measurements and bearing-range (laser) measurements.
9+
From borglab/gtsam python/gtsam/examples/PlanarSLAMExample.py (GTSAM 4.2.0).
10+
11+
Run inside container: python3 /examples/PlanarSLAMExample.py
12+
"""
13+
# pylint: disable=invalid-name, E1101
14+
15+
from __future__ import print_function
16+
17+
import sys
18+
import gtsam
19+
import numpy as np
20+
from gtsam.symbol_shorthand import L, X
21+
22+
# Create noise models
23+
PRIOR_NOISE = gtsam.noiseModel.Diagonal.Sigmas(np.array([0.3, 0.3, 0.1]))
24+
ODOMETRY_NOISE = gtsam.noiseModel.Diagonal.Sigmas(np.array([0.2, 0.2, 0.1]))
25+
MEASUREMENT_NOISE = gtsam.noiseModel.Diagonal.Sigmas(np.array([0.1, 0.2]))
26+
27+
28+
def main():
29+
"""Main runner."""
30+
# Create an empty nonlinear factor graph
31+
graph = gtsam.NonlinearFactorGraph()
32+
33+
# Create the keys corresponding to unknown variables in the factor graph
34+
x1, x2, x3 = X(1), X(2), X(3)
35+
l1, l2 = L(4), L(5)
36+
37+
# Add a prior on pose X1 at the origin
38+
graph.add(
39+
gtsam.PriorFactorPose2(x1, gtsam.Pose2(0.0, 0.0, 0.0), PRIOR_NOISE)
40+
)
41+
42+
# Add odometry factors between X1,X2 and X2,X3
43+
graph.add(
44+
gtsam.BetweenFactorPose2(x1, x2, gtsam.Pose2(2.0, 0.0, 0.0), ODOMETRY_NOISE)
45+
)
46+
graph.add(
47+
gtsam.BetweenFactorPose2(x2, x3, gtsam.Pose2(2.0, 0.0, 0.0), ODOMETRY_NOISE)
48+
)
49+
50+
# Add Range-Bearing measurements to two different landmarks L1 and L2
51+
graph.add(
52+
gtsam.BearingRangeFactor2D(
53+
x1, l1, gtsam.Rot2.fromDegrees(45), np.sqrt(4.0 + 4.0), MEASUREMENT_NOISE
54+
)
55+
)
56+
graph.add(
57+
gtsam.BearingRangeFactor2D(x2, l1, gtsam.Rot2.fromDegrees(90), 2.0, MEASUREMENT_NOISE)
58+
)
59+
graph.add(
60+
gtsam.BearingRangeFactor2D(x3, l2, gtsam.Rot2.fromDegrees(90), 2.0, MEASUREMENT_NOISE)
61+
)
62+
63+
print("Factor Graph:\n{}".format(graph))
64+
65+
# Create (deliberately inaccurate) initial estimate
66+
initial_estimate = gtsam.Values()
67+
initial_estimate.insert(x1, gtsam.Pose2(-0.25, 0.20, 0.15))
68+
initial_estimate.insert(x2, gtsam.Pose2(2.30, 0.10, -0.20))
69+
initial_estimate.insert(x3, gtsam.Pose2(4.10, 0.10, 0.10))
70+
initial_estimate.insert(l1, gtsam.Point2(1.80, 2.10))
71+
initial_estimate.insert(l2, gtsam.Point2(4.10, 1.80))
72+
73+
print("Initial Estimate:\n{}".format(initial_estimate))
74+
75+
# Optimize using Levenberg-Marquardt
76+
params = gtsam.LevenbergMarquardtParams()
77+
optimizer = gtsam.LevenbergMarquardtOptimizer(graph, initial_estimate, params)
78+
result = optimizer.optimize()
79+
print("\nFinal Result:\n{}".format(result))
80+
81+
# Calculate and print marginal covariances
82+
marginals = gtsam.Marginals(graph, result)
83+
for (key, label) in [(x1, "X1"), (x2, "X2"), (x3, "X3"), (l1, "L1"), (l2, "L2")]:
84+
print("{} covariance:\n{}\n".format(label, marginals.marginalCovariance(key)))
85+
86+
# Validation: expect result size and non-NaN covariances
87+
assert result.size() == 5, "Expected 5 values in result"
88+
_ = marginals.marginalCovariance(x1) # will raise if invalid
89+
print("VALIDATION OK")
90+
return 0
91+
92+
if __name__ == "__main__":
93+
sys.exit(main())

examples/validate_gtsam.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
Minimal GTSAM sanity check for container validation.
3+
Uses symbol_shorthand, Pose2, and Values (same API as PlanarSLAM/Odometry examples).
4+
Avoids noiseModel.Diagonal.Sigmas(numpy_array), which can segfault when numpy
5+
ABI doesn't match the version GTSAM was built against.
6+
7+
Run: python3 /examples/validate_gtsam.py
8+
"""
9+
from __future__ import print_function
10+
11+
import sys
12+
import gtsam
13+
from gtsam.symbol_shorthand import X
14+
15+
def main():
16+
# Core types from the official examples, no numpy
17+
x1 = X(1)
18+
values = gtsam.Values()
19+
values.insert(x1, gtsam.Pose2(0.0, 0.0, 0.0))
20+
assert values.size() == 1
21+
pose = values.atPose2(x1)
22+
assert pose.x() == 0.0 and pose.y() == 0.0
23+
graph = gtsam.NonlinearFactorGraph()
24+
assert graph.size() == 0
25+
print("VALIDATION OK")
26+
return 0
27+
28+
if __name__ == "__main__":
29+
sys.exit(main())

scripts/validate_container.sh

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env bash
2+
# Run a GTSAM example inside the runtime container to validate the image.
3+
#
4+
# Usage:
5+
# ./scripts/validate_container.sh [IMAGE_TAG] [EXAMPLE]
6+
#
7+
# Examples:
8+
# ./scripts/validate_container.sh
9+
# ./scripts/validate_container.sh gtsam_docker:latest
10+
# ./scripts/validate_container.sh gtsam_docker:latest /examples/PlanarSLAMExample.py
11+
#
12+
# Default EXAMPLE is /examples/validate_gtsam.py (minimal graph/values check).
13+
# Use /examples/PlanarSLAMExample.py for the full PlanarSLAM example from borglab/gtsam.
14+
#
15+
# Prereq: build the runtime image first, e.g. docker build -t gtsam_docker:latest .
16+
17+
set -e
18+
19+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
21+
EXAMPLES_DIR="$REPO_ROOT/examples"
22+
IMAGE="${1:-gtsam_docker:latest}"
23+
EXAMPLE="${2:-/examples/validate_gtsam.py}"
24+
# If EXAMPLE has no leading slash, treat as name under /examples/
25+
if [[ -n "$EXAMPLE" && "$EXAMPLE" != /* ]]; then
26+
EXAMPLE="/examples/$EXAMPLE"
27+
fi
28+
29+
if [[ ! -d "$EXAMPLES_DIR" ]]; then
30+
echo "ERROR: Examples directory not found: $EXAMPLES_DIR" >&2
31+
exit 1
32+
fi
33+
34+
if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then
35+
echo "Image $IMAGE not found. Build it first, e.g.:" >&2
36+
echo " docker build -t gtsam_docker:latest ." >&2
37+
exit 1
38+
fi
39+
40+
echo "Running GTSAM example in container (image: $IMAGE, script: $EXAMPLE)..."
41+
echo "---"
42+
43+
docker run --rm \
44+
-v "$EXAMPLES_DIR:/examples:ro" \
45+
"$IMAGE" \
46+
python3 "$EXAMPLE"
47+
48+
EXIT_CODE=$?
49+
echo "---"
50+
if [[ $EXIT_CODE -eq 0 ]]; then
51+
echo "OK: Example finished with exit code 0."
52+
else
53+
echo "FAIL: Example exited with code $EXIT_CODE." >&2
54+
exit $EXIT_CODE
55+
fi

0 commit comments

Comments
 (0)