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:

  1. Related main header (for .cpp files)

  2. Standard C/C++ library headers (<cmath>, <vector>, <type_traits>, <concepts>)

  3. Third-party library headers (<Eigen/Dense>, <catch2/...>, <sophus/...>)

  4. 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

PascalCase

LevenbergMarquardt, Jet, CostFunction

Type Aliases / Templates

PascalCase

Vec3, Mat33, Scalar, Derived

Functions & Methods

PascalCase (public API) / camelCase (helpers)

Optimize(...), CheckResidualsGradient(...), coord(...)

Variables & Members

snake_case

max_iters, damping_lambda, step_norm

Private Member Variables

snake_case_ (trailing underscore)

options_, workspace_, current_cost_

Constants & Enum Values

PascalCase

StopReason::MaxItersReached, StopReason::GradientTolerance

Namespaces

snake_case (lowercase)

tinyopt, tinyopt::diff, tinyopt::solvers

Concepts

PascalCase

VectorSpace, ManifoldParameter


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 constexpr to 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

  1. KISS (Keep It Simple, Stupid): Avoid over-engineering. Optimization code is mathematically intricate; keep implementation paths clear, readable, and direct.

  2. Zero Dynamic Allocation in Critical Paths: Never allocate heap memory inside optimization loops. Use fixed-size types or pass reusable workspace buffers.

  3. 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.

  4. Zero Warnings Policy: Both GCC and Clang are configured with -Wall -Wextra -Werror. No compiler warnings are permitted.

  5. Defensive Numerical Programming:

    • Check for non-finite values (std::isnan, std::isinf) in gradients and Hessians.

    • Return descriptive StopReason enum 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

📝

docs

Documentation additions or updates

📝 docs: add coding style and architecture guides

✨

feat

New features, optimizers, solvers, or APIs

✨ feat(solvers): add dogleg trust-region step solver

🐛

fix

Bug fixes and numerical stabilization

🐛 fix(lm): prevent division by zero in damping ratio

⚡

perf

Performance optimizations and memory reductions

⚡ perf(accumulate): eliminate matrix temporaries with noalias

🧪

test

Adding or updating unit tests and benchmarks

🧪 test(diff): add Catch2 gradient validation for Huber loss

♻️

refactor

Code refactoring without changing behavior

♻️ refactor(traits): simplify Jet scalar detection

🎨

style

Formatting, whitespace, clang-format alignment

🎨 style: format headers according to .clang-format

🔧

chore

Build configuration, Pixi tasks, CMake flags

🔧 chore(pixi): add openmp dependency to bench environment

🔒

security

Sanitizer fixes, bounds checking, security

🔒 security(asan): fix buffer read beyond bounds in sparse solver

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

  1. Branch Naming:

    • feature/<name> for new algorithms or capabilities.

    • fix/<issue-name> for bug fixes.

    • refactor/<name> for structural cleanups.

  2. 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:

  1. Updates the major, minor, and patch values in cmake/Version.cmake and updates the workspace version in pixi.toml and Python package version in pyproject.toml to the requested release.

  2. Creates a version-bump commit containing those three version files.

  3. Builds the generic CPack TGZ, Debian package, and source TGZ under tmp/dist/, then verifies the source archive contains the expected project files and excludes tmp/.

  4. Creates the lightweight v0.7.0 tag on the version-bump commit, pushes it to origin, 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

  1. Read the relevant architecture, API, and test documentation before choosing an implementation.

  2. State the intended behavior, invariants, and the smallest check that could disprove the approach.

  3. For work under include/ (and src/ if introduced), capture a baseline for the current HEAD before 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 to tmp/timings/<HEAD-short-hash>.json for comparisons during the experiment.

  4. Keep each experiment focused. Record the hypothesis and avoid mixing unrelated behavior changes.

Implementation and Validation

  1. Preserve public behavior unless an API change is explicitly part of the goal. Document any changed contract and add focused regression coverage.

  2. Run relevant tests and correctness checks while iterating. Validate derivative changes against numerical differentiation and optimizer changes for convergence where applicable.

  3. Do not use benchmark results as a substitute for correctness tests.

  4. After all code edits and other checks are complete, run this as the final validation step for changes under include/ or src/:

    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 HEAD commit. Do not edit code after this final measurement without running it again.

  5. 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.

  6. After committing, associate the final dev measurements with the new commit:

    pixi run refactor-timings --finalize
    

    This renames tmp/timings/dev.json to the new tmp/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_optimize target after cleaning build-tests; that minimal test calls Optimize() on both Vec2 and VecXf. 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 HEAD short 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-timings to generate tmp/timings/evolution.png from 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 (HEAD)

Current (dev)

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