1128 lines
29 KiB
Markdown
1128 lines
29 KiB
Markdown
# 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
|
||
|
||
```csharp
|
||
using CeresSharp;
|
||
using CeresSharp.Enums;
|
||
using CeresSharp.Advanced; // For advanced features
|
||
```
|
||
|
||
---
|
||
|
||
## 🔄 Conversion Patterns
|
||
|
||
### 1. Problem Creation
|
||
|
||
**C++:**
|
||
```cpp
|
||
ceres::Problem problem;
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
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++:**
|
||
```cpp
|
||
double parameters[2] = {1.0, 2.0};
|
||
problem.AddParameterBlock(parameters, 2);
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
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++:**
|
||
```cpp
|
||
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#:**
|
||
```csharp
|
||
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++:**
|
||
```cpp
|
||
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#:**
|
||
```csharp
|
||
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++:**
|
||
```cpp
|
||
ceres::HuberLoss loss(1.0);
|
||
problem.AddResidualBlock(cost_function, &loss, parameters);
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
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++:**
|
||
```cpp
|
||
ceres::QuaternionManifold* manifold = new ceres::QuaternionManifold();
|
||
problem.SetManifold(parameters, manifold);
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
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++:**
|
||
```cpp
|
||
ceres::Solver::Options options;
|
||
options.linear_solver_type = ceres::DENSE_QR;
|
||
options.max_num_iterations = 100;
|
||
options.function_tolerance = 1e-6;
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
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:**
|
||
|
||
```csharp
|
||
using CeresSharp;
|
||
|
||
var options = new SolverOptions
|
||
{
|
||
MaxNumIterations = Constants.DefaultMaxNumIterations,
|
||
FunctionTolerance = Constants.DefaultFunctionTolerance,
|
||
GradientTolerance = Constants.DefaultGradientTolerance
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
### 7. Solving
|
||
|
||
**C++:**
|
||
```cpp
|
||
ceres::Solver::Summary summary;
|
||
ceres::Solve(options, &problem, &summary);
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
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++:**
|
||
```cpp
|
||
std::cout << summary.termination_type << std::endl;
|
||
std::cout << summary.final_cost << std::endl;
|
||
std::cout << summary.iterations << std::endl;
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
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++:**
|
||
```cpp
|
||
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#:**
|
||
```csharp
|
||
options.SetIterationCallback(summary =>
|
||
{
|
||
Console.WriteLine($"Iteration: {summary.Iterations}");
|
||
return true; // Continue (false to stop)
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
### 10. Parameter Bounds
|
||
|
||
**C++:**
|
||
```cpp
|
||
problem.SetParameterLowerBound(parameters, 0, 0.0);
|
||
problem.SetParameterUpperBound(parameters, 0, 10.0);
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
problem.SetParameterLowerBound(parameters, index: 0, lowerBound: 0.0);
|
||
problem.SetParameterUpperBound(parameters, index: 0, upperBound: 10.0);
|
||
```
|
||
|
||
---
|
||
|
||
### 11. Setting Parameter Blocks Constant/Variable
|
||
|
||
**C++:**
|
||
```cpp
|
||
problem.SetParameterBlockConstant(parameters);
|
||
problem.SetParameterBlockVariable(parameters);
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
problem.SetParameterBlockConstant(parameters);
|
||
problem.SetParameterBlockVariable(parameters);
|
||
```
|
||
|
||
---
|
||
|
||
### 12. Error Handling
|
||
|
||
**C++:**
|
||
```cpp
|
||
// Ceres throws exceptions or returns error codes
|
||
try {
|
||
problem.AddParameterBlock(parameters, size);
|
||
} catch (std::exception& e) {
|
||
std::cerr << e.what() << std::endl;
|
||
}
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
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++:**
|
||
```cpp
|
||
#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#:**
|
||
```csharp
|
||
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++:**
|
||
```cpp
|
||
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#:**
|
||
```csharp
|
||
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++:**
|
||
```cpp
|
||
// 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#:**
|
||
```csharp
|
||
// 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)**
|
||
```csharp
|
||
// 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++:**
|
||
```cpp
|
||
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#:**
|
||
```csharp
|
||
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**
|
||
|
||
```csharp
|
||
// ✅ 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++:**
|
||
```cpp
|
||
for (int i = 0; i < num_observations; ++i) {
|
||
problem.AddResidualBlock(cost_function, nullptr, ¶meters[i]);
|
||
}
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
for (int i = 0; i < numObservations; i++)
|
||
{
|
||
problem.AddResidualBlock(costFunction, lossFunction: null,
|
||
parameterBlocks: new[] { parameters[i] });
|
||
}
|
||
```
|
||
|
||
### Pattern 2: Dynamic Cost Functions
|
||
|
||
**C++:**
|
||
```cpp
|
||
auto* cost_function = new ceres::DynamicAutoDiffCostFunction<CostFunctor>(
|
||
new CostFunctor);
|
||
cost_function->AddParameterBlock(2);
|
||
cost_function->SetNumResiduals(1);
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
var costFunction = new DynamicAutoDiffCostFunction(
|
||
(parameters, residuals) => { /* ... */ },
|
||
numResiduals: 1,
|
||
parameterBlockSizes: new[] { 2 });
|
||
```
|
||
|
||
### Pattern 3: Numeric Differentiation
|
||
|
||
**C++:**
|
||
```cpp
|
||
auto* cost_function = new ceres::NumericDiffCostFunction<CostFunctor,
|
||
ceres::CENTRAL, 1, 1>(new CostFunctor);
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
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
|
||
|
||
```csharp
|
||
// ✅ 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
|
||
|
||
```csharp
|
||
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
|
||
|
||
```csharp
|
||
// ✅ 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
|
||
|
||
```csharp
|
||
var options = new SolverOptions
|
||
{
|
||
MaxNumIterations = Constants.DefaultMaxNumIterations,
|
||
FunctionTolerance = Constants.DefaultFunctionTolerance,
|
||
GradientTolerance = Constants.DefaultGradientTolerance,
|
||
ParameterTolerance = Constants.DefaultParameterTolerance
|
||
};
|
||
```
|
||
|
||
### 5. Check Solver Results
|
||
|
||
```csharp
|
||
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++:**
|
||
```cpp
|
||
ceres::Covariance::Options options;
|
||
ceres::Covariance covariance(options);
|
||
covariance.Compute(covariance_blocks, &problem);
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
using var covOptions = new CovarianceOptions();
|
||
using var covariance = new Covariance(covOptions);
|
||
covariance.Compute(problem, covOptions, parameterBlocks);
|
||
```
|
||
|
||
### Gradient Checker
|
||
|
||
**C++:**
|
||
```cpp
|
||
ceres::GradientChecker checker(cost_function, manifolds, options);
|
||
checker.Probe(parameters, relative_precision, &error_message);
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
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++:**
|
||
```cpp
|
||
ceres::Context context;
|
||
ceres::Problem::Options problem_options;
|
||
problem_options.context = &context;
|
||
ceres::Problem problem(problem_options);
|
||
```
|
||
|
||
**C#:**
|
||
```csharp
|
||
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
|
||
- [ ] `nullptr` → `null`
|
||
- [ ] Snake_case → PascalCase (properties)
|
||
- [ ] Enum values → Strongly-typed enums
|
||
- [ ] Exception handling → `CeresException`
|
||
- [ ] Memory management → `using` statements
|
||
- [ ] Callbacks → Delegates with proper pinning
|
||
- [ ] `AutoDiffLocalParameterization` → `AutoDiffManifold` (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
|
||
|
||
```csharp
|
||
// 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**:
|
||
```csharp
|
||
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()`:
|
||
|
||
```csharp
|
||
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:
|
||
```csharp
|
||
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`):
|
||
```csharp
|
||
// 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)
|