Files
Denso/srcs/RobotNet10/RobotApp/Communication/CeresSharp/README.md
2026-07-03 16:31:37 +07:00

29 KiB
Raw Blame History

CeresSharp - AI Agent Conversion Guide

📋 Overview

CeresSharp is a C# wrapper library for Ceres Solver 2.2.0, providing a high-level API that closely mirrors the C++ API. This guide is designed for AI Agents to help convert C++ Ceres code to C# using CeresSharp.

Target Framework: .NET 10.0
Platform: Linux only (libceres_wrapper.so)
Status: 100% Complete - All 220+ APIs implemented (including AutoDiffManifold)


🎯 Quick Reference: C++ → C# Mapping

Namespace Mapping

C++ C#
ceres::Problem CeresSharp.Problem
ceres::Solver::Options CeresSharp.SolverOptions
ceres::Solver::Summary CeresSharp.SolverSummary
ceres::CostFunction CeresSharp.CostFunction (base)
ceres::AutoDiffCostFunction CeresSharp.AutoDiffCostFunction
ceres::LossFunction CeresSharp.LossFunction (base)
ceres::HuberLoss CeresSharp.HuberLoss
ceres::QuaternionManifold CeresSharp.QuaternionManifold

Using Statements

using CeresSharp;
using CeresSharp.Enums;
using CeresSharp.Advanced; // For advanced features

🔄 Conversion Patterns

1. Problem Creation

C++:

ceres::Problem problem;

C#:

using var problem = new Problem();
// or
var problem = new Problem();
// Remember to dispose: problem.Dispose();

Key Differences:

  • C# uses using statement for automatic disposal
  • All objects implement IDisposable for proper resource cleanup
  • IMPORTANT: Objects added to Problem (cost functions, loss functions, manifolds) are owned by Problem and should NOT be manually disposed

2. Adding Parameter Blocks

C++:

double parameters[2] = {1.0, 2.0};
problem.AddParameterBlock(parameters, 2);

C#:

var parameters = new double[] { 1.0, 2.0 };
problem.AddParameterBlock(parameters, parameters.Length);

Key Differences:

  • C# uses managed arrays (double[])
  • Arrays are automatically pinned when passed to native code
  • No need for manual memory management

3. Cost Functions

AutoDiffCostFunction

C++:

struct CostFunctor {
    template <typename T>
    bool operator()(const T* const x, T* residual) const {
        residual[0] = x[0] - 1.0;
        return true;
    }
};

auto* cost_function = new ceres::AutoDiffCostFunction<CostFunctor, 1, 1>(
    new CostFunctor);

C#:

var costFunction = new AutoDiffCostFunction(
    (parameters, residuals) =>
    {
        residuals[0] = parameters[0][0] - 1.0;
        return true;
    },
    numResiduals: 1,
    parameterBlockSizes: new[] { 1 });

Key Differences:

  • C# uses lambda expressions instead of functors
  • Parameters are passed as double[][] (array of parameter blocks)
  • Residuals are passed as double[] (output array)
  • Return true on success, false on failure

Multiple Parameter Blocks

C++:

struct CostFunctor {
    template <typename T>
    bool operator()(const T* const x, const T* const y, T* residual) const {
        residual[0] = x[0] * y[0] - 1.0;
        return true;
    }
};

auto* cost_function = new ceres::AutoDiffCostFunction<CostFunctor, 1, 2, 3>(
    new CostFunctor);

C#:

var costFunction = new AutoDiffCostFunction(
    (parameters, residuals) =>
    {
        // parameters[0] is first block (size 2)
        // parameters[1] is second block (size 3)
        residuals[0] = parameters[0][0] * parameters[1][0] - 1.0;
        return true;
    },
    numResiduals: 1,
    parameterBlockSizes: new[] { 2, 3 });

4. Loss Functions

C++:

ceres::HuberLoss loss(1.0);
problem.AddResidualBlock(cost_function, &loss, parameters);

C#:

using var loss = new HuberLoss(1.0);
var residualBlockId = problem.AddResidualBlock(
    costFunction,
    loss,
    parameterBlocks: new[] { parameters });

