Development
Tinyopt Coding Style Guide
Tinyopt is an ultra-high-performance, header-only C++20 library. Maintaining code uniformity, readability, and hardware efficiency is critical across the codebase.
All contributions must strictly adhere to the standards outlined in this guide.
1. Automated Formatting (.clang-format)
Tinyopt enforces formatting via Clang-Format using a Google-based standard with the following key rules:
Indentation: 2 spaces (no tabs).
Line Limit: 100 characters.
Braces: Attached style (
Attach), no line break before opening braces for classes, functions, or control statements.Access Modifiers: Indented by -1 relative to class (
public:,private:).Pointer & Reference Alignment: Left/Derive (
const Vec3 &x,double *grad).
Running Clang-Format
Before submitting or staging changes, run the repository formatter:
./scripts/format.sh
Or check for violations without modifying files:
./scripts/format.sh --check
2. File & License Headers
Every .h, .hpp, and .cpp file must begin with the official Apache-2.0 copyright header:
// Copyright 2026 Julien Michot.
// SPDX-License-Identifier: Apache-2.0
Headers must use #pragma once as the include guard.
3. Include Ordering
Includes must be grouped with blank lines separating each block:
Related main header (for
.cppfiles)Standard C/C++ library headers (
<cmath>,<vector>,<type_traits>,<concepts>)Third-party library headers (
<Eigen/Dense>,<catch2/...>,<sophus/...>)Tinyopt library headers (
<tinyopt/types.h>,<tinyopt/diff/jet.h>)
Example:
// Copyright 2026 Julien Michot.
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <cmath>
#include <concepts>
#include <type_traits>
#include <Eigen/Core>
#include <tinyopt/traits.h>
#include <tinyopt/types.h>
4. Naming Conventions
Entity |
Convention |
Example |
|---|---|---|
Types, Classes, Structs |
|
|
Type Aliases / Templates |
|
|
Functions & Methods |
|
|
Variables & Members |
|
|
Private Member Variables |
|
|
Constants & Enum Values |
|
|
Namespaces |
|
|
Concepts |
|
|
5. Commit Examples for Features and API Changes
Every feature, API addition, or user-visible behavior change must include a short usage example in the commit body when the change is code-related. This is not optional: the example should show the intended public usage and make the benefit immediately clear to a reviewer.
Required pattern:
<emoji> <type>(<optional-scope>): <subject>
This adds support for <feature>. Example usage:
const auto residual = x - target;
const auto &[res, J] = Residuals(residual, Huber(residual, 10.0));
Use a minimal snippet that demonstrates the new behavior without adding unrelated boilerplate. For solver or loss changes, the example should show the relevant call site and the resulting public API contract.
6. Modern C++20 Idioms & Best Practices
Const-Correctness
Mark all non-mutating member functions
const.Mark non-mutating variables and parameters
const.Pass cheap-to-copy types (scalar values, primitive types $\le 16$ bytes) by value. Pass larger structures, matrices, and parameters by
const &.
Compile-Time Optimization (constexpr & if constexpr)
Mark pure utility functions and traits
constexpr.Use
if constexprto eliminate dead code paths at compile time:if constexpr (!traits::is_nullptr_v<decltype(grad)>) { grad.noalias() += J.transpose() * res; }
[[nodiscard]] Attribute
Use [[nodiscard]] on all non-mutating functions that compute or return a value (getters, math functions, status checkers, factory methods):
[[nodiscard]] inline bool Converged() const noexcept {
return stop_reason == StopReason::GradientTolerance ||
stop_reason == StopReason::StepTolerance;
}
No Dynamic Allocations in Tight Loops
Never call
malloc,new,std::vector::resize, or create dynamic Eigen matrices (MatrixXd,VectorXd) inside optimization iterations or residual callbacks.Use fixed-size Eigen types whenever dimensions are known at compile-time (
Vec2,Vec3,Mat33,Matrix<T, Rows, Cols>).
Eigen .noalias() Discipline
When assigning matrix-matrix or matrix-vector products, always use .noalias() if the destination does not appear on the right-hand side:
// Correct: Zero temporaries
H.noalias() += J.transpose() * J;
g.noalias() += J.transpose() * r;
// Avoid: Allocates hidden heap/stack temporary
H += J.transpose() * J;
Tinyopt Software Development Guidelines
This document outlines the core engineering practices, version control standards, commit message conventions, and code review rules for developing in the Tinyopt repository.
1. Core Software Development Principles
KISS (Keep It Simple, Stupid): Avoid over-engineering. Optimization code is mathematically intricate; keep implementation paths clear, readable, and direct.
Zero Dynamic Allocation in Critical Paths: Never allocate heap memory inside optimization loops. Use fixed-size types or pass reusable workspace buffers.
Single Responsibility Principle (SRP): Keep components focused:
A loss function computes a scalar metric and derivatives.
A solver computes linear step $\delta x$.
An optimizer governs the trust region, step acceptance, and outer iteration loop.
Zero Warnings Policy: Both GCC and Clang are configured with
-Wall -Wextra -Werror. No compiler warnings are permitted.Defensive Numerical Programming:
Check for non-finite values (
std::isnan,std::isinf) in gradients and Hessians.Return descriptive
StopReasonenum values when numerical failure is encountered rather than crashing.
2. Agent Commit Policy
[!IMPORTANT] No Automatic Commits: AI agents and assistants must NEVER automatically commit changes unless explicitly requested by the user within the active session (e.g. “ok commit now”, “create a commit”). Working changes should remain staged or unstaged until explicitly instructed.
For versioned package builds and release commits, follow Packaging and Releasing.
3. Git Commit Message Conventions (With Emojis)
All commit titles must follow the Conventional Commits with Emojis specification:
<emoji> <type>(<optional-scope>): <subject>
Supported Commit Types & Emojis
Emoji |
Type |
Purpose |
Example |
|---|---|---|---|
📝 |
|
Documentation additions or updates |
|
✨ |
|
New features, optimizers, solvers, or APIs |
|
🐛 |
|
Bug fixes and numerical stabilization |
|
⚡ |
|
Performance optimizations and memory reductions |
|
🧪 |
|
Adding or updating unit tests and benchmarks |
|
♻️ |
|
Code refactoring without changing behavior |
|
🎨 |
|
Formatting, whitespace, clang-format alignment |
|
🔧 |
|
Build configuration, Pixi tasks, CMake flags |
|
🔒 |
|
Sanitizer fixes, bounds checking, security |
|
Rules for Commit Messages:
Use imperative, present tense (“add”, not “added” or “adds”).
Do not capitalize the first letter after the type prefix.
Do not place a period at the end of the subject line.
Keep the title under 72 characters.
4. Branching & Pull Request Workflow
Branch Naming:
feature/<name>for new algorithms or capabilities.fix/<issue-name>for bug fixes.refactor/<name>for structural cleanups.
Pre-Submission Checklist: Before asking to commit or opening a PR:
[ ] Run formatter:
./scripts/format.sh --check[ ] Run test suite:
./scripts/run_tests.sh all[ ] Verify gradients: Any new analytical residual or loss has verified derivatives using
diff::CheckResidualsGradient.[ ] Zero warnings under
-Werror.
Packaging and Releasing
Build Packages
Run the package task from the repository root:
pixi run build-pkg
This builds the C shared library, Debian package, and source archive in tmp/dist/. Binary packages
include libtinyopt_c, its generated and precision-specific C headers, the Doxygen API reference
under share/doc/tinyopt, and the Sphinx guide under share/doc/tinyopt/guide; the source archive
retains the C API sources and documentation. The Debian package target requires a Linux CPack
installation with DEB generator support.
For CMake consumer examples that select the header-only C++ API or the installed C library, see
Installation and Usage.
Create a Release
Run releases from Linux with Git configured and a clean worktree. First commit the release tooling
and any other intended changes. The release task refuses to run when tracked or untracked changes
are present, and it refuses to reuse an existing vX.Y.Z tag.
pixi run release 0.7.0
The task performs these steps in order:
Updates the major, minor, and patch values in
cmake/Version.cmakeand updates the workspace version inpixi.tomland Python package version inpyproject.tomlto the requested release.Creates a version-bump commit containing those three version files.
Builds the generic CPack TGZ, Debian package, and source TGZ under
tmp/dist/, then verifies the source archive contains the expected project files and excludestmp/.Creates the lightweight
v0.7.0tag on the version-bump commit, pushes it toorigin, and creates or updates the GitHub release with all three package files.
Temporary CTest output, package artifacts, and timing reports are kept under the ignored tmp/
directory and are excluded from source packages.
The release task does not update the remote branch ref. It requires the GitHub CLI to be available
and authenticated. If package generation, tag pushing, or asset upload fails after the version
commit, resolve the issue and rerun the same command; it resumes when HEAD is the matching
version-bump commit. If a local tag already exists, it must point to that commit. Existing GitHub
releases are updated by replacing matching assets, allowing an upload retry. If a release fails
before that commit, restore a clean worktree before trying again.
Refactoring and Experiment Guidelines
This document is the required workflow for refactorings and code experiments in Tinyopt. It applies across the repository and preserves behavioral correctness and measurable performance evidence.
Before Starting
Read the relevant architecture, API, and test documentation before choosing an implementation.
State the intended behavior, invariants, and the smallest check that could disprove the approach.
For work under
include/(andsrc/if introduced), capture a baseline for the currentHEADbefore editing:pixi run refactor-timings pixi run refactor-timings --finalize
The first command stores current measurements as
tmp/timings/dev.json; the second pins them totmp/timings/<HEAD-short-hash>.jsonfor comparisons during the experiment.Keep each experiment focused. Record the hypothesis and avoid mixing unrelated behavior changes.
Implementation and Validation
Preserve public behavior unless an API change is explicitly part of the goal. Document any changed contract and add focused regression coverage.
Run relevant tests and correctness checks while iterating. Validate derivative changes against numerical differentiation and optimizer changes for convergence where applicable.
Do not use benchmark results as a substitute for correctness tests.
After all code edits and other checks are complete, run this as the final validation step for changes under
include/orsrc/:pixi run refactor-timings
This clean-compiles the test targets, builds the non-Ceres benchmark executable, runs the benchmarks three times, and compares both measurements against the timing file for the current
HEADcommit. Do not edit code after this final measurement without running it again.Review both comparison rows. Investigate material regressions before accepting the change; a missing baseline is not evidence of parity. The script labels changes within 5% as neutral to avoid overstating timing noise.
After committing, associate the final
devmeasurements with the new commit:pixi run refactor-timings --finalize
This renames
tmp/timings/dev.jsonto the newtmp/timings/<HEAD-short-hash>.json. Timing files are intentionally local and ignored by Git.
Measurement Details
Optimize compilation time is the wall time for building the dedicated
tinyopt_bench_simple_optimizetarget after cleaningbuild-tests; that minimal test callsOptimize()on bothVec2andVecXf. Other test targets are excluded, and CMake configuration is not timed.The benchmark metric is the median, across three XML reports, of the sum of Catch2’s per-case mean durations from the non-Ceres benchmark suite. Catch2 reports these means in nanoseconds; reports are retained in the ignored
tmp/timings/directory. The executable is rebuilt before measurement.Comparisons always use the timing file named for the current
HEADshort hash. If that file is missing, capture and finalize a baseline before proceeding with the experiment.Record the displayed before/after values and status in the change summary. Report failures or missing results plainly; do not claim a speedup or parity without measurements.
Use
pixi run plot-refactor-timingsto generatetmp/timings/evolution.pngfrom finalized result files. Plot points use each commit’s recorded timestamp and short hash.
Results Table
The timing task prints a table with both required metrics:
Metric |
Before ( |
Current ( |
Delta |
Status |
|---|---|---|---|---|
Minimal Optimize() test compilation |
seconds |
seconds |
seconds and percent |
faster / slower / neutral |
Non-Ceres benchmark mean sum |
seconds |
seconds |
seconds and percent |
faster / slower / neutral |