// ⚠️ IMPORTANT: Problem now owns both costFunction and loss
// Don't manually dispose them - Problem will cleanup automatically

Available Loss Functions:

  • TrivialLoss() - No loss (equivalent to null in C#)
  • HuberLoss(double a)
  • CauchyLoss(double a)
  • SoftLOneLoss(double a)
  • ArctanLoss(double a)
  • TolerantLoss(double a, double b)

5. Manifolds

C++:

ceres::QuaternionManifold* manifold = new ceres::QuaternionManifold();
problem.SetManifold(parameters, manifold);

C#:

using var manifold = new QuaternionManifold();
problem.SetManifold(parameters, manifold);

// ⚠️ IMPORTANT: Problem now owns the manifold
// Don't manually dispose it - Problem will cleanup automatically

<tool▁calls▁begin><tool▁call▁begin> read_file

Available Manifolds:

  • QuaternionManifold() - For 3D rotations
  • SphereManifold(int dimension)
  • LineManifold(int dimension)
  • EuclideanManifold(int dimension)
  • SubsetManifold(int[] constantSubset, int ambientSize)
  • ProductManifold(Manifold[] manifolds)
  • AutoDiffManifold(int ambientSize, int tangentSize, PlusOperation plus, MinusOperation minus) NEW - Custom manifolds via callbacks (replacement for AutoDiffLocalParameterization)

6. Solver Options

C++:

ceres::Solver::Options options;
options.linear_solver_type = ceres::DENSE_QR;
options.max_num_iterations = 100;
options.function_tolerance = 1e-6;

C#:

using var options = new SolverOptions
{
    LinearSolverType = LinearSolverType.DenseQr,
    MaxNumIterations = 100,
    FunctionTolerance = 1e-6
};

Enum Mapping:

C++ C#
ceres::DENSE_QR LinearSolverType.DenseQr
ceres::SPARSE_NORMAL_CHOLESKY LinearSolverType.SparseNormalCholesky
ceres::TRUST_REGION MinimizerType.TrustRegion
ceres::LINE_SEARCH MinimizerType.LineSearch

Using Constants:

using CeresSharp;

var options = new SolverOptions
{
    MaxNumIterations = Constants.DefaultMaxNumIterations,
    FunctionTolerance = Constants.DefaultFunctionTolerance,
    GradientTolerance = Constants.DefaultGradientTolerance
};

7. Solving

C++:

ceres::Solver::Summary summary;
ceres::Solve(options, &problem, &summary);

C#:

using var summary = problem.Solve(options);

Key Differences:

  • C# returns SolverSummary directly
  • No need to pass summary as parameter
  • Summary is automatically created and returned

8. Accessing Results

C++:

std::cout << summary.termination_type << std::endl;
std::cout << summary.final_cost << std::endl;
std::cout << summary.iterations << std::endl;

C#:

Console.WriteLine($"Termination: {summary.TerminationType}");
Console.WriteLine($"Final cost: {summary.FinalCost}");
Console.WriteLine($"Iterations: {summary.Iterations}");

Key Differences:

  • C# uses properties (PascalCase) instead of fields (snake_case)
  • Enum types are strongly typed

9. Callbacks

Iteration Callback

C++:

struct IterationCallback : public ceres::IterationCallback {
    ceres::CallbackReturnType operator()(
        const ceres::IterationSummary& summary) override {
        std::cout << "Iteration: " << summary.iteration << std::endl;
        return ceres::SOLVER_CONTINUE;
    }
};

options.callbacks.push_back(new IterationCallback);

C#:

options.SetIterationCallback(summary =>
{
    Console.WriteLine($"Iteration: {summary.Iterations}");
    return true; // Continue (false to stop)
});

10. Parameter Bounds

C++:

problem.SetParameterLowerBound(parameters, 0, 0.0);
problem.SetParameterUpperBound(parameters, 0, 10.0);

C#:

problem.SetParameterLowerBound(parameters, index: 0, lowerBound: 0.0);
problem.SetParameterUpperBound(parameters, index: 0, upperBound: 10.0);

11. Setting Parameter Blocks Constant/Variable

C++:

problem.SetParameterBlockConstant(parameters);
problem.SetParameterBlockVariable(parameters);

C#:

problem.SetParameterBlockConstant(parameters);
problem.SetParameterBlockVariable(parameters);

12. Error Handling

C++:

// Ceres throws exceptions or returns error codes
try {
    problem.AddParameterBlock(parameters, size);
} catch (std::exception& e) {
    std::cerr << e.what() << std::endl;
}

C#:

try
{
    problem.AddParameterBlock(parameters, size);
}
catch (CeresException ex)
{
    Console.WriteLine($"Error: {ex.Message} (Code: {ex.ErrorCode})");
}

Key Differences:

  • All errors are wrapped in CeresException
  • Error codes are available via ex.ErrorCode property
  • Error messages are human-readable

📚 Complete Conversion Examples

Example 1: Simple Linear Least Squares

C++:

#include <ceres/ceres.h>

int main() {
    ceres::Problem problem;
    
    double x = 0.5;
    problem.AddParameterBlock(&x, 1);
    
    auto* cost_function = new ceres::AutoDiffCostFunction<CostFunctor, 1, 1>(
        new CostFunctor);
    problem.AddResidualBlock(cost_function, nullptr, &x);
    
    ceres::Solver::Options options;
    options.linear_solver_type = ceres::DENSE_QR;
    
    ceres::Solver::Summary summary;
    ceres::Solve(options, &problem, &summary);
    
    std::cout << summary.BriefReport() << std::endl;
    return 0;
}

C#:

using CeresSharp;
using CeresSharp.Enums;

var problem = new Problem();

var x = new double[] { 0.5 };
problem.AddParameterBlock(x, x.Length);

var costFunction = new AutoDiffCostFunction(
    (parameters, residuals) =>
    {
        residuals[0] = parameters[0][0] - 1.0; // Your cost function
        return true;
    },
    numResiduals: 1,
    parameterBlockSizes: new[] { 1 });

problem.AddResidualBlock(costFunction, lossFunction: null, 
    parameterBlocks: new[] { x });
// ⚠️ NOTE: Problem now owns costFunction - don't dispose it manually

var options = new SolverOptions
{
    LinearSolverType = LinearSolverType.DenseQr
};

var summary = problem.Solve(options);
Console.WriteLine(summary.FullReport);

Example 2: Pose Graph Optimization (with Quaternion)

C++:

ceres::Problem problem;
double pose[7] = {x, y, z, qx, qy, qz, qw};

problem.AddParameterBlock(pose, 7);
problem.SetManifold(pose, new ceres::QuaternionManifold);

auto* cost_function = new ceres::AutoDiffCostFunction<PoseCostFunctor, 6, 7>(
    new PoseCostFunctor);
problem.AddResidualBlock(cost_function, 
    new ceres::HuberLoss(1.0), pose);

C#:

var problem = new Problem();
var pose = new double[] { x, y, z, qx, qy, qz, qw };

problem.AddParameterBlock(pose, pose.Length);

using var quaternionManifold = new QuaternionManifold();
problem.SetManifold(pose, quaternionManifold);

var costFunction = new AutoDiffCostFunction(
    (parameters, residuals) =>
    {
        // Your pose cost function
        // parameters[0] is pose[7]
        // residuals[0..5] are 6 residuals
        return true;
    },
    numResiduals: 6,
    parameterBlockSizes: new[] { 7 });

using var loss = new HuberLoss(1.0);
problem.AddResidualBlock(costFunction, loss, 
    parameterBlocks: new[] { pose });

Example 3: AutoDiffManifold (Custom Manifolds)

C++:

// AutoDiffManifold for custom manifolds (Ceres 2.2.0)
// Example: Euclidean manifold
struct EuclideanManifold {
    template <typename T>
    bool Plus(const T* x, const T* delta, T* x_plus_delta) const {
        for (int i = 0; i < 3; i++) {
            x_plus_delta[i] = x[i] + delta[i];
        }
        return true;
    }
    
    template <typename T>
    bool Minus(const T* y, const T* x, T* y_minus_x) const {
        for (int i = 0; i < 3; i++) {
            y_minus_x[i] = y[i] - x[i];
        }
        return true;
    }
};

auto* manifold = new ceres::AutoDiffManifold<EuclideanManifold, 3, 3>(
    new EuclideanManifold);
problem.SetManifold(parameters, manifold);

C#:

// AutoDiffManifold with callbacks (similar pattern to AutoDiffCostFunction)
using var manifold = new AutoDiffManifold(
    ambientSize: 3,
    tangentSize: 3,
    plus: (x, delta, xPlusDelta) =>
    {
        // Plus operation: x + delta → x_plus_delta
        for (int i = 0; i < 3; i++)
            xPlusDelta[i] = x[i] + delta[i];
        return true;
    },
    minus: (y, x, yMinusX) =>
    {
        // Minus operation: y - x → y_minus_x
        for (int i = 0; i < 3; i++)
            yMinusX[i] = y[i] - x[i];
        return true;
    });

problem.SetManifold(parameters, manifold);
// ⚠️ IMPORTANT: Problem now owns the manifold

Use Case: ConstantYawQuaternion (Cartographer)

// Constant yaw quaternion manifold (4D ambient, 3D tangent)
// Only roll and pitch vary, yaw is constant
using var constantYawManifold = new AutoDiffManifold(
    ambientSize: 4,  // Quaternion (qx, qy, qz, qw)
    tangentSize: 3,  // Only roll, pitch vary (yaw fixed)
    plus: (x, delta, xPlusDelta) =>
    {
        // Implement Plus with constant yaw constraint
        // x is quaternion, delta is [roll_delta, pitch_delta, yaw_delta=0]
        // xPlusDelta is resulting quaternion
        // ... implementation ...
        return true;
    },
    minus: (y, x, yMinusX) =>
    {
        // Implement Minus with constant yaw constraint
        // y, x are quaternions
        // yMinusX is [roll_diff, pitch_diff, yaw_diff=0]
        // ... implementation ...
        return true;
    });

Key Differences:

  • C++ uses template class AutoDiffManifold<Functor, AmbientSize, TangentSize>
  • C# uses callback-based API with delegates (similar to AutoDiffCostFunction)
  • Jacobians are computed automatically via numeric differentiation (handled by C wrapper)
  • Perfect for custom geometric constraints (ConstantYawQuaternion, domain-specific manifolds)

Example 4: Using Interpolators

C++:

double data[10] = { /* ... */ };
ceres::BiCubicInterpolator interpolator(data, 5, 2);

double value, gradient_x, gradient_y;
interpolator.Evaluate(1.5, 0.5, &value, &gradient_x, &gradient_y);

C#:

var data = new double[10] { /* ... */ };
using var interpolator = new BiCubicInterpolator(data, rows: 5, cols: 2);

interpolator.Evaluate(x: 1.5, y: 0.5, 
    out double value, 
    out double? gradientX, 
    out double? gradientY);

⚠️ Important Differences & Gotchas

1. Memory Management

C++:

  • Manual memory management with new/delete
  • Objects can be stack-allocated

C#:

  • All objects are heap-allocated
  • Use using statements for automatic disposal
  • Objects implement IDisposable - always dispose them
// ✅ Correct
using var problem = new Problem();
using var options = new SolverOptions();
// Auto-disposed at end of scope

// ❌ Wrong - Memory leak
var problem = new Problem();
// Never disposed!

2. Array Handling

C++:

  • Raw pointers: double* parameters
  • Stack arrays: double parameters[10]

C#:

  • Managed arrays: double[] parameters
  • Arrays are automatically pinned when passed to native code
  • Do not modify arrays during solve (they're pinned)

3. Cost Function Callbacks

C++:

  • Functors with operator()
  • Template-based

C#:

  • Lambda expressions or delegates
  • Must return bool (true = success, false = failure)
  • Parameters: double[][] (array of parameter blocks)
  • Residuals: double[] (output array)

4. Null Pointers

C++:

  • nullptr for optional parameters

C#:

  • null for optional parameters (e.g., lossFunction: null)

5. Enums

C++:

  • Scoped enums: ceres::DENSE_QR
  • Integer values

C#:

  • Strongly-typed enums: LinearSolverType.DenseQr
  • Type-safe, no integer casting needed

🔍 Common Conversion Patterns

Pattern 1: Multiple Residual Blocks

C++:

for (int i = 0; i < num_observations; ++i) {
    problem.AddResidualBlock(cost_function, nullptr, &parameters[i]);
}

C#:

for (int i = 0; i < numObservations; i++)
{
    problem.AddResidualBlock(costFunction, lossFunction: null,
        parameterBlocks: new[] { parameters[i] });
}

Pattern 2: Dynamic Cost Functions

C++:

auto* cost_function = new ceres::DynamicAutoDiffCostFunction<CostFunctor>(
    new CostFunctor);
cost_function->AddParameterBlock(2);
cost_function->SetNumResiduals(1);

C#:

var costFunction = new DynamicAutoDiffCostFunction(
    (parameters, residuals) => { /* ... */ },
    numResiduals: 1,
    parameterBlockSizes: new[] { 2 });

Pattern 3: Numeric Differentiation

C++:

auto* cost_function = new ceres::NumericDiffCostFunction<CostFunctor, 
    ceres::CENTRAL, 1, 1>(new CostFunctor);

C#:

var costFunction = new NumericDiffCostFunction(
    (parameters, residuals) => { /* ... */ },
    method: NumericDiffMethod.Central,
    numResiduals: 1,
    parameterBlockSizes: new[] { 1 });

📖 API Reference Quick Lookup

Problem Operations

C++ C#
problem.AddParameterBlock(ptr, size) problem.AddParameterBlock(array, size)
problem.SetParameterBlockConstant(ptr) problem.SetParameterBlockConstant(array)
problem.AddResidualBlock(cf, loss, ptrs) problem.AddResidualBlock(cf, loss, arrays)
problem.NumParameterBlocks() problem.NumParameterBlocks (property)
problem.NumResidualBlocks() problem.NumResidualBlocks (property)

SolverOptions Properties

C++ C#
options.linear_solver_type options.LinearSolverType
options.max_num_iterations options.MaxNumIterations
options.function_tolerance options.FunctionTolerance
options.num_threads options.NumThreads
options.minimizer_progress_to_stdout options.MinimizerProgressToStdout

SolverSummary Properties

C++ C#
summary.termination_type summary.TerminationType
summary.final_cost summary.FinalCost
summary.iterations summary.Iterations
summary.BriefReport() summary.FullReport (property)

🎓 Best Practices for AI Agents

1. Always Use using Statements

// ✅ Correct
using var problem = new Problem();
using var options = new SolverOptions();
// Auto-disposed

// ❌ Wrong
var problem = new Problem();
var options = new SolverOptions();
// Must manually dispose: problem.Dispose(); options.Dispose();

2. Handle Exceptions

try
{
    problem.AddParameterBlock(parameters, size);
    var summary = problem.Solve(options);
    
    if (summary.TerminationType == TerminationType.Convergence)
    {
        // Success
    }
}
catch (CeresException ex)
{
    // Handle Ceres-specific errors
    Console.WriteLine($"Ceres error: {ex.Message}");
}
catch (ArgumentException ex)
{
    // Handle invalid arguments
    Console.WriteLine($"Invalid argument: {ex.Message}");
}

3. Reuse Objects When Possible

// ✅ Good - Reuse options
var options = new SolverOptions { MaxNumIterations = 100 };

for (int i = 0; i < 10; i++)
{
    var problem = new Problem();
    // ... setup problem ...
    var summary = problem.Solve(options); // Reuse options
    problem.Dispose();
}

options.Dispose();

4. Use Constants for Default Values

var options = new SolverOptions
{
    MaxNumIterations = Constants.DefaultMaxNumIterations,
    FunctionTolerance = Constants.DefaultFunctionTolerance,
    GradientTolerance = Constants.DefaultGradientTolerance,
    ParameterTolerance = Constants.DefaultParameterTolerance
};

5. Check Solver Results

var summary = problem.Solve(options);

switch (summary.TerminationType)
{
    case TerminationType.Convergence:
        // Success
        break;
    case TerminationType.NoConvergence:
        // Did not converge
        break;
    case TerminationType.Failure:
        // Solver failure
        break;
    case TerminationType.UserSuccess:
        // User stopped via callback
        break;
}

🔧 Advanced Features

Covariance Estimation

C++:

ceres::Covariance::Options options;
ceres::Covariance covariance(options);
covariance.Compute(covariance_blocks, &problem);

C#:

using var covOptions = new CovarianceOptions();
using var covariance = new Covariance(covOptions);
covariance.Compute(problem, covOptions, parameterBlocks);

Gradient Checker

C++:

ceres::GradientChecker checker(cost_function, manifolds, options);
checker.Probe(parameters, relative_precision, &error_message);

C#:

using var checkerOptions = new GradientCheckerOptions();
using var checker = new GradientChecker(costFunction, manifolds, checkerOptions);
var success = checker.Probe(parameters, relativePrecision, out string? errorMessage);

Context for Performance

C++:

ceres::Context context;
ceres::Problem::Options problem_options;
problem_options.context = &context;
ceres::Problem problem(problem_options);

C#:

using var context = new Context();
using var problemOptions = new ProblemOptions();
problemOptions.SetContext(context);
var problem = new Problem(problemOptions);

📝 Conversion Checklist

When converting C++ Ceres code to C#, check:

  • All ceres:: namespaces → CeresSharp. namespaces
  • Raw pointers → Managed arrays (double[])
  • Stack objects → Heap objects with using
  • Functors → Lambda expressions
  • nullptrnull
  • Snake_case → PascalCase (properties)
  • Enum values → Strongly-typed enums
  • Exception handling → CeresException
  • Memory management → using statements
  • Callbacks → Delegates with proper pinning
  • AutoDiffLocalParameterizationAutoDiffManifold (Ceres 2.2.0 migration)
  • CRITICAL: Don't manually dispose cost/loss functions added to Problem
  • CRITICAL: Let Problem manage ownership of added objects

🚀 Status

Current Status: 100% Complete

  • All 220+ APIs implemented (including AutoDiffManifold)
  • All core functionality working
  • Memory management complete
  • Error handling complete
  • Callbacks with proper memory management
  • AutoDiffManifold implemented NEW - Ready for Cartographer integration
  • Ready for production use

📚 Additional Resources

  • Implementation Progress: See IMPLEMENTATION_PROGRESS.md for detailed status
  • API Documentation: All classes have XML documentation comments
  • Examples: See conversion examples above

Quick Conversion Template

// 1. Add using statements
using CeresSharp;
using CeresSharp.Enums;

// 2. Create problem
using var problem = new Problem();

// 3. Add parameters
var parameters = new double[] { /* values */ };
problem.AddParameterBlock(parameters, parameters.Length);

// 4. Create cost function
var costFunction = new AutoDiffCostFunction(
    (params, residuals) => { /* implementation */ return true; },
    numResiduals: N,
    parameterBlockSizes: new[] { /* sizes */ });

// 5. Add residual block
using var loss = new HuberLoss(1.0); // or null
problem.AddResidualBlock(costFunction, loss, new[] { parameters });
// ⚠️ NOTE: Problem now owns costFunction and loss
// Don't manually dispose them - Problem will cleanup automatically

// 6. Configure solver
using var options = new SolverOptions
{
    LinearSolverType = LinearSolverType.DenseQr,
    MaxNumIterations = 100
};

// 7. Solve
using var summary = problem.Solve(options);

// 8. Check results
if (summary.TerminationType == TerminationType.Convergence)
{
    // Success - use optimized parameters
}

⚠️ Critical: Memory Management and Ownership

Cost Function Ownership

IMPORTANT: When you add a cost function to a Problem via AddResidualBlock(), the Problem takes ownership of the cost function. This means:

  1. DO: Let Problem manage the cost function lifecycle
  2. DON'T: Manually dispose cost functions that have been added to a Problem
  3. DO: Dispose the Problem, which will automatically cleanup all owned cost functions

Correct Usage:

using var problem = new Problem();
var x = new double[] { 0.0 };
problem.AddParameterBlock(x, x.Length);

// Create cost function
var costFunction = new AutoDiffCostFunction(
    (parameters, residuals) => {
        residuals[0] = parameters[0][0] - 2.0;
        return true;
    },
    numResiduals: 1,
    parameterBlockSizes: new[] { 1 });

// Add to problem - Problem now owns the cost function
problem.AddResidualBlock(costFunction, lossFunction: null, 
    parameterBlocks: new[] { x });

// ✅ CORRECT: Just dispose the problem
// Cost function will be automatically cleaned up by Problem
// ❌ WRONG: costFunction.Dispose(); // Don't do this!

using var options = new SolverOptions { /* ... */ };
using var summary = problem.Solve(options);
// Problem.Dispose() is called automatically by 'using' statement

Why This Matters:

  • If you manually dispose a cost function that was added to a Problem, you'll get a double-free crash in the finalizer thread
  • The Problem will try to delete the cost function when it's disposed, but it's already been deleted
  • This causes a segmentation fault in the native library

Loss Function Ownership

Similar to cost functions, Problem also owns loss functions when added via AddResidualBlock():

using var loss = new HuberLoss(1.0);
problem.AddResidualBlock(costFunction, loss, parameterBlocks);

// ✅ CORRECT: Let Problem cleanup the loss function
// ❌ WRONG: loss.Dispose(); // Don't do this!

Best Practices for Memory Management

  1. Always use using statements for Problem, SolverOptions, and SolverSummary:

    using var problem = new Problem();
    using var options = new SolverOptions { /* ... */ };
    using var summary = problem.Solve(options);
    
  2. Don't manually dispose objects owned by Problem:

    • Cost functions added via AddResidualBlock()
    • Loss functions added via AddResidualBlock()
    • Manifolds set via SetManifold()
  3. Parameter blocks are managed automatically:

    • Arrays are pinned automatically when passed to native code
    • They remain valid for the lifetime of the Problem
    • No manual memory management needed
  4. Dispose order matters (handled automatically by using):

    // Correct order (automatic with 'using'):
    // 1. Dispose SolverSummary (innermost)
    // 2. Dispose SolverOptions
    // 3. Dispose Problem (outermost) - this cleans up all owned objects
    

Known Issues and Workarounds

Double-Free Prevention

The library has been fixed to prevent double-free crashes. The fix ensures that:

  • Cost functions are not deleted by CostFunctionHandle when they're owned by Problem
  • Problem properly cleans up all owned objects when disposed
  • Finalizers don't attempt to free already-freed resources

If you encounter crashes:

  1. Ensure you're not manually disposing cost/loss functions that were added to a Problem
  2. Use using statements for proper disposal order
  3. Don't keep references to disposed objects

🔍 Troubleshooting

Common Issues

Issue: "Test Run Aborted" or Process Crash

Cause: Double-free of cost functions or improper disposal order

Solution:

  • Use using statements for automatic disposal
  • Don't manually dispose cost/loss functions added to Problem
  • Let Problem manage the lifecycle of owned objects

Issue: "Initial residual and Jacobian evaluation failed"

Cause: Invalid cost function implementation or parameter setup

Solution:

  • Check that your cost function returns true on success
  • Verify parameter block sizes match your cost function
  • Ensure initial parameter values are valid

Issue: Tests pass but process crashes after completion

Cause: This was a known issue (now fixed) related to double-free in finalizer thread

Solution:

  • Fixed in current version
  • Ensure you're using the latest version of CeresSharp
  • Follow the memory management best practices above

Last Updated: 2024-12-19
Version: 1.0.0
Ceres Solver Version: 2.2.0
AutoDiffManifold: Implemented - Ready for Cartographer integration (ConstantYawQuaternion use case)