Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
using System;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
namespace CeresSharp.Advanced;
/// <summary>
/// Context for performance optimization.
/// Allows reuse of expensive objects across multiple solves.
/// </summary>
public sealed class Context : IDisposable
{
private readonly ContextHandle _handle;
private bool _disposed;
/// <summary>
/// Creates a new Context instance.
/// </summary>
public Context()
{
_handle = ContextHandle.Create();
}
/// <summary>
/// Gets the native handle.
/// </summary>
internal ContextHandle Handle => _handle;
public void Dispose()
{
if (!_disposed)
{
_handle?.Dispose();
_disposed = true;
}
}
}

View File

@@ -0,0 +1,211 @@
using System;
using CeresSharp;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
namespace CeresSharp.Advanced;
/// <summary>
/// Covariance estimation for parameter blocks.
/// </summary>
public sealed class Covariance : IDisposable
{
private readonly CovarianceHandle _handle;
private bool _disposed;
/// <summary>
/// Creates a new Covariance instance with default options.
/// </summary>
public Covariance()
{
_handle = CovarianceHandle.Create();
}
/// <summary>
/// Creates a new Covariance instance with the specified options.
/// </summary>
public Covariance(CovarianceOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
_handle = CovarianceHandle.CreateWithOptions(options.Handle.DangerousGetHandle());
}
/// <summary>
/// Gets the native handle.
/// </summary>
internal CovarianceHandle Handle => _handle;
/// <summary>
/// Computes the covariance for the specified parameter blocks.
/// </summary>
/// <param name="problem">The problem.</param>
/// <param name="options">The covariance options.</param>
/// <param name="parameterBlocks">Array of parameter block arrays.</param>
/// <exception cref="Exceptions.CeresException">Thrown if the operation fails.</exception>
public void Compute(Problem problem, CovarianceOptions options, double[][] parameterBlocks)
{
if (problem == null)
throw new ArgumentNullException(nameof(problem));
if (options == null)
throw new ArgumentNullException(nameof(options));
if (parameterBlocks == null || parameterBlocks.Length == 0)
throw new ArgumentException("Parameter blocks cannot be null or empty", nameof(parameterBlocks));
unsafe
{
var parameterBlockPtrs = new IntPtr[parameterBlocks.Length];
var pinnedArrays = new System.Runtime.InteropServices.GCHandle[parameterBlocks.Length];
try
{
for (int i = 0; i < parameterBlocks.Length; i++)
{
if (parameterBlocks[i] == null)
throw new ArgumentException($"Parameter block {i} is null", nameof(parameterBlocks));
var handle = System.Runtime.InteropServices.GCHandle.Alloc(parameterBlocks[i], System.Runtime.InteropServices.GCHandleType.Pinned);
pinnedArrays[i] = handle;
parameterBlockPtrs[i] = handle.AddrOfPinnedObject();
}
var errorMessage = new System.Text.StringBuilder(512);
fixed (IntPtr* ptrs = parameterBlockPtrs)
{
var errorCode = CeresNative.ceres_wrapper_covariance_compute(
_handle.DangerousGetHandle(),
problem.Handle.DangerousGetHandle(),
options.Handle.DangerousGetHandle(),
(IntPtr)ptrs,
parameterBlocks.Length,
errorMessage,
errorMessage.Capacity);
if (errorCode != Exceptions.CeresErrorCode.Success)
{
var message = errorMessage.Length > 0 ? errorMessage.ToString() : null;
throw new Exceptions.CeresException(errorCode, message);
}
}
}
finally
{
foreach (var handle in pinnedArrays)
{
if (handle.IsAllocated)
handle.Free();
}
}
}
}
/// <summary>
/// Gets the covariance block between two parameter blocks.
/// </summary>
/// <param name="parameterBlock1">First parameter block.</param>
/// <param name="parameterBlock2">Second parameter block.</param>
/// <param name="covarianceBlock">Output covariance block matrix (row-major).</param>
/// <exception cref="Exceptions.CeresException">Thrown if the operation fails.</exception>
public void GetCovarianceBlock(double[] parameterBlock1, double[] parameterBlock2, double[] covarianceBlock)
{
if (parameterBlock1 == null)
throw new ArgumentNullException(nameof(parameterBlock1));
if (parameterBlock2 == null)
throw new ArgumentNullException(nameof(parameterBlock2));
if (covarianceBlock == null)
throw new ArgumentNullException(nameof(covarianceBlock));
unsafe
{
var errorMessage = new System.Text.StringBuilder(512);
fixed (double* ptr1 = parameterBlock1)
fixed (double* ptr2 = parameterBlock2)
fixed (double* covPtr = covarianceBlock)
{
var errorCode = CeresNative.ceres_wrapper_covariance_get_covariance_block(
_handle.DangerousGetHandle(),
(IntPtr)ptr1,
(IntPtr)ptr2,
(IntPtr)covPtr,
errorMessage,
errorMessage.Capacity);
if (errorCode != Exceptions.CeresErrorCode.Success)
{
var message = errorMessage.Length > 0 ? errorMessage.ToString() : null;
throw new Exceptions.CeresException(errorCode, message);
}
}
}
}
/// <summary>
/// Gets the covariance matrix for multiple parameter blocks.
/// </summary>
/// <param name="parameterBlocks">Array of parameter block arrays.</param>
/// <param name="covarianceMatrix">Output covariance matrix (row-major).</param>
/// <exception cref="Exceptions.CeresException">Thrown if the operation fails.</exception>
public void GetCovarianceMatrix(double[][] parameterBlocks, double[] covarianceMatrix)
{
if (parameterBlocks == null || parameterBlocks.Length == 0)
throw new ArgumentException("Parameter blocks cannot be null or empty", nameof(parameterBlocks));
if (covarianceMatrix == null)
throw new ArgumentNullException(nameof(covarianceMatrix));
unsafe
{
var parameterBlockPtrs = new IntPtr[parameterBlocks.Length];
var pinnedArrays = new System.Runtime.InteropServices.GCHandle[parameterBlocks.Length];
try
{
for (int i = 0; i < parameterBlocks.Length; i++)
{
if (parameterBlocks[i] == null)
throw new ArgumentException($"Parameter block {i} is null", nameof(parameterBlocks));
var handle = System.Runtime.InteropServices.GCHandle.Alloc(parameterBlocks[i], System.Runtime.InteropServices.GCHandleType.Pinned);
pinnedArrays[i] = handle;
parameterBlockPtrs[i] = handle.AddrOfPinnedObject();
}
var errorMessage = new System.Text.StringBuilder(512);
fixed (IntPtr* ptrs = parameterBlockPtrs)
fixed (double* covPtr = covarianceMatrix)
{
var errorCode = CeresNative.ceres_wrapper_covariance_get_covariance_matrix(
_handle.DangerousGetHandle(),
(IntPtr)ptrs,
parameterBlocks.Length,
(IntPtr)covPtr,
errorMessage,
errorMessage.Capacity);
if (errorCode != Exceptions.CeresErrorCode.Success)
{
var message = errorMessage.Length > 0 ? errorMessage.ToString() : null;
throw new Exceptions.CeresException(errorCode, message);
}
}
}
finally
{
foreach (var handle in pinnedArrays)
{
if (handle.IsAllocated)
handle.Free();
}
}
}
}
public void Dispose()
{
if (!_disposed)
{
_handle?.Dispose();
_disposed = true;
}
}
}

View File

@@ -0,0 +1,91 @@
using System;
using CeresSharp.Enums;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
namespace CeresSharp.Advanced;
/// <summary>
/// Options for covariance estimation.
/// </summary>
public sealed class CovarianceOptions : IDisposable
{
private readonly CovarianceOptionsHandle _handle;
private bool _disposed;
/// <summary>
/// Creates a new CovarianceOptions instance with default settings.
/// </summary>
public CovarianceOptions()
{
_handle = CovarianceOptionsHandle.Create();
}
/// <summary>
/// Gets the native handle.
/// </summary>
internal CovarianceOptionsHandle Handle => _handle;
/// <summary>
/// Gets or sets the number of threads to use.
/// </summary>
public int NumThreads
{
get => CeresNative.ceres_wrapper_covariance_options_get_num_threads(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_covariance_options_set_num_threads(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets the sparse linear algebra library type.
/// </summary>
public SparseLinearAlgebraLibraryType SparseLinearAlgebraLibraryType
{
get => (SparseLinearAlgebraLibraryType)CeresNative.ceres_wrapper_covariance_options_get_sparse_linear_algebra_library_type(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_covariance_options_set_sparse_linear_algebra_library_type(_handle.DangerousGetHandle(), (int)value);
}
/// <summary>
/// Gets or sets the algorithm type.
/// </summary>
public CovarianceAlgorithmType AlgorithmType
{
get => (CovarianceAlgorithmType)CeresNative.ceres_wrapper_covariance_options_get_algorithm_type(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_covariance_options_set_algorithm_type(_handle.DangerousGetHandle(), (int)value);
}
/// <summary>
/// Gets or sets the minimum reciprocal condition number.
/// </summary>
public double MinReciprocalConditionNumber
{
get => CeresNative.ceres_wrapper_covariance_options_get_min_reciprocal_condition_number(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_covariance_options_set_min_reciprocal_condition_number(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets the null space rank.
/// </summary>
public int NullSpaceRank
{
get => CeresNative.ceres_wrapper_covariance_options_get_null_space_rank(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_covariance_options_set_null_space_rank(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets whether to apply loss function.
/// </summary>
public bool ApplyLossFunction
{
get => CeresNative.ceres_wrapper_covariance_options_get_apply_loss_function(_handle.DangerousGetHandle()) != 0;
set => CeresNative.ceres_wrapper_covariance_options_set_apply_loss_function(_handle.DangerousGetHandle(), value ? 1 : 0);
}
public void Dispose()
{
if (!_disposed)
{
_handle?.Dispose();
_disposed = true;
}
}
}

View File

@@ -0,0 +1,10 @@
namespace CeresSharp.Advanced;
/// <summary>
/// Delegate for evaluation callback.
/// Called before evaluating cost functions to allow shared computation.
/// </summary>
/// <param name="numResiduals">Number of residuals.</param>
/// <param name="numParameterBlocks">Number of parameter blocks.</param>
/// <param name="parameterBlockSizes">Array of sizes for each parameter block (can be null).</param>
public delegate void EvaluationCallback(int numResiduals, int numParameterBlocks, int[]? parameterBlockSizes);

View File

@@ -0,0 +1,211 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
using CeresSharp;
namespace CeresSharp.Advanced;
/// <summary>
/// Gradient checker for validating cost function gradients.
/// </summary>
public sealed class GradientChecker : IDisposable
{
private readonly GradientCheckerHandle _handle;
private bool _disposed;
/// <summary>
/// Creates a new GradientChecker instance.
/// </summary>
/// <param name="costFunction">The cost function to check.</param>
/// <param name="manifolds">Array of manifolds (can be null if no manifolds).</param>
/// <param name="options">The gradient checker options.</param>
public GradientChecker(CostFunction costFunction, Manifold[]? manifolds, GradientCheckerOptions options)
{
if (costFunction == null)
throw new ArgumentNullException(nameof(costFunction));
if (options == null)
throw new ArgumentNullException(nameof(options));
unsafe
{
IntPtr manifoldsPtr = IntPtr.Zero;
int numManifolds = 0;
if (manifolds != null && manifolds.Length > 0)
{
var manifoldHandles = new IntPtr[manifolds.Length];
for (int i = 0; i < manifolds.Length; i++)
{
if (manifolds[i] == null)
throw new ArgumentException($"Manifold {i} is null", nameof(manifolds));
manifoldHandles[i] = manifolds[i].Handle;
}
fixed (IntPtr* ptr = manifoldHandles)
{
manifoldsPtr = (IntPtr)ptr;
numManifolds = manifolds.Length;
}
}
var handle = CeresNative.ceres_wrapper_create_gradient_checker(
costFunction.Handle,
manifoldsPtr,
numManifolds,
options.Handle.DangerousGetHandle());
_handle = GradientCheckerHandle.Create(handle);
}
}
/// <summary>
/// Gets the native handle.
/// </summary>
internal GradientCheckerHandle Handle => _handle;
/// <summary>
/// Probes gradients at the given parameters.
/// </summary>
/// <param name="parameters">Array of parameter block arrays.</param>
/// <param name="relativePrecision">Relative precision for comparison.</param>
/// <returns>True if gradients match, false otherwise.</returns>
/// <exception cref="Exceptions.CeresException">Thrown if the operation fails (other than gradient mismatch).</exception>
public bool Probe(double[][] parameters, double relativePrecision)
{
if (parameters == null || parameters.Length == 0)
throw new ArgumentException("Parameters cannot be null or empty", nameof(parameters));
unsafe
{
var parameterBlockPtrs = new IntPtr[parameters.Length];
var pinnedArrays = new GCHandle[parameters.Length];
try
{
for (int i = 0; i < parameters.Length; i++)
{
if (parameters[i] == null)
throw new ArgumentException($"Parameter block {i} is null", nameof(parameters));
var handle = GCHandle.Alloc(parameters[i], GCHandleType.Pinned);
pinnedArrays[i] = handle;
parameterBlockPtrs[i] = handle.AddrOfPinnedObject();
}
var sb = new StringBuilder(512);
fixed (IntPtr* ptrs = parameterBlockPtrs)
{
var errorCode = CeresNative.ceres_wrapper_gradient_checker_probe(
_handle.DangerousGetHandle(),
(IntPtr)ptrs,
relativePrecision,
sb,
sb.Capacity);
// InvalidParameter means gradients don't match (expected behavior)
if (errorCode == Exceptions.CeresErrorCode.InvalidParameter)
{
return false;
}
// Other error codes indicate actual errors
if (errorCode != Exceptions.CeresErrorCode.Success)
{
var message = sb.Length > 0 ? sb.ToString() : null;
throw new Exceptions.CeresException(errorCode, message);
}
return true;
}
}
finally
{
foreach (var handle in pinnedArrays)
{
if (handle.IsAllocated)
handle.Free();
}
}
}
}
/// <summary>
/// Probes gradients at the given parameters and returns error message if they don't match.
/// </summary>
/// <param name="parameters">Array of parameter block arrays.</param>
/// <param name="relativePrecision">Relative precision for comparison.</param>
/// <param name="errorMessage">Output error message if gradients don't match.</param>
/// <returns>True if gradients match, false otherwise.</returns>
/// <exception cref="Exceptions.CeresException">Thrown if the operation fails (other than gradient mismatch).</exception>
public bool Probe(double[][] parameters, double relativePrecision, out string? errorMessage)
{
if (parameters == null || parameters.Length == 0)
throw new ArgumentException("Parameters cannot be null or empty", nameof(parameters));
unsafe
{
var parameterBlockPtrs = new IntPtr[parameters.Length];
var pinnedArrays = new GCHandle[parameters.Length];
try
{
for (int i = 0; i < parameters.Length; i++)
{
if (parameters[i] == null)
throw new ArgumentException($"Parameter block {i} is null", nameof(parameters));
var handle = GCHandle.Alloc(parameters[i], GCHandleType.Pinned);
pinnedArrays[i] = handle;
parameterBlockPtrs[i] = handle.AddrOfPinnedObject();
}
var sb = new StringBuilder(512);
fixed (IntPtr* ptrs = parameterBlockPtrs)
{
var errorCode = CeresNative.ceres_wrapper_gradient_checker_probe(
_handle.DangerousGetHandle(),
(IntPtr)ptrs,
relativePrecision,
sb,
sb.Capacity);
// InvalidParameter means gradients don't match (expected behavior)
if (errorCode == Exceptions.CeresErrorCode.InvalidParameter)
{
errorMessage = sb.Length > 0 ? sb.ToString() : "Gradient check failed";
return false;
}
// Other error codes indicate actual errors
if (errorCode != Exceptions.CeresErrorCode.Success)
{
var message = sb.Length > 0 ? sb.ToString() : null;
throw new Exceptions.CeresException(errorCode, message);
}
errorMessage = null;
return true;
}
}
finally
{
foreach (var handle in pinnedArrays)
{
if (handle.IsAllocated)
handle.Free();
}
}
}
}
public void Dispose()
{
if (!_disposed)
{
_handle?.Dispose();
_disposed = true;
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
namespace CeresSharp.Advanced;
/// <summary>
/// Options for gradient checking.
/// </summary>
public sealed class GradientCheckerOptions : IDisposable
{
private readonly GradientCheckerOptionsHandle _handle;
private bool _disposed;
/// <summary>
/// Creates a new GradientCheckerOptions instance with default settings.
/// </summary>
public GradientCheckerOptions()
{
_handle = GradientCheckerOptionsHandle.Create();
}
/// <summary>
/// Gets the native handle.
/// </summary>
internal GradientCheckerOptionsHandle Handle => _handle;
/// <summary>
/// Gets or sets the gradient check relative precision.
/// </summary>
public double GradientCheckRelativePrecision
{
get => CeresNative.ceres_wrapper_gradient_checker_options_get_gradient_check_relative_precision(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_gradient_checker_options_set_gradient_check_relative_precision(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets the numeric derivative relative step size.
/// </summary>
public double NumericDerivativeRelativeStepSize
{
get => CeresNative.ceres_wrapper_gradient_checker_options_get_gradient_check_numeric_derivative_relative_step_size(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_gradient_checker_options_set_gradient_check_numeric_derivative_relative_step_size(_handle.DangerousGetHandle(), value);
}
public void Dispose()
{
if (!_disposed)
{
_handle?.Dispose();
_disposed = true;
}
}
}

View File

@@ -0,0 +1,153 @@
using System;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
namespace CeresSharp.Advanced;
/// <summary>
/// Options for configuring a Ceres Problem.
/// </summary>
public sealed class ProblemOptions : IDisposable
{
private readonly ProblemOptionsHandle _handle;
private System.Runtime.InteropServices.GCHandle? _evaluationCallbackHandle;
private System.Runtime.InteropServices.GCHandle? _evaluationNativeCallbackHandle;
private bool _disposed;
/// <summary>
/// Creates a new ProblemOptions instance with default settings.
/// </summary>
public ProblemOptions()
{
_handle = ProblemOptionsHandle.Create();
}
/// <summary>
/// Gets the native handle.
/// </summary>
internal ProblemOptionsHandle Handle => _handle;
/// <summary>
/// Gets or sets whether the problem owns cost functions.
/// </summary>
public bool CostFunctionOwnership
{
get => CeresNative.ceres_wrapper_problem_options_get_cost_function_ownership(_handle.DangerousGetHandle()) != 0;
set => CeresNative.ceres_wrapper_problem_options_set_cost_function_ownership(_handle.DangerousGetHandle(), value ? 1 : 0);
}
/// <summary>
/// Gets or sets whether the problem owns loss functions.
/// </summary>
public bool LossFunctionOwnership
{
get => CeresNative.ceres_wrapper_problem_options_get_loss_function_ownership(_handle.DangerousGetHandle()) != 0;
set => CeresNative.ceres_wrapper_problem_options_set_loss_function_ownership(_handle.DangerousGetHandle(), value ? 1 : 0);
}
/// <summary>
/// Gets or sets whether the problem owns manifolds.
/// </summary>
public bool ManifoldOwnership
{
get => CeresNative.ceres_wrapper_problem_options_get_manifold_ownership(_handle.DangerousGetHandle()) != 0;
set => CeresNative.ceres_wrapper_problem_options_set_manifold_ownership(_handle.DangerousGetHandle(), value ? 1 : 0);
}
/// <summary>
/// Gets or sets whether to enable fast removal of residual blocks.
/// </summary>
public bool EnableFastRemoval
{
get => CeresNative.ceres_wrapper_problem_options_get_enable_fast_removal(_handle.DangerousGetHandle()) != 0;
set => CeresNative.ceres_wrapper_problem_options_set_enable_fast_removal(_handle.DangerousGetHandle(), value ? 1 : 0);
}
/// <summary>
/// Gets or sets whether to disable all safety checks.
/// </summary>
public bool DisableAllSafetyChecks
{
get => CeresNative.ceres_wrapper_problem_options_get_disable_all_safety_checks(_handle.DangerousGetHandle()) != 0;
set => CeresNative.ceres_wrapper_problem_options_set_disable_all_safety_checks(_handle.DangerousGetHandle(), value ? 1 : 0);
}
/// <summary>
/// Sets the context for performance optimization.
/// </summary>
public void SetContext(Context context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
CeresNative.ceres_wrapper_problem_options_set_context(
_handle.DangerousGetHandle(),
context.Handle.DangerousGetHandle());
}
/// <summary>
/// Sets an evaluation callback for shared computation.
/// Called before evaluating cost functions to allow shared computation.
/// </summary>
/// <param name="callback">The evaluation callback.</param>
public void SetEvaluationCallback(EvaluationCallback callback)
{
if (callback == null)
throw new ArgumentNullException(nameof(callback));
// Pin the callback delegate
var callbackHandle = System.Runtime.InteropServices.GCHandle.Alloc(callback);
// Create native callback wrapper
var nativeCallback = new CeresNative.CeresEvaluationCallback((userData, numResiduals, numParameterBlocks, parameterBlockSizesPtr) =>
{
try
{
var handle = System.Runtime.InteropServices.GCHandle.FromIntPtr(userData);
var csCallback = (EvaluationCallback)handle.Target!;
// Parse parameter block sizes
int[]? parameterBlockSizes = null;
if (parameterBlockSizesPtr != IntPtr.Zero && numParameterBlocks > 0)
{
parameterBlockSizes = new int[numParameterBlocks];
System.Runtime.InteropServices.Marshal.Copy(parameterBlockSizesPtr, parameterBlockSizes, 0, numParameterBlocks);
}
// Call C# callback
csCallback(numResiduals, numParameterBlocks, parameterBlockSizes);
}
catch
{
// Ignore errors in callback
}
});
// Pin the native callback
var nativeCallbackHandle = System.Runtime.InteropServices.GCHandle.Alloc(nativeCallback);
CeresNative.ceres_wrapper_problem_options_set_evaluation_callback(
_handle.DangerousGetHandle(),
nativeCallback,
System.Runtime.InteropServices.GCHandle.ToIntPtr(callbackHandle));
// Track handles for cleanup when ProblemOptions is disposed
_evaluationCallbackHandle = callbackHandle;
_evaluationNativeCallbackHandle = nativeCallbackHandle;
}
public void Dispose()
{
if (!_disposed)
{
// Cleanup callback handles
if (_evaluationCallbackHandle.HasValue && _evaluationCallbackHandle.Value.IsAllocated)
_evaluationCallbackHandle.Value.Free();
if (_evaluationNativeCallbackHandle.HasValue && _evaluationNativeCallbackHandle.Value.IsAllocated)
_evaluationNativeCallbackHandle.Value.Free();
_handle?.Dispose();
_disposed = true;
}
}
}

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,271 @@
using System;
using System.Runtime.InteropServices;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
namespace CeresSharp;
/// <summary>
/// AutoDiff manifold for custom manifolds defined via Plus and Minus operations.
/// This is the replacement for AutoDiffLocalParameterization in Ceres 2.2.0.
/// </summary>
/// <remarks>
/// <para>
/// AutoDiffManifold allows you to define custom manifolds by providing Plus and Minus operations
/// as C# delegates. The Jacobians are computed automatically using numeric differentiation.
/// </para>
/// <para>
/// Use cases:
/// - ConstantYawQuaternion: Quaternion with constant yaw angle (Cartographer IMU extrapolation)
/// - Custom geometric constraints
/// - Domain-specific manifolds
/// </para>
/// <para>
/// Example (Euclidean manifold):
/// <code>
/// var manifold = new AutoDiffManifold(
/// ambientSize: 3,
/// tangentSize: 3,
/// plus: (x, delta, xPlusDelta) => {
/// for (int i = 0; i < 3; i++)
/// xPlusDelta[i] = x[i] + delta[i];
/// return true;
/// },
/// minus: (y, x, yMinusX) => {
/// for (int i = 0; i < 3; i++)
/// yMinusX[i] = y[i] - x[i];
/// return true;
/// });
/// </code>
/// </para>
/// </remarks>
public sealed class AutoDiffManifold : Manifold
{
private readonly GCHandle _wrapperHandle;
private readonly int _ambientSize;
private readonly int _tangentSize;
private bool _disposed;
/// <summary>
/// Delegate for Plus operation: x + delta → x_plus_delta
/// </summary>
/// <param name="x">Point on manifold (ambient_size elements)</param>
/// <param name="delta">Tangent vector (tangent_size elements)</param>
/// <param name="xPlusDelta">Output point on manifold (ambient_size elements)</param>
/// <returns>true on success, false on failure</returns>
public delegate bool PlusOperation(double[] x, double[] delta, double[] xPlusDelta);
/// <summary>
/// Delegate for Minus operation: y - x → y_minus_x
/// </summary>
/// <param name="y">Point on manifold (ambient_size elements)</param>
/// <param name="x">Point on manifold (ambient_size elements)</param>
/// <param name="yMinusX">Output tangent vector (tangent_size elements)</param>
/// <returns>true on success, false on failure</returns>
public delegate bool MinusOperation(double[] y, double[] x, double[] yMinusX);
/// <summary>
/// Creates a new AutoDiff manifold.
/// </summary>
/// <param name="ambientSize">Dimension of ambient space (must be positive)</param>
/// <param name="tangentSize">Dimension of tangent space (must be positive, ≤ ambientSize)</param>
/// <param name="plus">Plus operation: x + delta → x_plus_delta</param>
/// <param name="minus">Minus operation: y - x → y_minus_x</param>
/// <exception cref="ArgumentNullException">Thrown when plus or minus is null</exception>
/// <exception cref="ArgumentException">Thrown when sizes are invalid</exception>
/// <exception cref="CeresException">Thrown when native creation fails</exception>
public AutoDiffManifold(
int ambientSize,
int tangentSize,
PlusOperation plus,
MinusOperation minus)
: base(CreateHandle(ambientSize, tangentSize, plus, minus, out var wrapperHandle))
{
_ambientSize = ambientSize;
_tangentSize = tangentSize;
_wrapperHandle = wrapperHandle;
}
/// <summary>
/// Gets the ambient size.
/// </summary>
public new int AmbientSize => _ambientSize;
/// <summary>
/// Gets the tangent size.
/// </summary>
public new int TangentSize => _tangentSize;
public new void Dispose()
{
if (!_disposed)
{
// Call base Dispose first (frees native handle)
base.Dispose();
// Now safe to free wrapper handle after native handle is freed
try
{
if (_wrapperHandle.IsAllocated)
{
var wrapper = (CallbackWrapper)_wrapperHandle.Target!;
// Free native callback handles
if (wrapper.NativeCallbackHandles != null)
{
foreach (var handle in wrapper.NativeCallbackHandles)
{
if (handle.IsAllocated)
handle.Free();
}
}
_wrapperHandle.Free();
}
}
catch
{
// Ignore errors - handles may already be freed
}
_disposed = true;
}
}
private static ManifoldHandle CreateHandle(
int ambientSize,
int tangentSize,
PlusOperation plus,
MinusOperation minus,
out GCHandle wrapperHandle)
{
if (ambientSize <= 0)
throw new ArgumentException("Ambient size must be positive", nameof(ambientSize));
if (tangentSize <= 0)
throw new ArgumentException("Tangent size must be positive", nameof(tangentSize));
if (tangentSize > ambientSize)
throw new ArgumentException("Tangent size must be less than or equal to ambient size", nameof(tangentSize));
if (plus == null)
throw new ArgumentNullException(nameof(plus));
if (minus == null)
throw new ArgumentNullException(nameof(minus));
// Create wrapper struct to store callbacks and sizes
var wrapper = new CallbackWrapper
{
Plus = plus,
Minus = minus,
AmbientSize = ambientSize,
TangentSize = tangentSize
};
// Pin the wrapper
wrapperHandle = GCHandle.Alloc(wrapper);
// Create native Plus callback
var plusCallback = new CeresNative.CeresAutoDiffManifoldPlus((userData, xPtr, deltaPtr, xPlusDeltaPtr) =>
{
try
{
var handle = GCHandle.FromIntPtr(userData);
var wrapperObj = (CallbackWrapper)handle.Target!;
// Marshal arrays
var x = new double[wrapperObj.AmbientSize];
var delta = new double[wrapperObj.TangentSize];
var xPlusDelta = new double[wrapperObj.AmbientSize];
Marshal.Copy(xPtr, x, 0, wrapperObj.AmbientSize);
Marshal.Copy(deltaPtr, delta, 0, wrapperObj.TangentSize);
// Call C# delegate
var success = wrapperObj.Plus(x, delta, xPlusDelta);
// Marshal result back
if (success)
{
Marshal.Copy(xPlusDelta, 0, xPlusDeltaPtr, wrapperObj.AmbientSize);
return 1;
}
return 0;
}
catch (Exception)
{
// Return failure on any exception
return 0;
}
});
// Create native Minus callback
var minusCallback = new CeresNative.CeresAutoDiffManifoldMinus((userData, yPtr, xPtr, yMinusXPtr) =>
{
try
{
var handle = GCHandle.FromIntPtr(userData);
var wrapperObj = (CallbackWrapper)handle.Target!;
// Marshal arrays
var y = new double[wrapperObj.AmbientSize];
var x = new double[wrapperObj.AmbientSize];
var yMinusX = new double[wrapperObj.TangentSize];
Marshal.Copy(yPtr, y, 0, wrapperObj.AmbientSize);
Marshal.Copy(xPtr, x, 0, wrapperObj.AmbientSize);
// Call C# delegate
var success = wrapperObj.Minus(y, x, yMinusX);
// Marshal result back
if (success)
{
Marshal.Copy(yMinusX, 0, yMinusXPtr, wrapperObj.TangentSize);
return 1;
}
return 0;
}
catch (Exception)
{
// Return failure on any exception
return 0;
}
});
// Pin the native callbacks
var plusHandle = GCHandle.Alloc(plusCallback);
var minusHandle = GCHandle.Alloc(minusCallback);
wrapper.NativeCallbackHandles = new[] { plusHandle, minusHandle };
// Call native function
var handle = CeresNative.ceres_wrapper_create_autodiff_manifold(
ambientSize,
tangentSize,
plusCallback,
minusCallback,
GCHandle.ToIntPtr(wrapperHandle)); // Pass wrapper handle as user_data
if (handle == IntPtr.Zero)
{
// Cleanup on failure
plusHandle.Free();
minusHandle.Free();
wrapperHandle.Free();
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.OutOfMemory, "Failed to create AutoDiff manifold");
}
return ManifoldHandle.Create(handle);
}
/// <summary>
/// Wrapper struct to store callbacks and sizes.
/// </summary>
private class CallbackWrapper
{
public PlusOperation Plus = null!;
public MinusOperation Minus = null!;
public int AmbientSize;
public int TangentSize;
public GCHandle[]? NativeCallbackHandles;
}
}

View File

@@ -0,0 +1,90 @@
using System;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
namespace CeresSharp;
/// <summary>
/// BiCubic interpolator for 2D grid data.
/// </summary>
public sealed class BiCubicInterpolator : IDisposable
{
private readonly InterpolatorHandle _handle;
private bool _disposed;
/// <summary>
/// Creates a new BiCubic interpolator from 2D grid data.
/// </summary>
/// <param name="data">The data array in row-major order (size = rows * cols).</param>
/// <param name="rows">The number of rows.</param>
/// <param name="cols">The number of columns.</param>
public BiCubicInterpolator(double[] data, int rows, int cols)
{
if (data == null)
throw new ArgumentNullException(nameof(data));
if (rows <= 0)
throw new ArgumentException("Rows must be positive", nameof(rows));
if (cols <= 0)
throw new ArgumentException("Cols must be positive", nameof(cols));
if (data.Length < rows * cols)
throw new ArgumentException("Data array length must be at least rows * cols", nameof(data));
unsafe
{
fixed (double* ptr = data)
{
var handle = CeresNative.ceres_wrapper_create_bicubic_interpolator(
(IntPtr)ptr,
rows,
cols);
_handle = InterpolatorHandle.CreateBicubic(handle);
}
}
}
/// <summary>
/// Evaluates the interpolator at point (x, y).
/// </summary>
/// <param name="x">X coordinate in grid space (0 <= x < cols).</param>
/// <param name="y">Y coordinate in grid space (0 <= y < rows).</param>
/// <param name="value">Output value at (x, y).</param>
/// <param name="gradientX">Output gradient in X direction (can be null).</param>
/// <param name="gradientY">Output gradient in Y direction (can be null).</param>
public void Evaluate(double x, double y, out double value, out double? gradientX, out double? gradientY)
{
unsafe
{
double val = 0, gradX = 0, gradY = 0;
CeresNative.ceres_wrapper_bicubic_interpolator_evaluate(
_handle.DangerousGetHandle(),
x,
y,
(IntPtr)(&val),
(IntPtr)(&gradX),
(IntPtr)(&gradY));
value = val;
gradientX = gradX;
gradientY = gradY;
}
}
/// <summary>
/// Evaluates the interpolator at point (x, y) without gradients.
/// </summary>
public double Evaluate(double x, double y)
{
Evaluate(x, y, out var value, out _, out _);
return value;
}
public void Dispose()
{
if (!_disposed)
{
_handle?.Dispose();
_disposed = true;
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Runtime.InteropServices;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
namespace CeresSharp;
/// <summary>
/// Callback delegates and helpers for Ceres Solver.
/// </summary>
public static class Callbacks
{
/// <summary>
/// Delegate for iteration callback.
/// Called at the end of each iteration.
/// </summary>
/// <param name="summary">The current solver summary.</param>
/// <returns>True to continue, false to stop optimization.</returns>
public delegate bool IterationCallback(SolverSummary summary);
}
/// <summary>
/// Extension methods for setting callbacks.
/// </summary>
public static class SolverOptionsExtensions
{
/// <summary>
/// Sets an iteration callback for the solver options.
/// </summary>
/// <param name="options">The solver options.</param>
/// <param name="callback">The iteration callback.</param>
public static void SetIterationCallback(this SolverOptions options, Callbacks.IterationCallback callback)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
if (callback == null)
throw new ArgumentNullException(nameof(callback));
// Pin the callback delegate
var callbackHandle = GCHandle.Alloc(callback);
// Create native callback wrapper
var nativeCallback = new CeresNative.CeresIterationCallback((userData, summaryPtr) =>
{
try
{
var handle = GCHandle.FromIntPtr(userData);
var csCallback = (Callbacks.IterationCallback)handle.Target!;
// Create a temporary SolverSummary wrapper
// Note: We need to be careful here - the summary is owned by the solver
// We'll create a temporary handle that doesn't own the summary
var summaryHandle = new SolverSummaryHandle(summaryPtr, false);
var summary = new SolverSummary(summaryHandle);
// Call C# callback
var shouldContinue = csCallback(summary);
// Don't dispose the summary - it's owned by the solver
return shouldContinue ? 1 : 0;
}
catch
{
return 0; // Stop on error
}
});
// Pin the native callback
var nativeCallbackHandle = GCHandle.Alloc(nativeCallback);
CeresNative.ceres_wrapper_solver_options_set_iteration_callback(
options.Handle.DangerousGetHandle(),
nativeCallback,
GCHandle.ToIntPtr(callbackHandle));
// Track handles for cleanup when SolverOptions is disposed
options.SetIterationCallbackHandles(callbackHandle, nativeCallbackHandle);
}
}

View File

@@ -0,0 +1,155 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using CeresSharp.Native;
using CeresSharp.Exceptions;
namespace CeresSharp;
/// <summary>
/// Provides information about the Ceres Solver library, including version and build configuration.
/// </summary>
public static class CeresLibraryInfo
{
private const int BufferSize = 4096;
/// <summary>
/// Gets the Ceres version string (e.g., "2.2.0").
/// </summary>
public static string GetVersionString()
{
var versionPtr = CeresNative.ceres_wrapper_get_version_string();
if (versionPtr == IntPtr.Zero)
{
return "Unknown";
}
return Marshal.PtrToStringAnsi(versionPtr) ?? "Unknown";
}
/// <summary>
/// Gets the Ceres major version number.
/// </summary>
public static int GetVersionMajor()
{
return CeresNative.ceres_wrapper_get_version_major();
}
/// <summary>
/// Gets the Ceres minor version number.
/// </summary>
public static int GetVersionMinor()
{
return CeresNative.ceres_wrapper_get_version_minor();
}
/// <summary>
/// Gets the Ceres revision version number.
/// </summary>
public static int GetVersionRevision()
{
return CeresNative.ceres_wrapper_get_version_revision();
}
/// <summary>
/// Gets the detailed Ceres version string with build configuration.
/// </summary>
/// <returns>The detailed version string, or null if an error occurred.</returns>
public static string? GetDetailedVersionString()
{
var buffer = new StringBuilder(BufferSize);
var errorCode = CeresNative.ceres_wrapper_get_detailed_version_string(buffer, BufferSize);
if (errorCode != CeresErrorCode.Success)
{
return null;
}
return buffer.ToString();
}
/// <summary>
/// Gets comprehensive library information including version, build configuration, and features.
/// </summary>
/// <returns>The library information string, or null if an error occurred.</returns>
public static string? GetLibraryInfo()
{
var buffer = new StringBuilder(BufferSize);
var errorCode = CeresNative.ceres_wrapper_get_library_info(buffer, BufferSize);
if (errorCode != CeresErrorCode.Success)
{
return null;
}
return buffer.ToString();
}
/// <summary>
/// Prints the Ceres library information to the console.
/// </summary>
public static void PrintLibraryInfo()
{
// Basic version info
// Detailed version
var detailedVersion = GetDetailedVersionString();
if (detailedVersion != null)
{
}
// Full library info
var libraryInfo = GetLibraryInfo();
if (libraryInfo != null)
{
}
else
{
}
}
/// <summary>
/// Gets a formatted string containing all Ceres library information.
/// </summary>
/// <returns>A formatted string with all library information.</returns>
public static string GetFormattedInfo()
{
var sb = new StringBuilder();
sb.AppendLine("========================================");
sb.AppendLine("Ceres Solver Library Information");
sb.AppendLine("========================================");
sb.AppendLine();
// Basic version info
sb.AppendLine($"Version: {GetVersionString()}");
sb.AppendLine($" Major: {GetVersionMajor()}");
sb.AppendLine($" Minor: {GetVersionMinor()}");
sb.AppendLine($" Revision: {GetVersionRevision()}");
sb.AppendLine();
// Detailed version
var detailedVersion = GetDetailedVersionString();
if (detailedVersion != null)
{
sb.AppendLine($"Detailed Version: {detailedVersion}");
sb.AppendLine();
}
// Full library info
var libraryInfo = GetLibraryInfo();
if (libraryInfo != null)
{
sb.AppendLine(libraryInfo);
}
else
{
sb.AppendLine("(Unable to retrieve detailed library information)");
}
sb.AppendLine("========================================");
return sb.ToString();
}
}

View File

@@ -0,0 +1,219 @@
namespace CeresSharp;
/// <summary>
/// Constants for Ceres Solver default values and mathematical constants.
/// </summary>
public static class Constants
{
/// <summary>
/// Mathematical constant Pi.
/// </summary>
public const double Pi = 3.141592653589793238462643383279502884;
// ============================================================================
// SolverOptions Default Values
// ============================================================================
/// <summary>
/// Default maximum number of iterations.
/// </summary>
public const int DefaultMaxNumIterations = 50;
/// <summary>
/// Default number of threads.
/// </summary>
public const int DefaultNumThreads = 1;
/// <summary>
/// Default function tolerance.
/// </summary>
public const double DefaultFunctionTolerance = 1e-6;
/// <summary>
/// Default gradient tolerance.
/// </summary>
public const double DefaultGradientTolerance = 1e-10;
/// <summary>
/// Default parameter tolerance.
/// </summary>
public const double DefaultParameterTolerance = 1e-8;
/// <summary>
/// Default initial trust region radius.
/// </summary>
public const double DefaultInitialTrustRegionRadius = 1e4;
/// <summary>
/// Default maximum trust region radius.
/// </summary>
public const double DefaultMaxTrustRegionRadius = 1e16;
/// <summary>
/// Default minimum trust region radius.
/// </summary>
public const double DefaultMinTrustRegionRadius = 1e-32;
/// <summary>
/// Default minimum relative decrease.
/// </summary>
public const double DefaultMinRelativeDecrease = 1e-3;
/// <summary>
/// Default maximum solver time in seconds (effectively unlimited).
/// </summary>
public const double DefaultMaxSolverTimeInSeconds = 1e9;
/// <summary>
/// Default maximum L-BFGS rank.
/// </summary>
public const int DefaultMaxLbfgsRank = 20;
/// <summary>
/// Default maximum consecutive non-monotonic steps.
/// </summary>
public const int DefaultMaxConsecutiveNonmonotonicSteps = 5;
/// <summary>
/// Default maximum number of consecutive invalid steps.
/// </summary>
public const int DefaultMaxNumConsecutiveInvalidSteps = 5;
// ============================================================================
// Line Search Default Values
// ============================================================================
/// <summary>
/// Default minimum line search step size.
/// </summary>
public const double DefaultMinLineSearchStepSize = 1e-9;
/// <summary>
/// Default maximum line search step contraction.
/// </summary>
public const double DefaultMaxLineSearchStepContraction = 1e-3;
/// <summary>
/// Default minimum line search step contraction.
/// </summary>
public const double DefaultMinLineSearchStepContraction = 0.6;
/// <summary>
/// Default maximum number of line search step size iterations.
/// </summary>
public const int DefaultMaxNumLineSearchStepSizeIterations = 20;
/// <summary>
/// Default maximum number of line search direction restarts.
/// </summary>
public const int DefaultMaxNumLineSearchDirectionRestarts = 5;
/// <summary>
/// Default line search sufficient function decrease.
/// </summary>
public const double DefaultLineSearchSufficientFunctionDecrease = 1e-4;
/// <summary>
/// Default line search sufficient curvature decrease.
/// </summary>
public const double DefaultLineSearchSufficientCurvatureDecrease = 0.9;
/// <summary>
/// Default maximum line search step expansion.
/// </summary>
public const double DefaultMaxLineSearchStepExpansion = 10.0;
// ============================================================================
// Trust Region Default Values
// ============================================================================
/// <summary>
/// Default minimum LM diagonal.
/// </summary>
public const double DefaultMinLmDiagonal = 1e-6;
/// <summary>
/// Default maximum LM diagonal.
/// </summary>
public const double DefaultMaxLmDiagonal = 1e32;
// ============================================================================
// Linear Solver Default Values
// ============================================================================
/// <summary>
/// Default maximum linear solver iterations.
/// </summary>
public const int DefaultMaxLinearSolverIterations = 500;
/// <summary>
/// Default minimum linear solver iterations.
/// </summary>
public const int DefaultMinLinearSolverIterations = 0;
/// <summary>
/// Default linear solver tolerance.
/// </summary>
public const double DefaultLinearSolverTolerance = 1e-20;
// ============================================================================
// Inner Iterations Default Values
// ============================================================================
/// <summary>
/// Default inner iteration tolerance.
/// </summary>
public const double DefaultInnerIterationTolerance = 1e-3;
// ============================================================================
// Covariance Default Values
// ============================================================================
/// <summary>
/// Default minimum reciprocal condition number for covariance.
/// </summary>
public const double DefaultMinReciprocalConditionNumber = 1e-14;
// ============================================================================
// Gradient Checker Default Values
// ============================================================================
/// <summary>
/// Default gradient check relative precision.
/// </summary>
public const double DefaultGradientCheckRelativePrecision = 1e-4;
/// <summary>
/// Default numeric derivative relative step size.
/// </summary>
public const double DefaultNumericDerivativeRelativeStepSize = 1e-6;
// ============================================================================
// Numeric Diff Default Values
// ============================================================================
/// <summary>
/// Default relative step size for numeric differentiation.
/// </summary>
public const double DefaultNumericDiffRelativeStepSize = 1e-6;
/// <summary>
/// Default Ridders relative initial step size.
/// </summary>
public const double DefaultRiddersRelativeInitialStepSize = 1e-2;
/// <summary>
/// Default maximum number of Ridders extrapolations.
/// </summary>
public const int DefaultMaxNumRiddersExtrapolations = 10;
/// <summary>
/// Default Ridders epsilon.
/// </summary>
public const double DefaultRiddersEpsilon = 1e-12;
/// <summary>
/// Default Ridders step shrink factor.
/// </summary>
public const double DefaultRiddersStepShrinkFactor = 2.0;
}

View File

@@ -0,0 +1,607 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using CeresSharp.Enums;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
namespace CeresSharp;
/// <summary>
/// Base class for cost functions in Ceres Solver.
/// </summary>
public abstract class CostFunction : IDisposable
{
private readonly CostFunctionHandle _handle;
private GCHandle _callbackHandle;
private GCHandle? _nativeCallbackHandle;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the CostFunction class.
/// </summary>
internal CostFunction(CostFunctionHandle handle)
{
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
}
/// <summary>
/// Gets the native handle.
/// </summary>
internal IntPtr Handle => _handle.DangerousGetHandle();
/// <summary>
/// Sets the callback handle for pinning the delegate.
/// </summary>
protected void SetCallbackHandle(GCHandle handle)
{
if (_callbackHandle.IsAllocated)
_callbackHandle.Free();
_callbackHandle = handle;
}
/// <summary>
/// Sets the native callback handle for cleanup.
/// </summary>
protected void SetNativeCallbackHandle(GCHandle handle)
{
if (_nativeCallbackHandle.HasValue && _nativeCallbackHandle.Value.IsAllocated)
_nativeCallbackHandle.Value.Free();
_nativeCallbackHandle = handle;
}
public void Dispose()
{
Dispose(true);
// Suppress finalizer to prevent crash in finalizer thread
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// IMPORTANT: Free native handle FIRST before unpinning GCHandles
// This ensures native code has finished using the callbacks
// Dispose SafeHandle and suppress finalization to prevent double-free
if (_handle != null)
{
try
{
_handle.Dispose();
// Suppress finalization to prevent crash in finalizer thread
GC.SuppressFinalize(_handle);
}
catch
{
// Ignore errors during disposal
}
}
// Now safe to unpin callbacks after native handle is freed
// CRITICAL: Only free GCHandles in Dispose(), never in finalizer
// Native code may still be using callbacks during finalization
try
{
if (_callbackHandle.IsAllocated)
_callbackHandle.Free();
}
catch
{
// Ignore errors - handle may already be freed
}
try
{
if (_nativeCallbackHandle.HasValue && _nativeCallbackHandle.Value.IsAllocated)
_nativeCallbackHandle.Value.Free();
}
catch
{
// Ignore errors - handle may already be freed
}
}
else
{
// Finalizer path - DO NOT free GCHandles here!
// Native code may still be using them. Only free native handle.
// SafeHandle finalizer will handle native handle cleanup.
// GCHandles will be freed when objects are GC'd (after native code is done).
}
_disposed = true;
}
}
/// <summary>
/// Gets whether this cost function has been added to a Problem and is now owned by it.
/// If true, the cost function will be freed when the Problem is disposed.
/// If false, the caller is responsible for disposing.
/// </summary>
public bool IsOwnedByProblem { get; private set; }
/// <summary>
/// Marks this cost function as owned by a Problem, preventing standalone cleanup.
/// </summary>
internal void MarkOwnedByProblem()
{
IsOwnedByProblem = true;
_handle.MarkOwnedByProblem();
}
/// <summary>
/// Protected virtual method to free additional GCHandles in derived classes.
/// This is called by FreeGCHandlesAfterProblemDisposed() to allow derived classes
/// to free their own GCHandles (e.g., _wrapperHandle in AutoDiffCostFunction).
/// </summary>
protected virtual void FreeAdditionalGCHandles()
{
// Base class has no additional handles
// Derived classes override this to free their _wrapperHandle
}
/// <summary>
/// Internal method to free GCHandles after native Problem is disposed.
/// This is called by Problem.Dispose() after native handle is freed to prevent memory leaks.
///
/// CRITICAL: After Problem is disposed, native CostFunction objects are already deleted by Ceres.
/// However, the C# wrapper objects still hold GCHandles (for callbacks and wrappers) that MUST be freed
/// to prevent memory leaks. Since CostFunctionHandle.ReleaseNativeHandle() is a no-op (to prevent double-free),
/// it's safe to call Dispose() logic which will free GCHandles but not try to delete native handle.
/// </summary>
internal void FreeGCHandlesAfterProblemDisposed()
{
if (!_disposed)
{
// Free base class GCHandles (_callbackHandle, _nativeCallbackHandle)
// Native handle is already freed by Ceres, so CostFunctionHandle.Dispose() will be a no-op
try
{
if (_callbackHandle.IsAllocated)
_callbackHandle.Free();
}
catch
{
// Ignore errors - handle may already be freed
}
try
{
if (_nativeCallbackHandle.HasValue && _nativeCallbackHandle.Value.IsAllocated)
_nativeCallbackHandle.Value.Free();
}
catch
{
// Ignore errors - handle may already be freed
}
// Free additional GCHandles in derived classes (e.g., _wrapperHandle)
try
{
FreeAdditionalGCHandles();
}
catch
{
// Ignore errors during cleanup
}
// Also dispose the handle (it's a no-op but ensures proper state)
// CostFunctionHandle.ReleaseNativeHandle() is intentionally a no-op to prevent double-free
try
{
_handle?.Dispose();
}
catch
{
// Ignore - handle may already be disposed
}
// Mark as disposed to prevent double-free
_disposed = true;
}
}
// Note: No finalizer for CostFunction
// Problem owns cost functions, so they should be disposed when Problem is disposed
// If not disposed, SafeHandle finalizer will cleanup native handle
// But we don't want to cleanup GCHandles in finalizer as native code may still use them
}
/// <summary>
/// AutoDiff cost function with fixed parameter block sizes.
/// </summary>
public sealed class AutoDiffCostFunction : CostFunction
{
private readonly GCHandle _wrapperHandle;
private readonly int[] _parameterBlockSizes;
/// <summary>
/// Creates a new AutoDiff cost function.
/// </summary>
/// <param name="callback">The callback function to evaluate the cost.</param>
/// <param name="numResiduals">The number of residuals.</param>
/// <param name="parameterBlockSizes">Array of sizes for each parameter block.</param>
public AutoDiffCostFunction(
AutoDiffCostFunctionCallback callback,
int numResiduals,
int[] parameterBlockSizes) : base(CreateHandle(callback, numResiduals, parameterBlockSizes, out var wrapperHandle, out var nativeCallbackHandle))
{
_wrapperHandle = wrapperHandle;
_parameterBlockSizes = parameterBlockSizes;
SetNativeCallbackHandle(nativeCallbackHandle);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
try { if (_wrapperHandle.IsAllocated) _wrapperHandle.Free(); } catch { }
}
base.Dispose(disposing);
}
/// <summary>
/// Override to free _wrapperHandle when Problem is disposed.
/// </summary>
protected override void FreeAdditionalGCHandles()
{
try
{
if (_wrapperHandle.IsAllocated)
_wrapperHandle.Free();
}
catch
{
// Ignore errors - handle may already be freed
}
}
private static CostFunctionHandle CreateHandle(
AutoDiffCostFunctionCallback callback,
int numResiduals,
int[] parameterBlockSizes,
out GCHandle wrapperHandle,
out GCHandle nativeCallbackHandle)
{
CostFunctionHelpers.ValidateCostFunctionParameters(callback, numResiduals, parameterBlockSizes);
// Create wrapper struct to store callback and parameter info
var wrapper = new CostFunctionHelpers.AutoDiffCallbackWrapper
{
Callback = callback,
NumResiduals = numResiduals,
ParameterBlockSizes = parameterBlockSizes
};
// Pin the wrapper
wrapperHandle = GCHandle.Alloc(wrapper);
// Create native callback using helper
var nativeCallback = CostFunctionHelpers.CreateAutoDiffCallback(wrapper);
// Pin the native callback
nativeCallbackHandle = GCHandle.Alloc(nativeCallback);
try
{
var handle = CostFunctionHelpers.CreateAutoDiffHandle(
nativeCallback,
GCHandle.ToIntPtr(wrapperHandle),
numResiduals,
parameterBlockSizes,
isDynamic: false);
return handle;
}
catch
{
// Cleanup on failure
wrapperHandle.Free();
nativeCallbackHandle.Free();
throw;
}
}
}
/// <summary>
/// Dynamic AutoDiff cost function with runtime-determined parameter block sizes.
/// </summary>
public sealed class DynamicAutoDiffCostFunction : CostFunction
{
private readonly GCHandle _wrapperHandle;
/// <summary>
/// Creates a new Dynamic AutoDiff cost function.
/// </summary>
/// <param name="callback">The callback function to evaluate the cost.</param>
/// <param name="numResiduals">The number of residuals (determined at runtime).</param>
/// <param name="parameterBlockSizes">Array of sizes for each parameter block.</param>
public DynamicAutoDiffCostFunction(
AutoDiffCostFunctionCallback callback,
int numResiduals,
int[] parameterBlockSizes) : base(CreateHandle(callback, numResiduals, parameterBlockSizes, out var wrapperHandle, out var nativeCallbackHandle))
{
_wrapperHandle = wrapperHandle;
SetNativeCallbackHandle(nativeCallbackHandle);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
try { if (_wrapperHandle.IsAllocated) _wrapperHandle.Free(); } catch { }
}
base.Dispose(disposing);
}
/// <summary>
/// Override to free _wrapperHandle when Problem is disposed.
/// </summary>
protected override void FreeAdditionalGCHandles()
{
try
{
if (_wrapperHandle.IsAllocated)
_wrapperHandle.Free();
}
catch
{
// Ignore errors - handle may already be freed
}
}
private static CostFunctionHandle CreateHandle(
AutoDiffCostFunctionCallback callback,
int numResiduals,
int[] parameterBlockSizes,
out GCHandle wrapperHandle,
out GCHandle nativeCallbackHandle)
{
CostFunctionHelpers.ValidateCostFunctionParameters(callback, numResiduals, parameterBlockSizes);
// Create wrapper struct to store callback and parameter info
var wrapper = new CostFunctionHelpers.AutoDiffCallbackWrapper
{
Callback = callback,
NumResiduals = numResiduals,
ParameterBlockSizes = parameterBlockSizes
};
// Pin the wrapper
wrapperHandle = GCHandle.Alloc(wrapper);
// Create native callback using helper
var nativeCallback = CostFunctionHelpers.CreateAutoDiffCallback(wrapper);
// Pin the native callback
nativeCallbackHandle = GCHandle.Alloc(nativeCallback);
try
{
var handle = CostFunctionHelpers.CreateAutoDiffHandle(
nativeCallback,
GCHandle.ToIntPtr(wrapperHandle),
numResiduals,
parameterBlockSizes,
isDynamic: true);
return handle;
}
catch
{
// Cleanup on failure
wrapperHandle.Free();
nativeCallbackHandle.Free();
throw;
}
}
}
/// <summary>
/// NumericDiff cost function with fixed parameter block sizes.
/// </summary>
public sealed class NumericDiffCostFunction : CostFunction
{
private readonly GCHandle _wrapperHandle;
/// <summary>
/// Creates a new NumericDiff cost function.
/// </summary>
/// <param name="callback">The callback function to evaluate the cost.</param>
/// <param name="method">The numeric differentiation method.</param>
/// <param name="numResiduals">The number of residuals.</param>
/// <param name="parameterBlockSizes">Array of sizes for each parameter block.</param>
public NumericDiffCostFunction(
NumericDiffCostFunctionCallback callback,
NumericDiffMethod method,
int numResiduals,
int[] parameterBlockSizes) : base(CreateHandle(callback, method, numResiduals, parameterBlockSizes, out var wrapperHandle, out var nativeCallbackHandle))
{
_wrapperHandle = wrapperHandle;
SetNativeCallbackHandle(nativeCallbackHandle);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
try { if (_wrapperHandle.IsAllocated) _wrapperHandle.Free(); } catch { }
}
base.Dispose(disposing);
}
/// <summary>
/// Override to free _wrapperHandle when Problem is disposed.
/// </summary>
protected override void FreeAdditionalGCHandles()
{
try
{
if (_wrapperHandle.IsAllocated)
_wrapperHandle.Free();
}
catch
{
// Ignore errors - handle may already be freed
}
}
private static CostFunctionHandle CreateHandle(
NumericDiffCostFunctionCallback callback,
NumericDiffMethod method,
int numResiduals,
int[] parameterBlockSizes,
out GCHandle wrapperHandle,
out GCHandle nativeCallbackHandle)
{
CostFunctionHelpers.ValidateCostFunctionParameters(callback, numResiduals, parameterBlockSizes);
var wrapper = new CostFunctionHelpers.NumericCallbackWrapper
{
Callback = callback,
NumResiduals = numResiduals,
ParameterBlockSizes = parameterBlockSizes
};
wrapperHandle = GCHandle.Alloc(wrapper);
// Create native callback using helper
var nativeCallback = CostFunctionHelpers.CreateNumericDiffCallback(wrapper);
nativeCallbackHandle = GCHandle.Alloc(nativeCallback);
try
{
var handle = CostFunctionHelpers.CreateNumericDiffHandle(
nativeCallback,
GCHandle.ToIntPtr(wrapperHandle),
numResiduals,
parameterBlockSizes,
method,
isDynamic: false);
return handle;
}
catch
{
// Cleanup on failure
wrapperHandle.Free();
nativeCallbackHandle.Free();
throw;
}
}
}
/// <summary>
/// Dynamic NumericDiff cost function with runtime-determined parameter block sizes.
/// </summary>
public sealed class DynamicNumericDiffCostFunction : CostFunction
{
private readonly GCHandle _wrapperHandle;
/// <summary>
/// Creates a new Dynamic NumericDiff cost function.
/// </summary>
/// <param name="callback">The callback function to evaluate the cost.</param>
/// <param name="method">The numeric differentiation method.</param>
/// <param name="numResiduals">The number of residuals (determined at runtime).</param>
/// <param name="parameterBlockSizes">Array of sizes for each parameter block.</param>
public DynamicNumericDiffCostFunction(
NumericDiffCostFunctionCallback callback,
NumericDiffMethod method,
int numResiduals,
int[] parameterBlockSizes) : base(CreateHandle(callback, method, numResiduals, parameterBlockSizes, out var wrapperHandle, out var nativeCallbackHandle))
{
_wrapperHandle = wrapperHandle;
SetNativeCallbackHandle(nativeCallbackHandle);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
try { if (_wrapperHandle.IsAllocated) _wrapperHandle.Free(); } catch { }
}
base.Dispose(disposing);
}
/// <summary>
/// Override to free _wrapperHandle when Problem is disposed.
/// </summary>
protected override void FreeAdditionalGCHandles()
{
try
{
if (_wrapperHandle.IsAllocated)
_wrapperHandle.Free();
}
catch
{
// Ignore errors - handle may already be freed
}
}
private static CostFunctionHandle CreateHandle(
NumericDiffCostFunctionCallback callback,
NumericDiffMethod method,
int numResiduals,
int[] parameterBlockSizes,
out GCHandle wrapperHandle,
out GCHandle nativeCallbackHandle)
{
CostFunctionHelpers.ValidateCostFunctionParameters(callback, numResiduals, parameterBlockSizes);
var wrapper = new CostFunctionHelpers.NumericCallbackWrapper
{
Callback = callback,
NumResiduals = numResiduals,
ParameterBlockSizes = parameterBlockSizes
};
wrapperHandle = GCHandle.Alloc(wrapper);
// Create native callback using helper
var nativeCallback = CostFunctionHelpers.CreateNumericDiffCallback(wrapper);
nativeCallbackHandle = GCHandle.Alloc(nativeCallback);
try
{
var handle = CostFunctionHelpers.CreateNumericDiffHandle(
nativeCallback,
GCHandle.ToIntPtr(wrapperHandle),
numResiduals,
parameterBlockSizes,
method,
isDynamic: true);
return handle;
}
catch
{
// Cleanup on failure
wrapperHandle.Free();
nativeCallbackHandle.Free();
throw;
}
}
}
/// <summary>
/// Delegate for AutoDiff cost function evaluation.
/// </summary>
/// <param name="parameters">Array of parameter blocks (each block is contiguous).</param>
/// <param name="residuals">Output array for residuals.</param>
/// <returns>True on success, false on failure.</returns>
public delegate bool AutoDiffCostFunctionCallback(double[][] parameters, double[] residuals);
/// <summary>
/// Delegate for NumericDiff cost function evaluation.
/// </summary>
/// <param name="parameters">Array of parameter blocks (each block is contiguous).</param>
/// <param name="residuals">Output array for residuals.</param>
/// <returns>True on success, false on failure.</returns>
public delegate bool NumericDiffCostFunctionCallback(double[][] parameters, double[] residuals);

View File

@@ -0,0 +1,246 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using CeresSharp.Enums;
using CeresSharp.Exceptions;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
namespace CeresSharp;
/// <summary>
/// Helper class for creating cost functions to reduce code duplication.
/// </summary>
internal static class CostFunctionHelpers
{
/// <summary>
/// Validates cost function parameters.
/// </summary>
internal static void ValidateCostFunctionParameters(
object? callback,
int numResiduals,
int[]? parameterBlockSizes,
string callbackParamName = "callback",
string numResidualsParamName = "numResiduals",
string parameterBlockSizesParamName = "parameterBlockSizes")
{
if (callback == null)
throw new ArgumentNullException(callbackParamName);
if (numResiduals <= 0)
throw new ArgumentException("Number of residuals must be positive", numResidualsParamName);
if (parameterBlockSizes == null || parameterBlockSizes.Length == 0)
throw new ArgumentException("Parameter block sizes cannot be null or empty", parameterBlockSizesParamName);
if (parameterBlockSizes.Any(size => size <= 0))
throw new ArgumentException("All parameter block sizes must be positive", parameterBlockSizesParamName);
}
/// <summary>
/// Creates a native AutoDiff cost function callback wrapper.
/// </summary>
internal static CeresNative.CeresAutoDiffCostFunctionCallback CreateAutoDiffCallback(
AutoDiffCallbackWrapper wrapper)
{
return (userData, parametersPtr, residualsPtr) =>
{
try
{
var handle = GCHandle.FromIntPtr(userData);
var wrapperObj = (AutoDiffCallbackWrapper)handle.Target!;
// Copy parameter blocks from native memory
var parameterBlocks = Native.UnsafeHelpers.CopyParameterBlocks(parametersPtr, wrapperObj.ParameterBlockSizes);
// Allocate residuals array
var residuals = new double[wrapperObj.NumResiduals];
if (residuals.Length <= 0)
return 0;
// Call C# callback
var success = wrapperObj.Callback(parameterBlocks, residuals);
// CRITICAL: Check for NaN/Inf in residuals before copying to native memory
// This prevents Ceres from getting invalid residuals that cause InitialCost=-1
if (success && residuals.Length == wrapperObj.NumResiduals)
{
bool hasInvalid = false;
int nanCount = 0;
int infCount = 0;
double sumSquared = 0.0;
for (int i = 0; i < residuals.Length; ++i)
{
if (double.IsNaN(residuals[i]))
{
hasInvalid = true;
nanCount++;
if (nanCount <= 5)
{
}
}
else if (double.IsInfinity(residuals[i]))
{
hasInvalid = true;
infCount++;
if (infCount <= 5)
{
}
}
else
{
sumSquared += residuals[i] * residuals[i];
}
}
if (hasInvalid)
{
return 0; // Return failure
}
// Copy residuals back
Marshal.Copy(residuals, 0, residualsPtr, wrapperObj.NumResiduals);
return 1;
}
return 0;
}
catch (Exception)
{
// Return failure on any exception
return 0;
}
};
}
/// <summary>
/// Creates a native NumericDiff cost function callback wrapper.
/// </summary>
internal static CeresNative.CeresNumericDiffCostFunctionCallback CreateNumericDiffCallback(
NumericCallbackWrapper wrapper)
{
return (parametersPtr, residualsPtr, userData) =>
{
try
{
var handle = GCHandle.FromIntPtr(userData);
var wrapperObj = (NumericCallbackWrapper)handle.Target!;
// Copy parameter blocks from native memory
var parameterBlocks = Native.UnsafeHelpers.CopyParameterBlocks(parametersPtr, wrapperObj.ParameterBlockSizes);
// Allocate residuals array
var residuals = new double[wrapperObj.NumResiduals];
if (residuals.Length <= 0)
return 0;
// Call C# callback
var success = wrapperObj.Callback(parameterBlocks, residuals);
// Copy residuals back
if (success && residuals.Length == wrapperObj.NumResiduals)
{
Marshal.Copy(residuals, 0, residualsPtr, wrapperObj.NumResiduals);
return 1;
}
return 0;
}
catch (Exception)
{
return 0;
}
};
}
/// <summary>
/// Creates an AutoDiff cost function handle (fixed or dynamic).
/// </summary>
internal static CostFunctionHandle CreateAutoDiffHandle(
CeresNative.CeresAutoDiffCostFunctionCallback nativeCallback,
IntPtr userData,
int numResiduals,
int[] parameterBlockSizes,
bool isDynamic)
{
return Native.UnsafeHelpers.WithPinnedArray(parameterBlockSizes, (ptr) =>
{
var handle = isDynamic
? CeresNative.ceres_wrapper_create_dynamic_autodiff_cost_function(
nativeCallback,
userData,
numResiduals,
parameterBlockSizes.Length,
ptr)
: CeresNative.ceres_wrapper_create_autodiff_cost_function(
nativeCallback,
userData,
numResiduals,
parameterBlockSizes.Length,
ptr);
if (handle == IntPtr.Zero)
{
throw new CeresException(CeresErrorCode.OutOfMemory,
$"Failed to create {(isDynamic ? "Dynamic " : "")}AutoDiff cost function");
}
return CostFunctionHandle.Create(handle);
});
}
/// <summary>
/// Creates a NumericDiff cost function handle (fixed or dynamic).
/// </summary>
internal static CostFunctionHandle CreateNumericDiffHandle(
CeresNative.CeresNumericDiffCostFunctionCallback nativeCallback,
IntPtr userData,
int numResiduals,
int[] parameterBlockSizes,
NumericDiffMethod method,
bool isDynamic)
{
return Native.UnsafeHelpers.WithPinnedArray(parameterBlockSizes, (ptr) =>
{
var options = new CeresNative.CeresNumericDiffOptions
{
numResiduals = numResiduals,
numParameterBlocks = parameterBlockSizes.Length,
parameterBlockSizes = ptr,
callback = nativeCallback,
userData = userData
};
var handle = isDynamic
? CeresNative.ceres_wrapper_create_dynamic_numeric_diff_cost_function(ref options, (int)method)
: CeresNative.ceres_wrapper_create_numeric_diff_cost_function(ref options, (int)method);
if (handle == IntPtr.Zero)
{
throw new CeresException(CeresErrorCode.OutOfMemory,
$"Failed to create {(isDynamic ? "Dynamic " : "")}NumericDiff cost function");
}
return CostFunctionHandle.Create(handle);
});
}
/// <summary>
/// Wrapper struct for AutoDiff cost function callbacks.
/// </summary>
internal class AutoDiffCallbackWrapper
{
public AutoDiffCostFunctionCallback Callback = null!;
public int NumResiduals;
public int[] ParameterBlockSizes = null!;
}
/// <summary>
/// Wrapper struct for NumericDiff cost function callbacks.
/// </summary>
internal class NumericCallbackWrapper
{
public NumericDiffCostFunctionCallback Callback = null!;
public int NumResiduals;
public int[] ParameterBlockSizes = null!;
}
}

View File

@@ -0,0 +1,76 @@
using System;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
namespace CeresSharp;
/// <summary>
/// Cubic interpolator for 1D data.
/// </summary>
public sealed class CubicInterpolator : IDisposable
{
private readonly InterpolatorHandle _handle;
private bool _disposed;
/// <summary>
/// Creates a new cubic interpolator from 1D data array.
/// </summary>
/// <param name="data">The data array.</param>
public CubicInterpolator(double[] data)
{
if (data == null)
throw new ArgumentNullException(nameof(data));
if (data.Length == 0)
throw new ArgumentException("Data array cannot be empty", nameof(data));
unsafe
{
fixed (double* ptr = data)
{
var handle = CeresNative.ceres_wrapper_create_cubic_interpolator(
(IntPtr)ptr,
data.Length);
_handle = InterpolatorHandle.CreateCubic(handle);
}
}
}
/// <summary>
/// Evaluates the interpolator at point x.
/// </summary>
/// <param name="x">Coordinate in data space (0 <= x < numValues).</param>
/// <param name="value">Output value at x.</param>
/// <param name="gradient">Output gradient at x (can be null).</param>
public void Evaluate(double x, out double value, out double? gradient)
{
unsafe
{
double val = 0, grad = 0;
CeresNative.ceres_wrapper_cubic_interpolator_evaluate(
_handle.DangerousGetHandle(),
x,
(IntPtr)(&val),
(IntPtr)(&grad));
value = val;
gradient = grad;
}
}
/// <summary>
/// Evaluates the interpolator at point x without gradient.
/// </summary>
public double Evaluate(double x)
{
Evaluate(x, out var value, out _);
return value;
}
public void Dispose()
{
if (!_disposed)
{
_handle?.Dispose();
_disposed = true;
}
}
}

View File

@@ -0,0 +1,180 @@
using System;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
namespace CeresSharp;
/// <summary>
/// Base class for loss functions in Ceres Solver.
/// </summary>
public abstract class LossFunction : IDisposable
{
private readonly LossFunctionHandle _handle;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the LossFunction class.
/// </summary>
internal LossFunction(LossFunctionHandle handle)
{
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
}
/// <summary>
/// Gets the native handle.
/// </summary>
internal IntPtr Handle => _handle.DangerousGetHandle();
/// <summary>
/// Gets whether this loss function is owned by a Problem.
/// </summary>
internal bool IsOwnedByProblem => _handle is { IsInvalid: false } && _ownedByProblem;
private volatile bool _ownedByProblem;
/// <summary>
/// Marks this loss function as owned by a Problem.
/// After this, the native handle will NOT be freed on Dispose - Ceres handles cleanup.
/// </summary>
internal void MarkOwnedByProblem()
{
_ownedByProblem = true;
_handle.MarkOwnedByProblem();
}
public void Dispose()
{
if (!_disposed)
{
_handle?.Dispose();
_disposed = true;
}
}
}
/// <summary>
/// Trivial loss function (no loss applied).
/// </summary>
public sealed class TrivialLoss : LossFunction
{
public TrivialLoss() : base(LossFunctionHandle.Create(CeresNative.ceres_wrapper_create_trivial_loss()))
{
}
}
/// <summary>
/// Huber loss function.
/// </summary>
public sealed class HuberLoss : LossFunction
{
/// <summary>
/// Creates a new Huber loss function.
/// </summary>
/// <param name="a">The scaling parameter.</param>
public HuberLoss(double a) : base(LossFunctionHandle.Create(CeresNative.ceres_wrapper_create_huber_loss(a)))
{
}
}
/// <summary>
/// Cauchy loss function.
/// </summary>
public sealed class CauchyLoss : LossFunction
{
/// <summary>
/// Creates a new Cauchy loss function.
/// </summary>
/// <param name="a">The scaling parameter.</param>
public CauchyLoss(double a) : base(LossFunctionHandle.Create(CeresNative.ceres_wrapper_create_cauchy_loss(a)))
{
}
}
/// <summary>
/// SoftLOne loss function.
/// </summary>
public sealed class SoftLOneLoss : LossFunction
{
/// <summary>
/// Creates a new SoftLOne loss function.
/// </summary>
/// <param name="a">The scaling parameter.</param>
public SoftLOneLoss(double a) : base(LossFunctionHandle.Create(CeresNative.ceres_wrapper_create_softl1_loss(a)))
{
}
}
/// <summary>
/// Arctan loss function.
/// </summary>
public sealed class ArctanLoss : LossFunction
{
/// <summary>
/// Creates a new Arctan loss function.
/// </summary>
/// <param name="a">The scaling parameter.</param>
public ArctanLoss(double a) : base(LossFunctionHandle.Create(CeresNative.ceres_wrapper_create_arctan_loss(a)))
{
}
}
/// <summary>
/// Tolerant loss function.
/// </summary>
public sealed class TolerantLoss : LossFunction
{
/// <summary>
/// Creates a new Tolerant loss function.
/// </summary>
/// <param name="a">The first scaling parameter.</param>
/// <param name="b">The second scaling parameter.</param>
public TolerantLoss(double a, double b) : base(LossFunctionHandle.Create(CeresNative.ceres_wrapper_create_tolerant_loss(a, b)))
{
}
}
/// <summary>
/// Composed loss function: f(g(s)).
/// The composed loss takes ownership of both f and g.
/// WARNING: After creating a ComposedLoss, do NOT dispose the original f and g
/// LossFunction objects - ownership is transferred to the composed loss.
/// </summary>
public sealed class ComposedLoss : LossFunction
{
/// <summary>
/// Creates a new Composed loss function f(g(s)).
/// </summary>
/// <param name="f">The outer loss function.</param>
/// <param name="g">The inner loss function.</param>
public ComposedLoss(LossFunction f, LossFunction g)
: base(LossFunctionHandle.Create(
CeresNative.ceres_wrapper_create_composed_loss(
f?.Handle ?? throw new ArgumentNullException(nameof(f)),
1, // TAKE_OWNERSHIP
g?.Handle ?? throw new ArgumentNullException(nameof(g)),
1))) // TAKE_OWNERSHIP
{
}
}
/// <summary>
/// Scaled loss function: a * rho(s).
/// The scaled loss takes ownership of the underlying loss function.
/// WARNING: After creating a ScaledLoss, do NOT dispose the original rho
/// LossFunction object - ownership is transferred to the scaled loss.
/// </summary>
public sealed class ScaledLoss : LossFunction
{
/// <summary>
/// Creates a new Scaled loss function a * rho(s).
/// </summary>
/// <param name="rho">The underlying loss function (null for identity).</param>
/// <param name="a">The scaling factor.</param>
public ScaledLoss(LossFunction? rho, double a)
: base(LossFunctionHandle.Create(
CeresNative.ceres_wrapper_create_scaled_loss(
rho?.Handle ?? IntPtr.Zero,
a,
1))) // TAKE_OWNERSHIP
{
}
}

View File

@@ -0,0 +1,313 @@
using System;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
namespace CeresSharp;
/// <summary>
/// Base class for manifolds in Ceres Solver.
/// </summary>
public abstract class Manifold : IDisposable
{
private readonly ManifoldHandle _handle;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the Manifold class.
/// </summary>
internal Manifold(ManifoldHandle handle)
{
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
}
/// <summary>
/// Gets the native handle.
/// </summary>
internal IntPtr Handle => _handle.DangerousGetHandle();
/// <summary>
/// Gets the ambient size (the size of the parameter space).
/// </summary>
public int AmbientSize => CeresNative.ceres_wrapper_manifold_ambient_size(_handle.DangerousGetHandle());
/// <summary>
/// Gets the tangent size (the size of the tangent space).
/// </summary>
public int TangentSize => CeresNative.ceres_wrapper_manifold_tangent_size(_handle.DangerousGetHandle());
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_handle?.Dispose();
}
_disposed = true;
}
}
}
/// <summary>
/// Quaternion manifold for 3D rotations.
/// </summary>
public sealed class QuaternionManifold : Manifold
{
public QuaternionManifold() : base(ManifoldHandle.Create(CeresNative.ceres_wrapper_create_quaternion_manifold()))
{
}
}
/// <summary>
/// Sphere manifold.
/// </summary>
public sealed class SphereManifold : Manifold
{
/// <summary>
/// Creates a new sphere manifold.
/// </summary>
/// <param name="dimension">The dimension of the sphere.</param>
public SphereManifold(int dimension) : base(ManifoldHandle.Create(CeresNative.ceres_wrapper_create_sphere_manifold(dimension)))
{
if (dimension <= 0)
throw new ArgumentException("Dimension must be positive", nameof(dimension));
}
}
/// <summary>
/// Line manifold.
/// </summary>
public sealed class LineManifold : Manifold
{
/// <summary>
/// Creates a new line manifold.
/// </summary>
/// <param name="dimension">The dimension of the line.</param>
public LineManifold(int dimension) : base(ManifoldHandle.Create(CeresNative.ceres_wrapper_create_line_manifold(dimension)))
{
if (dimension <= 0)
throw new ArgumentException("Dimension must be positive", nameof(dimension));
}
}
/// <summary>
/// Euclidean manifold (standard Euclidean space, no constraints).
/// </summary>
public sealed class EuclideanManifold : Manifold
{
/// <summary>
/// Creates a new Euclidean manifold.
/// </summary>
/// <param name="dimension">The dimension of the Euclidean space.</param>
public EuclideanManifold(int dimension) : base(ManifoldHandle.Create(CeresNative.ceres_wrapper_create_euclidean_manifold(dimension)))
{
if (dimension <= 0)
throw new ArgumentException("Dimension must be positive", nameof(dimension));
}
}
/// <summary>
/// Subset manifold (fix subset of parameters).
/// </summary>
public sealed class SubsetManifold : Manifold
{
/// <summary>
/// Creates a new subset manifold.
/// </summary>
/// <param name="constantSubset">Array of indices to keep constant.</param>
/// <param name="ambientSize">Total size of parameter block.</param>
public SubsetManifold(int[] constantSubset, int ambientSize) : base(CreateHandle(constantSubset, ambientSize))
{
}
private static ManifoldHandle CreateHandle(int[] constantSubset, int ambientSize)
{
if (constantSubset == null)
throw new ArgumentNullException(nameof(constantSubset));
if (ambientSize <= 0)
throw new ArgumentException("Ambient size must be positive", nameof(ambientSize));
unsafe
{
fixed (int* ptr = constantSubset)
{
var handle = CeresNative.ceres_wrapper_create_subset_manifold(
(IntPtr)ptr,
constantSubset.Length,
ambientSize);
return ManifoldHandle.Create(handle);
}
}
}
}
/// <summary>
/// Product manifold (combines multiple manifolds).
/// </summary>
public sealed class ProductManifold : Manifold
{
// CRITICAL: Keep references to child manifolds to prevent GC collection
// Native code may still use handles from these manifolds, so they must remain alive
private readonly Manifold[] _manifolds;
/// <summary>
/// Creates a new product manifold from multiple manifolds.
/// </summary>
/// <param name="manifolds">Array of manifolds to combine.</param>
public ProductManifold(Manifold[] manifolds) : base(CreateHandle(manifolds))
{
// CRITICAL: Keep reference to prevent GC collection of child manifolds
// Native code may still use handles from these manifolds during optimization
_manifolds = manifolds ?? throw new ArgumentNullException(nameof(manifolds));
}
private static ManifoldHandle CreateHandle(Manifold[] manifolds)
{
if (manifolds == null || manifolds.Length == 0)
throw new ArgumentException("Manifolds cannot be null or empty", nameof(manifolds));
unsafe
{
var handles = new IntPtr[manifolds.Length];
for (int i = 0; i < manifolds.Length; i++)
{
if (manifolds[i] == null)
throw new ArgumentException($"Manifold {i} is null", nameof(manifolds));
handles[i] = manifolds[i].Handle;
}
fixed (IntPtr* ptr = handles)
{
var handle = CeresNative.ceres_wrapper_create_product_manifold(
(IntPtr)ptr,
manifolds.Length);
return ManifoldHandle.Create(handle);
}
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
// Dispose child manifolds to cleanup their resources (including GCHandles in AutoDiffManifold)
// This is critical because AutoDiffManifold (used by AngleManifold) contains GCHandles
// that must be freed when the manifold is no longer needed
if (_manifolds != null)
{
foreach (var manifold in _manifolds)
{
manifold?.Dispose();
}
}
}
base.Dispose(disposing);
}
}
/// <summary>
/// Angle manifold for 2D rotation angles.
/// Constrains angles to [-π, π] range and handles wrap-around correctly.
/// </summary>
public sealed class AngleManifold : Manifold
{
private readonly AutoDiffManifold _autoDiffManifold;
/// <summary>
/// Creates a new angle manifold for 2D rotation.
/// </summary>
public AngleManifold() : base(CreateHandle(out var autoDiffManifold))
{
_autoDiffManifold = autoDiffManifold;
}
private static ManifoldHandle CreateHandle(out AutoDiffManifold autoDiffManifold)
{
autoDiffManifold = new AutoDiffManifold(
ambientSize: 1,
tangentSize: 1,
plus: (x, delta, xPlusDelta) =>
{
// Normalize angle: x[0] + delta[0] -> [-π, π]
double angle = x[0] + delta[0];
xPlusDelta[0] = NormalizeAngle(angle);
return true;
},
minus: (y, x, yMinusX) =>
{
// Normalize both angles and compute difference
double normalizedY = NormalizeAngle(y[0]);
double normalizedX = NormalizeAngle(x[0]);
yMinusX[0] = NormalizeAngle(normalizedY - normalizedX);
return true;
});
// Get handle from AutoDiffManifold using reflection
var handleField = typeof(Manifold).GetField("_handle", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
return (ManifoldHandle)handleField!.GetValue(autoDiffManifold)!;
}
/// <summary>
/// Normalizes angle to [-π, π] range.
/// </summary>
private static double NormalizeAngle(double angle)
{
while (angle > Math.PI)
angle -= 2.0 * Math.PI;
while (angle < -Math.PI)
angle += 2.0 * Math.PI;
return angle;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_autoDiffManifold?.Dispose();
}
base.Dispose(disposing);
}
}
/// <summary>
/// Pose2D manifold for 2D poses [x, y, theta].
/// Combines EuclideanManifold for translation [x, y] and AngleManifold for rotation [theta].
/// </summary>
public sealed class Pose2DManifold : Manifold
{
private readonly ProductManifold _productManifold;
/// <summary>
/// Creates a new Pose2D manifold.
/// </summary>
public Pose2DManifold() : base(CreateHandle(out var productManifold))
{
_productManifold = productManifold;
}
private static ManifoldHandle CreateHandle(out ProductManifold productManifold)
{
var translationManifold = new EuclideanManifold(2); // [x, y]
var rotationManifold = new AngleManifold(); // [theta]
productManifold = new ProductManifold([translationManifold, rotationManifold]);
// Get handle from ProductManifold using reflection
var handleField = typeof(Manifold).GetField("_handle", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
return (ManifoldHandle)handleField!.GetValue(productManifold)!;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_productManifold?.Dispose();
}
base.Dispose(disposing);
}
}

View File

@@ -0,0 +1,852 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using CeresSharp.Advanced;
using CeresSharp.Exceptions;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
using static CeresSharp.Native.NativeHelpers;
using static CeresSharp.Native.UnsafeHelpers;
namespace CeresSharp;
/// <summary>
/// Represents a Ceres optimization problem.
/// </summary>
public sealed class Problem : IDisposable
{
private readonly ProblemHandle _handle;
// CRITICAL: Use volatile for thread-safe disposal check
// Dispose() should be idempotent, but volatile ensures proper memory visibility
// across threads when checking _disposed flag
private volatile bool _disposed;
// Keep references to parameter blocks to prevent GC and keep them pinned
// Dictionary maps pinned pointer address -> GCHandle for efficient lookup on RemoveParameterBlock
private readonly Dictionary<IntPtr, GCHandle> _pinnedParameterBlocks = new Dictionary<IntPtr, GCHandle>();
// CRITICAL: Keep references to cost functions and loss functions to prevent GC collection
// Native code may still call callbacks even after solve completes, so we must keep
// these objects alive for the lifetime of the Problem
private readonly List<CostFunction> _costFunctions = new List<CostFunction>();
private readonly List<LossFunction> _lossFunctions = new List<LossFunction>();
// CRITICAL: Keep references to manifolds to prevent GC collection
// Native code may still call manifold callbacks (Plus/Minus) even after solve completes,
// especially for AutoDiffManifold with GCHandle callbacks, so we must keep these objects alive
private readonly List<Manifold> _manifolds = new List<Manifold>();
/// <summary>
/// Creates a new Problem instance.
/// </summary>
public Problem()
{
_handle = ProblemHandle.Create();
}
/// <summary>
/// Creates a new Problem instance with the specified options.
/// </summary>
public Problem(ProblemOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
_handle = ProblemHandle.CreateWithOptions(options.Handle.DangerousGetHandle());
}
/// <summary>
/// Gets the native handle.
/// </summary>
internal ProblemHandle Handle => _handle;
// ============================================================================
// Parameter Block Management
// ============================================================================
/// <summary>
/// Adds a parameter block to the problem.
/// </summary>
/// <param name="parameters">The parameter array.</param>
/// <param name="size">The size of the parameter block.</param>
/// <exception cref="ArgumentNullException">Thrown when parameters is null.</exception>
/// <exception cref="CeresException">Thrown when the operation fails.</exception>
public void AddParameterBlock(double[] parameters, int size)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
if (size <= 0)
throw new ArgumentException("Size must be positive", nameof(size));
if (parameters.Length < size)
throw new ArgumentException("Parameters array length must be at least size", nameof(parameters));
// CRITICAL: Pin array permanently for the lifetime of the problem
// Ceres stores pointers to the arrays, so they must remain valid and pinned
// This is the same approach used in AddResidualBlock
var handle = PinArray(parameters);
try
{
var ptr = handle.AddrOfPinnedObject();
var errorMessageBuffer = CreateErrorMessageBuffer();
var errorCode = CeresNative.ceres_wrapper_problem_add_parameter_block(
_handle.DangerousGetHandle(),
ptr,
size,
errorMessageBuffer,
errorMessageBuffer.Capacity);
if (errorCode != CeresErrorCode.Success)
{
// On error, free the handle before throwing
if (handle.IsAllocated)
handle.Free();
var errorMessage = ExtractErrorMessage(errorMessageBuffer);
throw new CeresException(errorCode, errorMessage);
}
// Keep array pinned for the lifetime of the problem
// Add to the dictionary keyed by pointer address (will be unpinned in Dispose)
lock (_pinnedParameterBlocks)
{
_pinnedParameterBlocks[ptr] = handle;
}
}
catch
{
// On exception, free the handle before re-throwing
if (handle.IsAllocated)
handle.Free();
throw;
}
}
/// <summary>
/// Sets a parameter block to be constant (not optimized).
/// </summary>
public void SetParameterBlockConstant(double[] parameters)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
WithPinnedArray(parameters, (ptr) =>
{
CeresNative.ceres_wrapper_problem_set_parameter_block_constant(
_handle.DangerousGetHandle(),
ptr);
});
}
/// <summary>
/// Sets a parameter block to be variable (optimized).
/// </summary>
public void SetParameterBlockVariable(double[] parameters)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
WithPinnedArray(parameters, (ptr) =>
{
CeresNative.ceres_wrapper_problem_set_parameter_block_variable(
_handle.DangerousGetHandle(),
ptr);
});
}
/// <summary>
/// Removes a parameter block from the problem.
/// </summary>
/// <returns>True if the parameter block was removed, false if it was not found.</returns>
/// <exception cref="ArgumentNullException">Thrown when parameters is null.</exception>
/// <exception cref="CeresException">Thrown when the operation fails.</exception>
public bool RemoveParameterBlock(double[] parameters)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
return WithPinnedArray(parameters, (ptr) =>
{
var errorMessageBuffer = CreateErrorMessageBuffer();
var errorCode = CeresNative.ceres_wrapper_problem_remove_parameter_block(
_handle.DangerousGetHandle(),
ptr,
errorMessageBuffer,
errorMessageBuffer.Capacity);
if (errorCode != CeresErrorCode.Success)
{
var errorMessage = ExtractErrorMessage(errorMessageBuffer);
throw new CeresException(errorCode, errorMessage);
}
// Free the GCHandle for the removed parameter block to unpin its memory
lock (_pinnedParameterBlocks)
{
if (_pinnedParameterBlocks.TryGetValue(ptr, out var gcHandle))
{
if (gcHandle.IsAllocated)
gcHandle.Free();
_pinnedParameterBlocks.Remove(ptr);
}
}
return true;
});
}
// ============================================================================
// Residual Block Management
// ============================================================================
/// <summary>
/// Adds a residual block to the problem.
/// </summary>
/// <param name="costFunction">The cost function.</param>
/// <param name="lossFunction">The loss function (can be null for TrivialLoss).</param>
/// <param name="parameterBlocks">Array of parameter block arrays.</param>
/// <returns>The residual block ID.</returns>
public IntPtr AddResidualBlock(
CostFunction costFunction,
LossFunction? lossFunction,
double[][] parameterBlocks)
{
if (costFunction == null)
throw new ArgumentNullException(nameof(costFunction));
if (parameterBlocks == null || parameterBlocks.Length == 0)
throw new ArgumentException("Parameter blocks cannot be null or empty", nameof(parameterBlocks));
// Pin parameter blocks - Ceres stores pointers to the arrays, so they must
// remain valid and pinned for the lifetime of the problem
// NOTE: These temporary collections are created per call but are necessary for proper
// memory management. They will be cleaned up by GC after the method returns.
var pinnedArrays = new List<GCHandle>(parameterBlocks.Length);
var parameterBlockPtrs = new IntPtr[parameterBlocks.Length];
try
{
// Pin all parameter blocks
for (int i = 0; i < parameterBlocks.Length; i++)
{
if (parameterBlocks[i] == null)
throw new ArgumentException($"Parameter block {i} is null", nameof(parameterBlocks));
var handle = PinArray(parameterBlocks[i]);
pinnedArrays.Add(handle);
parameterBlockPtrs[i] = handle.AddrOfPinnedObject();
}
// Execute native call with pinned pointer array
// NOTE: WithPinnedArray uses 'fixed' statement which automatically unpins when done
IntPtr residualBlockId = IntPtr.Zero;
WithPinnedArray(parameterBlockPtrs, (ptrs) =>
{
// NOTE: StringBuilder is created per call but is lightweight and will be GC'd
// This is acceptable overhead for error message handling
var errorMessageBuffer = CreateErrorMessageBuffer();
var errorCode = CeresNative.ceres_wrapper_problem_add_residual_block(
_handle.DangerousGetHandle(),
costFunction.Handle,
lossFunction?.Handle ?? IntPtr.Zero,
ptrs,
parameterBlocks.Length,
out residualBlockId,
errorMessageBuffer,
errorMessageBuffer.Capacity);
if (errorCode != CeresErrorCode.Success)
{
var errorMsg = ExtractErrorMessage(errorMessageBuffer);
throw new CeresException(errorCode, errorMsg);
}
});
// Keep arrays pinned for the lifetime of the problem
// Add to the dictionary keyed by pointer address (will be unpinned in Dispose)
// CRITICAL: Must add to dictionary atomically to avoid partial adds on exception
lock (_pinnedParameterBlocks)
{
for (int i = 0; i < pinnedArrays.Count; i++)
{
_pinnedParameterBlocks[parameterBlockPtrs[i]] = pinnedArrays[i];
}
// Clear local list after successful add to prevent double-free in catch block
// Handles are now owned by _pinnedParameterBlocks and will be freed in Dispose
pinnedArrays.Clear();
}
// Mark cost function and loss function as owned by this Problem so their native handles
// won't be freed independently (Ceres will free them when Problem is destroyed)
costFunction.MarkOwnedByProblem();
lossFunction?.MarkOwnedByProblem();
// CRITICAL: Keep references to cost function and loss function to prevent GC collection
// Native code may still call callbacks even after solve completes
lock (_costFunctions)
{
_costFunctions.Add(costFunction);
}
if (lossFunction != null)
{
lock (_lossFunctions)
{
_lossFunctions.Add(lossFunction);
}
}
return residualBlockId;
}
catch
{
// On error, unpin arrays that were pinned but not yet added to _pinnedParameterBlocks
// This prevents memory leaks if exception occurs before AddRange completes
// NOTE: After successful AddRange, pinnedArrays is cleared, so this only frees
// handles that were pinned but not yet added to the list
foreach (var handle in pinnedArrays)
{
try
{
if (handle.IsAllocated)
handle.Free();
}
catch
{
// Ignore errors during cleanup - handle may already be freed or invalid
}
}
// Clear the list to help GC
pinnedArrays.Clear();
throw;
}
}
/// <summary>
/// Removes a residual block from the problem.
/// </summary>
/// <returns>True if the residual block was removed, false if it was not found.</returns>
/// <exception cref="ArgumentException">Thrown when residualBlockId is zero.</exception>
/// <exception cref="CeresException">Thrown when the operation fails.</exception>
public bool RemoveResidualBlock(IntPtr residualBlockId)
{
if (residualBlockId == IntPtr.Zero)
throw new ArgumentException("Residual block ID cannot be zero", nameof(residualBlockId));
var errorMessageBuffer = CreateErrorMessageBuffer();
var errorCode = CeresNative.ceres_wrapper_problem_remove_residual_block(
_handle.DangerousGetHandle(),
residualBlockId,
errorMessageBuffer,
errorMessageBuffer.Capacity);
if (errorCode != CeresErrorCode.Success)
{
var errorMessage = ExtractErrorMessage(errorMessageBuffer);
throw new CeresException(errorCode, errorMessage);
}
return true;
}
// ============================================================================
// Manifold Management
// ============================================================================
/// <summary>
/// Sets a manifold for a parameter block.
/// </summary>
public void SetManifold(double[] parameters, Manifold manifold)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
if (manifold == null)
throw new ArgumentNullException(nameof(manifold));
WithPinnedArray(parameters, (ptr) =>
{
CeresNative.ceres_wrapper_problem_set_manifold(
_handle.DangerousGetHandle(),
ptr,
manifold.Handle);
});
// CRITICAL: Keep reference to manifold to prevent GC collection
// Native code may still call manifold callbacks (Plus/Minus) even after solve completes,
// especially for AutoDiffManifold with GCHandle callbacks, so we must keep the object alive
// for the lifetime of the Problem
lock (_manifolds)
{
_manifolds.Add(manifold);
}
}
// ============================================================================
// Parameter Bounds
// ============================================================================
/// <summary>
/// Sets a lower bound for a parameter.
/// </summary>
/// <summary>
/// Sets a lower bound for a parameter in a parameter block.
/// </summary>
public void SetParameterLowerBound(double[] parameters, int index, double lowerBound)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
if (index < 0 || index >= parameters.Length)
throw new ArgumentOutOfRangeException(nameof(index));
WithPinnedArray(parameters, (ptr) =>
{
CeresNative.ceres_wrapper_problem_set_parameter_lower_bound(
_handle.DangerousGetHandle(),
ptr,
index,
lowerBound);
});
}
/// <summary>
/// Sets an upper bound for a parameter.
/// </summary>
public void SetParameterUpperBound(double[] parameters, int index, double upperBound)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
if (index < 0 || index >= parameters.Length)
throw new ArgumentOutOfRangeException(nameof(index));
WithPinnedArray(parameters, (ptr) =>
{
CeresNative.ceres_wrapper_problem_set_parameter_upper_bound(
_handle.DangerousGetHandle(),
ptr,
index,
upperBound);
});
}
/// <summary>
/// Gets the lower bound for a parameter.
/// </summary>
public double GetParameterLowerBound(double[] parameters, int index)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
if (index < 0 || index >= parameters.Length)
throw new ArgumentOutOfRangeException(nameof(index));
return WithPinnedArray(parameters, (ptr) =>
{
return CeresNative.ceres_wrapper_problem_get_parameter_lower_bound(
_handle.DangerousGetHandle(),
ptr,
index);
});
}
/// <summary>
/// Gets the upper bound for a parameter.
/// </summary>
public double GetParameterUpperBound(double[] parameters, int index)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
if (index < 0 || index >= parameters.Length)
throw new ArgumentOutOfRangeException(nameof(index));
return WithPinnedArray(parameters, (ptr) =>
{
return CeresNative.ceres_wrapper_problem_get_parameter_upper_bound(
_handle.DangerousGetHandle(),
ptr,
index);
});
}
// ============================================================================
// Query Methods
// ============================================================================
/// <summary>
/// Gets the number of parameter blocks in the problem.
/// </summary>
public int NumParameterBlocks =>
CeresNative.ceres_wrapper_problem_num_parameter_blocks(_handle.DangerousGetHandle());
/// <summary>
/// Gets the number of residual blocks in the problem.
/// </summary>
public int NumResidualBlocks =>
CeresNative.ceres_wrapper_problem_num_residual_blocks(_handle.DangerousGetHandle());
/// <summary>
/// Gets the total number of parameters in the problem.
/// </summary>
public int NumParameters =>
CeresNative.ceres_wrapper_problem_num_parameters(_handle.DangerousGetHandle());
/// <summary>
/// Gets the total number of residuals in the problem.
/// </summary>
public int NumResiduals =>
CeresNative.ceres_wrapper_problem_num_residuals(_handle.DangerousGetHandle());
/// <summary>
/// Checks if a parameter block exists in the problem.
/// </summary>
public bool HasParameterBlock(double[] parameters)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
return WithPinnedArray(parameters, (ptr) =>
{
return CeresNative.ceres_wrapper_problem_has_parameter_block(
_handle.DangerousGetHandle(),
ptr) != 0;
});
}
/// <summary>
/// Checks if a parameter block is constant.
/// </summary>
public bool IsParameterBlockConstant(double[] parameters)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
return WithPinnedArray(parameters, (ptr) =>
{
return CeresNative.ceres_wrapper_problem_is_parameter_block_constant(
_handle.DangerousGetHandle(),
ptr) != 0;
});
}
/// <summary>
/// Gets the size of a parameter block.
/// </summary>
public int GetParameterBlockSize(double[] parameters)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
return WithPinnedArray(parameters, (ptr) =>
{
return CeresNative.ceres_wrapper_problem_get_parameter_block_size(
_handle.DangerousGetHandle(),
ptr);
});
}
/// <summary>
/// Gets the tangent size of a parameter block (with manifold).
/// </summary>
public int GetParameterBlockTangentSize(double[] parameters)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
return WithPinnedArray(parameters, (ptr) =>
{
return CeresNative.ceres_wrapper_problem_get_parameter_block_tangent_size(
_handle.DangerousGetHandle(),
ptr);
});
}
/// <summary>
/// Checks if a parameter block has a manifold.
/// </summary>
public bool HasManifold(double[] parameters)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
return WithPinnedArray(parameters, (ptr) =>
{
return CeresNative.ceres_wrapper_problem_has_manifold(
_handle.DangerousGetHandle(),
ptr) != 0;
});
}
/// <summary>
/// Gets the manifold handle for a parameter block (returns IntPtr.Zero if no manifold).
/// Note: This returns the native handle. To get a Manifold object, use HasManifold first.
/// </summary>
public IntPtr GetManifoldHandle(double[] parameters)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
return WithPinnedArray(parameters, (ptr) =>
{
return CeresNative.ceres_wrapper_problem_get_manifold(
_handle.DangerousGetHandle(),
ptr);
});
}
// ============================================================================
// Solve
// ============================================================================
/// <summary>
/// Solves the problem with the given options and returns a summary.
/// </summary>
/// <exception cref="CeresException">Thrown if the solve operation fails.</exception>
public SolverSummary Solve(SolverOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
// Check if disposed
if (_disposed)
{
throw new ObjectDisposedException(nameof(Problem), "Cannot solve a disposed problem");
}
// Validate _handle
if (_handle == null)
{
throw new InvalidOperationException("Problem handle is null");
}
if (_handle.IsInvalid)
{
throw new InvalidOperationException("Problem handle is invalid");
}
// Validate options.Handle
if (options.Handle == null)
{
throw new InvalidOperationException("SolverOptions handle is null");
}
if (options.Handle.IsInvalid)
{
throw new InvalidOperationException("SolverOptions handle is invalid");
}
var summaryHandle = SolverSummaryHandle.Create();
if (summaryHandle == null)
{
throw new InvalidOperationException("Failed to create SolverSummaryHandle");
}
if (summaryHandle.IsInvalid)
{
summaryHandle?.Dispose();
throw new InvalidOperationException("SolverSummaryHandle is invalid");
}
// Track ownership transfer to prevent double-free
// Once SolverSummary is created, it owns summaryHandle and will dispose it
bool ownershipTransferred = false;
try
{
// Extract handles before calling native function
var problemHandle = _handle.DangerousGetHandle();
var solverOptionsHandle = options.Handle.DangerousGetHandle();
var summaryHandlePtr = summaryHandle.DangerousGetHandle();
// Validate handles are not null/zero
if (problemHandle == IntPtr.Zero)
{
summaryHandle?.Dispose();
throw new InvalidOperationException("Problem handle pointer is zero");
}
if (solverOptionsHandle == IntPtr.Zero)
{
summaryHandle?.Dispose();
throw new InvalidOperationException("SolverOptions handle pointer is zero");
}
if (summaryHandlePtr == IntPtr.Zero)
{
summaryHandle?.Dispose();
throw new InvalidOperationException("SolverSummary handle pointer is zero");
}
var errorMessageBuffer = CreateErrorMessageBuffer(512);
if (errorMessageBuffer == null)
{
summaryHandle?.Dispose();
throw new InvalidOperationException("Failed to create error message buffer");
}
var errorCode = CeresNative.ceres_wrapper_solve(
problemHandle,
solverOptionsHandle,
summaryHandlePtr,
errorMessageBuffer,
errorMessageBuffer.Capacity);
if (errorCode != CeresErrorCode.Success)
{
summaryHandle?.Dispose();
var message = ExtractErrorMessage(errorMessageBuffer);
throw new CeresException(errorCode, message);
}
// CRITICAL: Create SolverSummary and transfer ownership of summaryHandle
// After SolverSummary is created successfully, ownership is transferred
// and summaryHandle will be disposed when SolverSummary is disposed
var summary = new SolverSummary(summaryHandle);
// Ownership successfully transferred to SolverSummary
ownershipTransferred = true;
return summary;
}
catch (CeresException)
{
// Dispose summaryHandle only if ownership hasn't been transferred to SolverSummary
// This prevents double-free if SolverSummary was created successfully
if (!ownershipTransferred)
{
summaryHandle?.Dispose();
}
throw;
}
catch (Exception)
{
// Dispose summaryHandle only if ownership hasn't been transferred to SolverSummary
// This prevents double-free if SolverSummary was created successfully
if (!ownershipTransferred)
{
summaryHandle?.Dispose();
}
throw;
}
}
// ============================================================================
// IDisposable
// ============================================================================
public void Dispose()
{
Dispose(true);
// Suppress finalizer to prevent crash in finalizer thread
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// CRITICAL: Dispose native Problem handle FIRST
// This ensures native code has finished using all callbacks and objects
if (_handle != null)
{
_handle.Dispose();
// Suppress finalization to prevent crash in finalizer thread
GC.SuppressFinalize(_handle);
}
// CRITICAL: Free GCHandles in CostFunctions/LossFunctions AFTER native Problem is freed
// Native code (Ceres) owns the CostFunction/LossFunction objects and will delete them
// when Problem is freed. However, the C# wrapper objects still hold GCHandles
// (for callbacks and wrappers) that MUST be freed to prevent memory leaks.
//
// IMPORTANT: We call FreeGCHandlesAfterProblemDisposed() instead of Dispose() because:
// 1. Native objects are already deleted by Ceres when Problem is freed
// 2. CostFunctionHandle.ReleaseNativeHandle() is intentionally a no-op to prevent double-free
// 3. But we MUST free GCHandles to prevent memory leaks (GCHandles pin memory)
//
// FreeGCHandlesAfterProblemDisposed() will:
// - Free _callbackHandle and _nativeCallbackHandle in base CostFunction
// - Free _wrapperHandle in derived classes (AutoDiffCostFunction, DynamicAutoDiffCostFunction, etc.)
// - Dispose the SafeHandle (which is a no-op but ensures proper state)
// - Mark as disposed to prevent double-free
CostFunction[] costFunctionsToFree;
LossFunction[] lossFunctionsToFree;
Manifold[] manifoldsToFree;
lock (_costFunctions)
{
costFunctionsToFree = _costFunctions.ToArray();
_costFunctions.Clear();
}
lock (_lossFunctions)
{
lossFunctionsToFree = _lossFunctions.ToArray();
_lossFunctions.Clear();
}
lock (_manifolds)
{
manifoldsToFree = _manifolds.ToArray();
_manifolds.Clear();
}
// Free GCHandles in CostFunctions (outside lock to avoid deadlock)
foreach (var costFunction in costFunctionsToFree)
{
try
{
// Free GCHandles without trying to delete native handle (already freed by Ceres)
costFunction.FreeGCHandlesAfterProblemDisposed();
}
catch
{
// Ignore errors - may already be freed or disposed
}
}
// Note: LossFunctions and Manifolds don't have GCHandles that need explicit freeing.
// Arrays will go out of scope after method returns, allowing GC to collect them.
// No need to explicitly set to null.
// Unpin parameter blocks AFTER native Problem is freed
// Use a copy to avoid issues during enumeration
GCHandle[] handlesToFree;
lock (_pinnedParameterBlocks)
{
handlesToFree = new GCHandle[_pinnedParameterBlocks.Count];
_pinnedParameterBlocks.Values.CopyTo(handlesToFree, 0);
_pinnedParameterBlocks.Clear();
}
// Free handles outside the lock
foreach (var handle in handlesToFree)
{
if (handle.IsAllocated) handle.Free();
}
}
else
{
// Finalizer path - free native handle first, then GCHandles
if (_handle != null)
{
_handle.Dispose();
}
// Free pinned GCHandles to prevent permanent memory leak
// GCHandles are NOT automatically freed by GC - they must be explicitly freed.
// At finalization time, native Problem is freed above, so it's safe to unpin.
lock (_pinnedParameterBlocks)
{
foreach (var handle in _pinnedParameterBlocks.Values)
{
try { if (handle.IsAllocated) handle.Free(); } catch { }
}
_pinnedParameterBlocks.Clear();
}
}
_disposed = true;
}
}
~Problem()
{
// Finalizer - cleanup native handle and free pinned GCHandles
// GCHandles are NOT automatically freed by GC, so we must free them explicitly
Dispose(false);
}
}

View File

@@ -0,0 +1,409 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using CeresSharp.Enums;
using CeresSharp.Exceptions;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
namespace CeresSharp;
/// <summary>
/// Options for configuring the Ceres Solver.
/// </summary>
public sealed class SolverOptions : IDisposable
{
private readonly SolverOptionsHandle _handle;
private System.Runtime.InteropServices.GCHandle? _iterationCallbackHandle;
private System.Runtime.InteropServices.GCHandle? _iterationNativeCallbackHandle;
private bool _disposed;
/// <summary>
/// Creates a new SolverOptions instance with default settings.
/// </summary>
public SolverOptions()
{
_handle = SolverOptionsHandle.Create();
}
/// <summary>
/// Gets the native handle.
/// </summary>
internal SolverOptionsHandle Handle => _handle;
// ============================================================================
// Basic Options
// ============================================================================
/// <summary>
/// Gets or sets the linear solver type.
/// </summary>
public LinearSolverType LinearSolverType
{
get => (LinearSolverType)CeresNative.ceres_wrapper_solver_options_get_linear_solver_type(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_linear_solver_type(_handle.DangerousGetHandle(), (int)value);
}
/// <summary>
/// Gets or sets the minimizer type.
/// </summary>
public MinimizerType MinimizerType
{
get => (MinimizerType)CeresNative.ceres_wrapper_solver_options_get_minimizer_type(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_minimizer_type(_handle.DangerousGetHandle(), (int)value);
}
/// <summary>
/// Gets or sets the maximum number of iterations.
/// </summary>
public int MaxNumIterations
{
get => CeresNative.ceres_wrapper_solver_options_get_max_num_iterations(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_max_num_iterations(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets the number of threads to use.
/// </summary>
public int NumThreads
{
get => CeresNative.ceres_wrapper_solver_options_get_num_threads(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_num_threads(_handle.DangerousGetHandle(), value);
}
// ============================================================================
// Tolerances
// ============================================================================
/// <summary>
/// Gets or sets the function tolerance.
/// </summary>
public double FunctionTolerance
{
get => CeresNative.ceres_wrapper_solver_options_get_function_tolerance(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_function_tolerance(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets the gradient tolerance.
/// </summary>
public double GradientTolerance
{
get => CeresNative.ceres_wrapper_solver_options_get_gradient_tolerance(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_gradient_tolerance(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets the parameter tolerance.
/// </summary>
public double ParameterTolerance
{
get => CeresNative.ceres_wrapper_solver_options_get_parameter_tolerance(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_parameter_tolerance(_handle.DangerousGetHandle(), value);
}
// ============================================================================
// Trust Region Options
// ============================================================================
/// <summary>
/// Gets or sets the initial trust region radius.
/// </summary>
public double InitialTrustRegionRadius
{
get => CeresNative.ceres_wrapper_solver_options_get_initial_trust_region_radius(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_initial_trust_region_radius(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets the maximum trust region radius.
/// </summary>
public double MaxTrustRegionRadius
{
get => CeresNative.ceres_wrapper_solver_options_get_max_trust_region_radius(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_max_trust_region_radius(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets the minimum trust region radius.
/// </summary>
public double MinTrustRegionRadius
{
get => CeresNative.ceres_wrapper_solver_options_get_min_trust_region_radius(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_min_trust_region_radius(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets the trust region strategy type.
/// </summary>
public TrustRegionStrategyType TrustRegionStrategyType
{
get => (TrustRegionStrategyType)CeresNative.ceres_wrapper_solver_options_get_trust_region_strategy_type(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_trust_region_strategy_type(_handle.DangerousGetHandle(), (int)value);
}
/// <summary>
/// Gets or sets the dogleg type.
/// </summary>
public DoglegType DoglegType
{
get => (DoglegType)CeresNative.ceres_wrapper_solver_options_get_dogleg_type(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_dogleg_type(_handle.DangerousGetHandle(), (int)value);
}
// ============================================================================
// Preconditioner Options
// ============================================================================
/// <summary>
/// Gets or sets the preconditioner type.
/// </summary>
public PreconditionerType PreconditionerType
{
get => (PreconditionerType)CeresNative.ceres_wrapper_solver_options_get_preconditioner_type(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_preconditioner_type(_handle.DangerousGetHandle(), (int)value);
}
// ============================================================================
// Other Options
// ============================================================================
/// <summary>
/// Gets or sets whether to use non-monotonic steps.
/// </summary>
public bool UseNonmonotonicSteps
{
get => CeresNative.ceres_wrapper_solver_options_get_use_nonmonotonic_steps(_handle.DangerousGetHandle()) != 0;
set => CeresNative.ceres_wrapper_solver_options_set_use_nonmonotonic_steps(_handle.DangerousGetHandle(), value ? 1 : 0);
}
/// <summary>
/// Gets or sets the maximum consecutive non-monotonic steps.
/// </summary>
public int MaxConsecutiveNonmonotonicSteps
{
get => CeresNative.ceres_wrapper_solver_options_get_max_consecutive_nonmonotonic_steps(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_max_consecutive_nonmonotonic_steps(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets the maximum number of consecutive invalid steps.
/// </summary>
public int MaxNumConsecutiveInvalidSteps
{
get => CeresNative.ceres_wrapper_solver_options_get_max_num_consecutive_invalid_steps(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_max_num_consecutive_invalid_steps(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets the minimum relative decrease.
/// </summary>
public double MinRelativeDecrease
{
get => CeresNative.ceres_wrapper_solver_options_get_min_relative_decrease(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_min_relative_decrease(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets the logging type.
/// </summary>
public LoggingType LoggingType
{
get => (LoggingType)CeresNative.ceres_wrapper_solver_options_get_logging_type(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_logging_type(_handle.DangerousGetHandle(), (int)value);
}
/// <summary>
/// Gets or sets whether to output minimizer progress to stdout.
/// </summary>
public bool MinimizerProgressToStdout
{
get => CeresNative.ceres_wrapper_solver_options_get_minimizer_progress_to_stdout(_handle.DangerousGetHandle()) != 0;
set => CeresNative.ceres_wrapper_solver_options_set_minimizer_progress_to_stdout(_handle.DangerousGetHandle(), value ? 1 : 0);
}
// ============================================================================
// Line Search Options
// ============================================================================
/// <summary>
/// Gets or sets the line search type.
/// </summary>
public LineSearchType LineSearchType
{
get => (LineSearchType)CeresNative.ceres_wrapper_solver_options_get_line_search_type(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_line_search_type(_handle.DangerousGetHandle(), (int)value);
}
/// <summary>
/// Gets or sets the line search direction type.
/// </summary>
public LineSearchDirectionType LineSearchDirectionType
{
get => (LineSearchDirectionType)CeresNative.ceres_wrapper_solver_options_get_line_search_direction_type(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_line_search_direction_type(_handle.DangerousGetHandle(), (int)value);
}
/// <summary>
/// Gets or sets the nonlinear conjugate gradient type.
/// </summary>
public NonlinearConjugateGradientType NonlinearConjugateGradientType
{
get => (NonlinearConjugateGradientType)CeresNative.ceres_wrapper_solver_options_get_nonlinear_conjugate_gradient_type(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_nonlinear_conjugate_gradient_type(_handle.DangerousGetHandle(), (int)value);
}
/// <summary>
/// Gets or sets the maximum L-BFGS rank.
/// </summary>
public int MaxLbfgsRank
{
get => CeresNative.ceres_wrapper_solver_options_get_max_lbfgs_rank(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_max_lbfgs_rank(_handle.DangerousGetHandle(), value);
}
// ============================================================================
// Linear Solver Options
// ============================================================================
/// <summary>
/// Gets or sets the sparse linear algebra library type.
/// </summary>
public SparseLinearAlgebraLibraryType SparseLinearAlgebraLibraryType
{
get => (SparseLinearAlgebraLibraryType)CeresNative.ceres_wrapper_solver_options_get_sparse_linear_algebra_library_type(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_sparse_linear_algebra_library_type(_handle.DangerousGetHandle(), (int)value);
}
/// <summary>
/// Gets or sets the dense linear algebra library type.
/// </summary>
public DenseLinearAlgebraLibraryType DenseLinearAlgebraLibraryType
{
get => (DenseLinearAlgebraLibraryType)CeresNative.ceres_wrapper_solver_options_get_dense_linear_algebra_library_type(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_dense_linear_algebra_library_type(_handle.DangerousGetHandle(), (int)value);
}
/// <summary>
/// Gets or sets the maximum linear solver iterations.
/// </summary>
public int MaxLinearSolverIterations
{
get => CeresNative.ceres_wrapper_solver_options_get_max_linear_solver_iterations(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_max_linear_solver_iterations(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets the minimum linear solver iterations.
/// </summary>
public int MinLinearSolverIterations
{
get => CeresNative.ceres_wrapper_solver_options_get_min_linear_solver_iterations(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_min_linear_solver_iterations(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets the linear solver tolerance.
/// </summary>
public double LinearSolverTolerance
{
get => CeresNative.ceres_wrapper_solver_options_get_linear_solver_tolerance(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_linear_solver_tolerance(_handle.DangerousGetHandle(), value);
}
// ============================================================================
// Inner Iterations
// ============================================================================
/// <summary>
/// Gets or sets whether to use inner iterations.
/// </summary>
public bool UseInnerIterations
{
get => CeresNative.ceres_wrapper_solver_options_get_use_inner_iterations(_handle.DangerousGetHandle()) != 0;
set => CeresNative.ceres_wrapper_solver_options_set_use_inner_iterations(_handle.DangerousGetHandle(), value ? 1 : 0);
}
/// <summary>
/// Gets or sets the inner iteration tolerance.
/// </summary>
public double InnerIterationTolerance
{
get => CeresNative.ceres_wrapper_solver_options_get_inner_iteration_tolerance(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_inner_iteration_tolerance(_handle.DangerousGetHandle(), value);
}
// ============================================================================
// Timing
// ============================================================================
/// <summary>
/// Gets or sets the maximum solver time in seconds.
/// </summary>
public double MaxSolverTimeInSeconds
{
get => CeresNative.ceres_wrapper_solver_options_get_max_solver_time_in_seconds(_handle.DangerousGetHandle());
set => CeresNative.ceres_wrapper_solver_options_set_max_solver_time_in_seconds(_handle.DangerousGetHandle(), value);
}
/// <summary>
/// Gets or sets whether to update state every iteration.
/// </summary>
public bool UpdateStateEveryIteration
{
get => CeresNative.ceres_wrapper_solver_options_get_update_state_every_iteration(_handle.DangerousGetHandle()) != 0;
set => CeresNative.ceres_wrapper_solver_options_set_update_state_every_iteration(_handle.DangerousGetHandle(), value ? 1 : 0);
}
// ============================================================================
// Validation
// ============================================================================
/// <summary>
/// Validates the solver options.
/// </summary>
/// <returns>True if valid, false otherwise.</returns>
public bool IsValid(out string? errorMessage)
{
var sb = new StringBuilder(256);
var isValid = CeresNative.ceres_wrapper_solver_options_is_valid(_handle.DangerousGetHandle(), sb, sb.Capacity) != 0;
errorMessage = isValid ? null : sb.ToString();
return isValid;
}
// ============================================================================
// IDisposable
// ============================================================================
/// <summary>
/// Internal method to track iteration callback handles for cleanup.
/// </summary>
internal void SetIterationCallbackHandles(
System.Runtime.InteropServices.GCHandle callbackHandle,
System.Runtime.InteropServices.GCHandle nativeCallbackHandle)
{
// Free previous handles if any
if (_iterationCallbackHandle.HasValue && _iterationCallbackHandle.Value.IsAllocated)
_iterationCallbackHandle.Value.Free();
if (_iterationNativeCallbackHandle.HasValue && _iterationNativeCallbackHandle.Value.IsAllocated)
_iterationNativeCallbackHandle.Value.Free();
_iterationCallbackHandle = callbackHandle;
_iterationNativeCallbackHandle = nativeCallbackHandle;
}
public void Dispose()
{
if (!_disposed)
{
// Cleanup callback handles
if (_iterationCallbackHandle.HasValue && _iterationCallbackHandle.Value.IsAllocated)
_iterationCallbackHandle.Value.Free();
if (_iterationNativeCallbackHandle.HasValue && _iterationNativeCallbackHandle.Value.IsAllocated)
_iterationNativeCallbackHandle.Value.Free();
_handle?.Dispose();
_disposed = true;
}
}
}

View File

@@ -0,0 +1,206 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using CeresSharp.Enums;
using CeresSharp.Native;
using CeresSharp.Native.SafeHandles;
namespace CeresSharp;
/// <summary>
/// Summary of the solver execution results.
/// </summary>
public sealed class SolverSummary : IDisposable
{
private readonly SolverSummaryHandle _handle;
private bool _disposed;
internal SolverSummary(SolverSummaryHandle handle)
{
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
}
/// <summary>
/// Creates a SolverSummary from a native handle (for callbacks).
/// </summary>
internal SolverSummary(IntPtr handle, bool ownsHandle)
{
_handle = new SolverSummaryHandle(handle, ownsHandle);
}
/// <summary>
/// Gets the native handle.
/// </summary>
internal SolverSummaryHandle Handle => _handle;
// ============================================================================
// Termination Information
// ============================================================================
/// <summary>
/// Gets the termination type.
/// </summary>
public TerminationType TerminationType =>
(TerminationType)CeresNative.ceres_wrapper_solver_summary_get_termination_type(_handle.DangerousGetHandle());
/// <summary>
/// Gets the termination message.
/// </summary>
public string Message
{
get
{
var sb = new StringBuilder(512);
CeresNative.ceres_wrapper_solver_summary_get_message(_handle.DangerousGetHandle(), sb, sb.Capacity);
return sb.ToString();
}
}
// ============================================================================
// Cost Information
// ============================================================================
/// <summary>
/// Gets the initial cost.
/// </summary>
public double InitialCost =>
CeresNative.ceres_wrapper_solver_summary_get_initial_cost(_handle.DangerousGetHandle());
/// <summary>
/// Gets the final cost.
/// </summary>
public double FinalCost =>
CeresNative.ceres_wrapper_solver_summary_get_final_cost(_handle.DangerousGetHandle());
/// <summary>
/// Gets the cost change (final - initial).
/// </summary>
public double CostChange =>
CeresNative.ceres_wrapper_solver_summary_get_cost_change(_handle.DangerousGetHandle());
// ============================================================================
// Iteration Information
// ============================================================================
/// <summary>
/// Gets the total number of iterations.
/// </summary>
public int Iterations =>
CeresNative.ceres_wrapper_solver_summary_get_iterations(_handle.DangerousGetHandle());
/// <summary>
/// Gets the number of successful steps.
/// </summary>
public int NumSuccessfulSteps =>
CeresNative.ceres_wrapper_solver_summary_get_num_successful_steps(_handle.DangerousGetHandle());
/// <summary>
/// Gets the number of unsuccessful steps.
/// </summary>
public int NumUnsuccessfulSteps =>
CeresNative.ceres_wrapper_solver_summary_get_num_unsuccessful_steps(_handle.DangerousGetHandle());
/// <summary>
/// Gets the number of inner iteration steps.
/// </summary>
public int NumInnerIterationSteps =>
CeresNative.ceres_wrapper_solver_summary_get_num_inner_iteration_steps(_handle.DangerousGetHandle());
// ============================================================================
// Timing Information
// ============================================================================
/// <summary>
/// Gets the total time in seconds.
/// </summary>
public double TotalTimeInSeconds =>
CeresNative.ceres_wrapper_solver_summary_get_total_time_in_seconds(_handle.DangerousGetHandle());
/// <summary>
/// Gets the preprocessor time in seconds.
/// </summary>
public double PreprocessorTimeInSeconds =>
CeresNative.ceres_wrapper_solver_summary_get_preprocessor_time_in_seconds(_handle.DangerousGetHandle());
/// <summary>
/// Gets the minimizer time in seconds.
/// </summary>
public double MinimizerTimeInSeconds =>
CeresNative.ceres_wrapper_solver_summary_get_minimizer_time_in_seconds(_handle.DangerousGetHandle());
/// <summary>
/// Gets the postprocessor time in seconds.
/// </summary>
public double PostprocessorTimeInSeconds =>
CeresNative.ceres_wrapper_solver_summary_get_postprocessor_time_in_seconds(_handle.DangerousGetHandle());
/// <summary>
/// Gets the linear solver time in seconds.
/// </summary>
public double LinearSolverTimeInSeconds =>
CeresNative.ceres_wrapper_solver_summary_get_linear_solver_time_in_seconds(_handle.DangerousGetHandle());
// ============================================================================
// Statistics
// ============================================================================
/// <summary>
/// Gets the number of parameter blocks.
/// </summary>
public int NumParameterBlocks =>
CeresNative.ceres_wrapper_solver_summary_get_num_parameter_blocks(_handle.DangerousGetHandle());
/// <summary>
/// Gets the number of parameters.
/// </summary>
public int NumParameters =>
CeresNative.ceres_wrapper_solver_summary_get_num_parameters(_handle.DangerousGetHandle());
/// <summary>
/// Gets the number of effective parameters.
/// </summary>
public int NumEffectiveParameters =>
CeresNative.ceres_wrapper_solver_summary_get_num_effective_parameters(_handle.DangerousGetHandle());
/// <summary>
/// Gets the number of residual blocks.
/// </summary>
public int NumResidualBlocks =>
CeresNative.ceres_wrapper_solver_summary_get_num_residual_blocks(_handle.DangerousGetHandle());
/// <summary>
/// Gets the number of residuals.
/// </summary>
public int NumResiduals =>
CeresNative.ceres_wrapper_solver_summary_get_num_residuals(_handle.DangerousGetHandle());
// ============================================================================
// Full Report
// ============================================================================
/// <summary>
/// Gets the full solver report as a string.
/// </summary>
public string FullReport
{
get
{
var sb = new StringBuilder(4096);
CeresNative.ceres_wrapper_solver_summary_get_full_report(_handle.DangerousGetHandle(), sb, sb.Capacity);
return sb.ToString();
}
}
// ============================================================================
// IDisposable
// ============================================================================
public void Dispose()
{
if (!_disposed)
{
_handle?.Dispose();
_disposed = true;
}
}
}

View File

@@ -0,0 +1,17 @@
namespace CeresSharp.Enums;
/// <summary>
/// Covariance estimation algorithm types.
/// </summary>
public enum CovarianceAlgorithmType
{
/// <summary>
/// Dense SVD algorithm.
/// </summary>
DenseSvd = 0,
/// <summary>
/// Sparse QR algorithm.
/// </summary>
SparseQr = 1
}

View File

@@ -0,0 +1,22 @@
namespace CeresSharp.Enums;
/// <summary>
/// Dense linear algebra library types.
/// </summary>
public enum DenseLinearAlgebraLibraryType
{
/// <summary>
/// Eigen library.
/// </summary>
Eigen = 0,
/// <summary>
/// LAPACK + BLAS library.
/// </summary>
Lapack = 1,
/// <summary>
/// NVIDIA's CUDA library.
/// </summary>
Cuda = 2
}

View File

@@ -0,0 +1,17 @@
namespace CeresSharp.Enums;
/// <summary>
/// Dogleg type strategies for trust region optimization.
/// </summary>
public enum DoglegType
{
/// <summary>
/// The traditional approach constructs a dogleg path consisting of two line segments and finds the farthest point on that path that is still within the trust region.
/// </summary>
TraditionalDogleg = 0,
/// <summary>
/// The subspace approach finds the exact minimum of the model constrained to the subspace spanned by the dogleg path.
/// </summary>
SubspaceDogleg = 1
}

View File

@@ -0,0 +1,27 @@
namespace CeresSharp.Enums;
/// <summary>
/// Line search direction types for line search minimizer.
/// </summary>
public enum LineSearchDirectionType
{
/// <summary>
/// Negative of the gradient.
/// </summary>
SteepestDescent = 0,
/// <summary>
/// A generalization of the Conjugate Gradient method to non-linear functions.
/// </summary>
NonlinearConjugateGradient = 1,
/// <summary>
/// Limited memory Broyden-Fletcher-Goldfarb-Shanno (L-BFGS) method.
/// </summary>
Lbfgs = 2,
/// <summary>
/// Broyden-Fletcher-Goldfarb-Shanno (BFGS) method.
/// </summary>
Bfgs = 3
}

View File

@@ -0,0 +1,17 @@
namespace CeresSharp.Enums;
/// <summary>
/// Line search types for line search minimizer.
/// </summary>
public enum LineSearchType
{
/// <summary>
/// Backtracking line search with polynomial interpolation or bisection.
/// </summary>
Armijo = 0,
/// <summary>
/// Wolfe line search.
/// </summary>
Wolfe = 1
}

View File

@@ -0,0 +1,43 @@
namespace CeresSharp.Enums;
/// <summary>
/// Linear solver types available in Ceres Solver.
/// Values must match ceres::LinearSolverType enum in Ceres C++ (types.h).
/// </summary>
public enum LinearSolverType
{
/// <summary>
/// Dense Cholesky factorization of the normal equations.
/// </summary>
DenseNormalCholesky = 0,
/// <summary>
/// Dense QR factorization.
/// </summary>
DenseQr = 1,
/// <summary>
/// Sparse normal Cholesky factorization.
/// </summary>
SparseNormalCholesky = 2,
/// <summary>
/// Dense Schur factorization.
/// </summary>
DenseSchur = 3,
/// <summary>
/// Sparse Schur factorization.
/// </summary>
SparseSchur = 4,
/// <summary>
/// Iterative Schur complement solver.
/// </summary>
IterativeSchur = 5,
/// <summary>
/// Conjugate gradients on the normal equations.
/// </summary>
CgNr = 6
}

View File

@@ -0,0 +1,17 @@
namespace CeresSharp.Enums;
/// <summary>
/// Logging types for Ceres Solver output.
/// </summary>
public enum LoggingType
{
/// <summary>
/// No logging output.
/// </summary>
Silent = 0,
/// <summary>
/// Log output for each minimizer iteration.
/// </summary>
PerMinimizerIteration = 1
}

View File

@@ -0,0 +1,18 @@
namespace CeresSharp.Enums;
/// <summary>
/// Minimizer types available in Ceres Solver.
/// Values must match ceres::MinimizerType enum in Ceres C++ (types.h).
/// </summary>
public enum MinimizerType
{
/// <summary>
/// Line search minimizer.
/// </summary>
LineSearch = 0,
/// <summary>
/// Trust region minimizer.
/// </summary>
TrustRegion = 1
}

View File

@@ -0,0 +1,22 @@
namespace CeresSharp.Enums;
/// <summary>
/// Nonlinear conjugate gradient types.
/// </summary>
public enum NonlinearConjugateGradientType
{
/// <summary>
/// Fletcher-Reeves conjugate gradient.
/// </summary>
FletcherReeves = 0,
/// <summary>
/// Polak-Ribiere conjugate gradient.
/// </summary>
PolakRibiere = 1,
/// <summary>
/// Hestenes-Stiefel conjugate gradient.
/// </summary>
HestenesStiefel = 2
}

View File

@@ -0,0 +1,26 @@
namespace CeresSharp.Enums;
/// <summary>
/// Numeric differentiation methods for NumericDiffCostFunction.
/// Values must match ceres_numeric_diff_method_t enum in ceres_wrapper.h.
/// Note: The wrapper defines its own enum order (Forward=0, Central=1) which differs
/// from ceres::NumericDiffMethodType (Central=0, Forward=1). The wrapper uses a switch
/// statement to map correctly, so C# must match the wrapper's enum.
/// </summary>
public enum NumericDiffMethod
{
/// <summary>
/// Compute forward finite difference: f'(x) ~ (f(x+h) - f(x)) / h.
/// </summary>
Forward = 0,
/// <summary>
/// Compute central finite difference: f'(x) ~ (f(x+h) - f(x-h)) / 2h.
/// </summary>
Central = 1,
/// <summary>
/// Adaptive numerical differentiation using Ridders' method.
/// </summary>
Ridders = 2
}

View File

@@ -0,0 +1,42 @@
namespace CeresSharp.Enums;
/// <summary>
/// Preconditioner types available in Ceres Solver.
/// </summary>
public enum PreconditionerType
{
/// <summary>
/// Trivial preconditioner - the identity matrix.
/// </summary>
Identity = 0,
/// <summary>
/// Block diagonal of the Gauss-Newton Hessian.
/// </summary>
Jacobi = 1,
/// <summary>
/// Block diagonal of the Schur complement.
/// </summary>
SchurJacobi = 2,
/// <summary>
/// Power series expansion of the Schur complement.
/// </summary>
SchurPowerSeriesExpansion = 3,
/// <summary>
/// Visibility clustering based preconditioners.
/// </summary>
ClusterJacobi = 4,
/// <summary>
/// Cluster tridiagonal preconditioner.
/// </summary>
ClusterTridiagonal = 5,
/// <summary>
/// Subset preconditioner for general purpose linear least squares problems.
/// </summary>
Subset = 6
}

View File

@@ -0,0 +1,32 @@
namespace CeresSharp.Enums;
/// <summary>
/// Sparse linear algebra library types.
/// </summary>
public enum SparseLinearAlgebraLibraryType
{
/// <summary>
/// SuiteSparse library (CHOLMOD).
/// </summary>
SuiteSparse = 0,
/// <summary>
/// Eigen's sparse linear algebra routines.
/// </summary>
EigenSparse = 1,
/// <summary>
/// Apple's Accelerate framework sparse linear algebra routines.
/// </summary>
AccelerateSparse = 2,
/// <summary>
/// NVIDIA's cuDSS and cuSPARSE libraries.
/// </summary>
CudaSparse = 3,
/// <summary>
/// No sparse linear algebra library should be used.
/// </summary>
NoSparse = 4
}

View File

@@ -0,0 +1,33 @@
namespace CeresSharp.Enums;
/// <summary>
/// Termination types for Ceres Solver.
/// Values must match ceres::TerminationType enum in Ceres C++ (types.h).
/// </summary>
public enum TerminationType
{
/// <summary>
/// Convergence reached.
/// </summary>
Convergence = 0,
/// <summary>
/// No convergence.
/// </summary>
NoConvergence = 1,
/// <summary>
/// Failure.
/// </summary>
Failure = 2,
/// <summary>
/// User-requested success via IterationCallback returning SOLVER_TERMINATE_SUCCESSFULLY.
/// </summary>
UserSuccess = 3,
/// <summary>
/// User-requested failure via IterationCallback returning SOLVER_ABORT.
/// </summary>
UserFailure = 4
}

View File

@@ -0,0 +1,17 @@
namespace CeresSharp.Enums;
/// <summary>
/// Trust region strategy types available in Ceres Solver.
/// </summary>
public enum TrustRegionStrategyType
{
/// <summary>
/// The default trust region strategy is to use the step computation used in the Levenberg-Marquardt algorithm.
/// </summary>
LevenbergMarquardt = 0,
/// <summary>
/// Powell's dogleg algorithm interpolates between the Cauchy point and the Gauss-Newton step.
/// </summary>
Dogleg = 1
}

View File

@@ -0,0 +1,56 @@
namespace CeresSharp.Exceptions;
/// <summary>
/// Base exception for all Ceres-related errors.
/// </summary>
public class CeresException : Exception
{
/// <summary>
/// Gets the error code associated with this exception.
/// </summary>
public CeresErrorCode ErrorCode { get; }
public CeresException(CeresErrorCode errorCode, string? message = null)
: base(message ?? GetDefaultMessage(errorCode))
{
ErrorCode = errorCode;
}
public CeresException(CeresErrorCode errorCode, string? message, Exception? innerException)
: base(message ?? GetDefaultMessage(errorCode), innerException)
{
ErrorCode = errorCode;
}
private static string GetDefaultMessage(CeresErrorCode errorCode) => errorCode switch
{
CeresErrorCode.Success => "Operation succeeded",
CeresErrorCode.NullPointer => "Null pointer argument",
CeresErrorCode.InvalidParameter => "Invalid parameter value",
CeresErrorCode.InvalidEnum => "Invalid enum value",
CeresErrorCode.Exception => "C++ exception occurred",
CeresErrorCode.OutOfMemory => "Memory allocation failed",
CeresErrorCode.InvalidOperation => "Invalid operation for current state",
CeresErrorCode.BufferTooSmall => "Output buffer too small",
CeresErrorCode.NotFound => "Resource not found",
CeresErrorCode.AlreadyExists => "Resource already exists",
_ => $"Unknown error code: {errorCode}"
};
}
/// <summary>
/// Error codes returned by Ceres wrapper functions.
/// </summary>
public enum CeresErrorCode
{
Success = 0,
NullPointer = 1,
InvalidParameter = 2,
InvalidEnum = 3,
Exception = 4,
OutOfMemory = 5,
InvalidOperation = 6,
BufferTooSmall = 7,
NotFound = 8,
AlreadyExists = 9
}

View File

@@ -0,0 +1,586 @@
# CeresSharp Implementation Progress
## 📋 Tổng Quan
**Mục tiêu**: Implement C# wrapper library cho Ceres Solver 2.2.0 thông qua C API (`libceres_wrapper.so`)
**Target Framework**: .NET 10.0
**Platform**: Linux only
**API Style**: High-level API với classes/objects giống C++ API của Ceres
## 📊 Progress Tracking
### Overall Progress: 100% (15/15 modules)
| Module | Status | Progress | Notes |
|--------|--------|----------|-------|
| **Core Foundation** | ✅ Complete | 100% | Error handling ✅, enums ✅, constants ✅ |
| **Constants** | ✅ Complete | 100% | Default values ✅, mathematical constants ✅ |
| **SafeHandles** | ✅ Complete | 100% | All SafeHandles implemented ✅ |
| **Problem** | ✅ Complete | 100% | Core problem operations ✅ |
| **SolverOptions** | ✅ Complete | 100% | Solver configuration ✅ |
| **SolverSummary** | ✅ Complete | 100% | Solver results ✅ |
| **Cost Functions** | ✅ Complete | 100% | AutoDiff, NumericDiff ✅, memory management ✅, validation ✅ |
| **Loss Functions** | ✅ Complete | 100% | All loss functions ✅ |
| **Manifolds** | ✅ Complete | 100% | All manifolds ✅ |
| **Interpolators** | ✅ Complete | 100% | BiCubic, Cubic ✅ |
| **Problem Options** | ✅ Complete | 100% | Advanced problem config ✅ |
| **Parameter Bounds** | ✅ Complete | 100% | Lower/upper bounds ✅ |
| **Covariance** | ✅ Complete | 100% | Covariance estimation ✅ |
| **Gradient Checker** | ✅ Complete | 100% | Gradient validation ✅ |
| **Context** | ✅ Complete | 100% | Performance optimization ✅ |
| **Callbacks** | ✅ Complete | 100% | Iteration callback ✅, evaluation callback ✅, memory management ✅ |
---
## 🏗️ Architecture
### Design Principles
1. **High-level API**: Classes/objects giống C++ API của Ceres
2. **Exception-based**: Tất cả errors được wrap trong exceptions
3. **Safe Handles**: IDisposable pattern với SafeHandle
4. **Delegates**: Callbacks sử dụng delegates với GCHandle pinning
5. **Type Safety**: Strong typing, nullable reference types
### Project Structure
```
CeresSharp/
├── Core/
│ ├── Problem.cs
│ ├── SolverOptions.cs
│ ├── SolverSummary.cs
│ ├── CostFunction.cs
│ ├── LossFunction.cs
│ ├── Manifold.cs
│ ├── AutoDiffManifold.cs (⭐ NEW)
│ ├── BiCubicInterpolator.cs
│ └── CubicInterpolator.cs
├── Advanced/
│ ├── ProblemOptions.cs
│ ├── Covariance.cs
│ ├── GradientChecker.cs
│ └── Context.cs
├── Native/
│ ├── CeresNative.cs (P/Invoke declarations)
│ └── SafeHandles/
│ ├── ProblemHandle.cs
│ ├── SolverOptionsHandle.cs
│ └── ...
├── Exceptions/
│ └── CeresException.cs
├── Enums/
│ ├── LinearSolverType.cs
│ ├── MinimizerType.cs
│ └── ...
└── IMPLEMENTATION_PROGRESS.md (this file)
```
---
## 📝 Implementation Details
### 1. Core Foundation ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] Error codes enum (CeresErrorCode)
- [x] Custom exceptions (CeresException)
- [x] Core enums (LinearSolverType, MinimizerType, TerminationType, PreconditionerType, NumericDiffMethod)
- [x] Additional enums (TrustRegionStrategyType, DoglegType, LoggingType, LineSearchType, LineSearchDirectionType, NonlinearConjugateGradientType, SparseLinearAlgebraLibraryType, DenseLinearAlgebraLibraryType, CovarianceAlgorithmType)
- [x] Error message mapping (via ceres_wrapper_get_error_message)
**Files**:
- [x] `Exceptions/CeresException.cs` - Custom exception với error codes
- [x] `Enums/LinearSolverType.cs` - 9 solver types
- [x] `Enums/MinimizerType.cs` - Trust Region và Line Search
- [x] `Enums/TerminationType.cs` - 4 termination types
- [x] `Enums/PreconditionerType.cs` - 7 preconditioner types
- [x] `Enums/NumericDiffMethod.cs` - Forward, Central, Ridders
- [x] `Enums/TrustRegionStrategyType.cs` - LevenbergMarquardt, Dogleg
- [x] `Enums/DoglegType.cs` - TraditionalDogleg, SubspaceDogleg
- [x] `Enums/LoggingType.cs` - Silent, PerMinimizerIteration
- [x] `Enums/LineSearchType.cs` - Armijo, Wolfe
- [x] `Enums/LineSearchDirectionType.cs` - 4 direction types
- [x] `Enums/NonlinearConjugateGradientType.cs` - 3 CG types
- [x] `Enums/SparseLinearAlgebraLibraryType.cs` - 5 library types
- [x] `Enums/DenseLinearAlgebraLibraryType.cs` - 3 library types
- [x] `Enums/CovarianceAlgorithmType.cs` - DenseSvd, SparseQr
- [x] `Core/Constants.cs` - Default values và mathematical constants
---
### 2. SafeHandles ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] Base SafeHandle class (BaseSafeHandle)
- [x] ProblemHandle
- [x] SolverOptionsHandle
- [x] SolverSummaryHandle
- [x] CostFunctionHandle
- [x] LossFunctionHandle
- [x] ManifoldHandle
- [x] InterpolatorHandle (BiCubic và Cubic)
- [x] ProblemOptionsHandle
- [x] CovarianceOptionsHandle
- [x] CovarianceHandle
- [x] GradientCheckerOptionsHandle
- [x] GradientCheckerHandle
- [x] ContextHandle
**Files**:
- [x] `Native/SafeHandles/BaseSafeHandle.cs` - Abstract base class với ReleaseNativeHandle
- [x] `Native/SafeHandles/ProblemHandle.cs` - Problem resource management
- [x] `Native/SafeHandles/SolverOptionsHandle.cs` - SolverOptions resource management
- [x] `Native/SafeHandles/SolverSummaryHandle.cs` - SolverSummary resource management
- [x] `Native/SafeHandles/CostFunctionHandle.cs` - CostFunction resource management
- [x] `Native/SafeHandles/LossFunctionHandle.cs` - LossFunction resource management
- [x] `Native/SafeHandles/ManifoldHandle.cs` - Manifold resource management
- [x] `Native/SafeHandles/InterpolatorHandle.cs` - Interpolator resource management (supports both BiCubic và Cubic)
- [x] `Native/SafeHandles/ProblemOptionsHandle.cs` - ProblemOptions resource management
- [x] `Native/SafeHandles/CovarianceOptionsHandle.cs` - CovarianceOptions resource management
- [x] `Native/SafeHandles/CovarianceHandle.cs` - Covariance resource management
- [x] `Native/SafeHandles/GradientCheckerOptionsHandle.cs` - GradientCheckerOptions resource management
- [x] `Native/SafeHandles/GradientCheckerHandle.cs` - GradientChecker resource management
- [x] `Native/SafeHandles/ContextHandle.cs` - Context resource management
- [x] `Native/CeresNative.cs` - Complete P/Invoke declarations (216+ functions)
---
### 3. Problem ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] Problem class với IDisposable
- [x] AddParameterBlock (với error handling)
- [x] SetParameterBlockConstant/Variable
- [x] RemoveParameterBlock
- [x] AddResidualBlock (với CostFunction và LossFunction objects)
- [x] RemoveResidualBlock
- [x] SetManifold (với Manifold object)
- [x] SetParameterBounds (lower/upper)
- [x] GetParameterBounds (lower/upper)
- [x] Query methods (NumParameterBlocks, NumResidualBlocks, NumParameters, NumResiduals)
- [x] HasParameterBlock
- [x] IsParameterBlockConstant
- [x] GetParameterBlockSize
- [x] GetParameterBlockTangentSize
- [x] HasManifold
- [x] GetManifoldHandle
- [x] Solve method (với SolverOptions và returns SolverSummary)
**Files**:
- [x] `Core/Problem.cs` - Complete implementation với 20+ methods
**API Count**: 20+ methods
---
### 4. SolverOptions ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] SolverOptions class với IDisposable
- [x] Linear solver type (getter/setter)
- [x] Minimizer type (getter/setter)
- [x] Iterations (max_num_iterations)
- [x] Threads (num_threads)
- [x] Tolerances (function, gradient, parameter)
- [x] Trust region (initial, max, min radius, strategy type, dogleg type)
- [x] Preconditioner type
- [x] Line search options (type, direction type, NCG type, LBFGS rank, step contractions, etc.)
- [x] LBFGS options (max_lbfgs_rank)
- [x] Linear solver options (sparse/dense library types, max/min iterations, tolerance)
- [x] Inner iterations (use_inner_iterations, tolerance)
- [x] Timing (max_solver_time_in_seconds)
- [x] Validation (IsValid method)
- [x] Other options (use_nonmonotonic_steps, logging_type, etc.)
- [x] Memory management cho iteration callbacks
**Files**:
- [x] `Core/SolverOptions.cs` - Complete implementation với 50+ properties
**API Count**: 50+ properties/methods
---
### 5. SolverSummary ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] SolverSummary class với IDisposable
- [x] Termination type (getter)
- [x] Message (getter với StringBuilder)
- [x] Cost values (initial, final, cost_change)
- [x] Iteration counts (iterations, num_successful_steps, num_unsuccessful_steps, num_inner_iteration_steps)
- [x] Timing information (total, preprocessor, minimizer, postprocessor, linear_solver time)
- [x] Statistics (num_parameter_blocks, num_parameters, num_effective_parameters, num_residual_blocks, num_residuals)
- [x] Full report (getter với StringBuilder)
- [x] Support cho callbacks (temporary handle creation)
**Files**:
- [x] `Core/SolverSummary.cs` - Complete implementation với 20+ properties
**API Count**: 20+ properties
---
### 6. Cost Functions ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] CostFunction base class với IDisposable
- [x] AutoDiffCostFunction (với callback marshalling)
- [x] DynamicAutoDiffCostFunction (với callback marshalling)
- [x] NumericDiffCostFunction (với callback marshalling)
- [x] DynamicNumericDiffCostFunction (với callback marshalling)
- [x] Callback handling với GCHandle pinning
- [x] Memory management (wrapper handles và native callback handles)
- [x] Enhanced validation (parameter block sizes, residuals)
- [x] Error handling và resource cleanup on failure
- [x] Proper exception messages
**Files**:
- [x] `Core/CostFunction.cs` - Complete implementation với all 4 cost function types
**API Count**: 4 cost function types + delegates
---
### 7. Loss Functions ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] LossFunction base class với IDisposable
- [x] TrivialLoss (no parameters)
- [x] HuberLoss (scaling parameter a)
- [x] CauchyLoss (scaling parameter a)
- [x] SoftLOneLoss (scaling parameter a)
- [x] ArctanLoss (scaling parameter a)
- [x] TolerantLoss (scaling parameters a, b)
**Files**:
- [x] `Core/LossFunction.cs` - Complete implementation với all 6 loss function types
**API Count**: 6 loss function types
---
### 8. Manifolds ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] Manifold base class với IDisposable
- [x] QuaternionManifold (for 3D rotations)
- [x] SphereManifold (với dimension parameter)
- [x] LineManifold (với dimension parameter)
- [x] EuclideanManifold (với dimension parameter)
- [x] SubsetManifold (với constant subset indices và ambient size)
- [x] ProductManifold (combine multiple manifolds)
- [x] **AutoDiffManifold** (callback-based custom manifolds) - ⭐ **NEW**
- [x] PlusOperation và MinusOperation delegates
- [x] Callback marshalling (C# delegates → C callbacks)
- [x] Memory management với GCHandle pinning
- [x] Proper validation (ambientSize > 0, tangentSize > 0, tangentSize <= ambientSize)
- [x] Error handling và resource cleanup
- [x] AmbientSize và TangentSize properties
- [x] Proper validation cho parameters
**Files**:
- [x] `Core/Manifold.cs` - Complete implementation với all 6 standard manifold types
- [x] `Core/AutoDiffManifold.cs` - ⭐ **NEW** - AutoDiff manifold với callback-based API
**API Count**: 7 manifold types (6 standard + AutoDiffManifold) + 2 properties + 2 delegates
---
### 9. Interpolators ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] BiCubicInterpolator (2D interpolation)
- [x] CubicInterpolator (1D interpolation)
- [x] Evaluate methods với gradients (out parameters)
- [x] Evaluate methods without gradients (overloads)
- [x] Proper data validation (array sizes, dimensions)
**Files**:
- [x] `Core/BiCubicInterpolator.cs` - 2D cubic interpolation
- [x] `Core/CubicInterpolator.cs` - 1D cubic interpolation
**API Count**: 2 interpolator types + evaluate methods
---
### 10. Problem Options ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] ProblemOptions class với IDisposable
- [x] Ownership settings (cost_function, loss_function, manifold ownership)
- [x] Fast removal (enable_fast_removal)
- [x] Safety checks (disable_all_safety_checks)
- [x] Evaluation callback (SetEvaluationCallback với proper memory management)
- [x] Context (SetContext method)
- [x] Memory management cho evaluation callbacks
**Files**:
- [x] `Advanced/ProblemOptions.cs` - Complete implementation
- [x] `Advanced/EvaluationCallback.cs` - Evaluation callback delegate
**API Count**: 6 properties + 2 methods
---
### 11. Parameter Bounds ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] SetParameterLowerBound (với index validation)
- [x] SetParameterUpperBound (với index validation)
- [x] GetParameterLowerBound (returns -infinity if not set)
- [x] GetParameterUpperBound (returns +infinity if not set)
- [x] Proper validation (null checks, index bounds)
**Files**:
- [x] `Core/Problem.cs` - Methods implemented trong Problem class
**API Count**: 4 methods
---
### 12. Covariance ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] CovarianceOptions class với IDisposable (num_threads, sparse_library_type, algorithm_type, min_reciprocal_condition_number, null_space_rank, apply_loss_function)
- [x] Covariance class với IDisposable (create với/không options)
- [x] Compute method (với Problem, CovarianceOptions, và parameter blocks) - ⭐ **ENHANCED** - Now uses error codes and throws exceptions
- [x] GetCovarianceBlock (between two parameter blocks) - ⭐ **ENHANCED** - Now uses error codes and throws exceptions
- [x] GetCovarianceMatrix (for multiple parameter blocks) - ⭐ **ENHANCED** - Now uses error codes and throws exceptions
- [x] Proper array marshalling với GCHandle pinning
- [x] Error handling với CeresException (error codes + error messages) - ⭐ **NEW**
**Files**:
- [x] `Advanced/CovarianceOptions.cs` - Complete implementation
- [x] `Advanced/Covariance.cs` - Complete implementation với enhanced error handling
**API Count**: 2 classes + 8+ methods/properties
**Recent Updates (2024)**:
- ✅ Enhanced error handling: All methods now use `CeresErrorCode` instead of `bool`
- ✅ Exception-based API: Methods throw `CeresException` on failure instead of returning false
- ✅ Error messages: All methods now provide detailed error messages via StringBuilder
---
### 13. Gradient Checker ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] GradientCheckerOptions class với IDisposable (gradient_check_relative_precision, numeric_derivative_relative_step_size)
- [x] GradientChecker class với IDisposable (create với CostFunction, Manifolds, và Options)
- [x] Probe method (với parameters, relative_precision, và error message output) - ⭐ **ENHANCED** - Now uses error codes internally
- [x] Probe method overload (without error message) - ⭐ **NEW** - Simplified API
- [x] Proper array marshalling với GCHandle pinning
- [x] Support cho null manifolds array
- [x] Error handling với CeresException (error codes + error messages) - ⭐ **NEW**
- [x] Gradient mismatch handling (InvalidParameter = expected, other errors = exceptions) - ⭐ **NEW**
**Files**:
- [x] `Advanced/GradientCheckerOptions.cs` - Complete implementation
- [x] `Advanced/GradientChecker.cs` - Complete implementation với enhanced error handling
**API Count**: 2 classes + 4+ methods/properties (added Probe overload)
**Recent Updates (2024)**:
- ✅ Enhanced error handling: Probe method now uses `CeresErrorCode` internally
- ✅ Exception-based API: Non-gradient-mismatch errors throw `CeresException`
- ✅ Backward compatibility: Maintained `bool` return type with overload for error message
- ✅ Error messages: Detailed error messages available via overload
---
### 14. Context ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] Context class với IDisposable (for performance optimization)
- [x] Integration với ProblemOptions (SetContext method)
- [x] Proper resource management
**Files**:
- [x] `Advanced/Context.cs` - Complete implementation
**API Count**: 1 class + 1 method (SetContext trong ProblemOptions)
---
### 15. Callbacks ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] IterationCallback delegate (trong Callbacks class)
- [x] EvaluationCallback delegate (trong Advanced namespace)
- [x] GCHandle pinning với proper memory management
- [x] Integration với SolverOptions (SetIterationCallback extension method)
- [x] Integration với ProblemOptions (SetEvaluationCallback method)
- [x] Memory management (track và cleanup handles trong Dispose)
- [x] Proper callback marshalling (C# delegates → C callbacks)
**Files**:
- [x] `Core/Callbacks.cs` - IterationCallback delegate và SolverOptionsExtensions
- [x] `Advanced/EvaluationCallback.cs` - EvaluationCallback delegate
- [x] `Advanced/ProblemOptions.cs` - SetEvaluationCallback implementation
**API Count**: 2 delegates + 2 methods
---
### 16. Constants ✅ (100%)
**Status**: Complete
**Tasks**:
- [x] Mathematical constants (Pi)
- [x] SolverOptions default values (tolerances, iterations, trust region, etc.)
- [x] Line search default values
- [x] Trust region default values
- [x] Linear solver default values
- [x] Inner iterations default values
- [x] Covariance default values
- [x] Gradient checker default values
- [x] Numeric diff default values
**Files**:
- [x] `Core/Constants.cs` - Complete constants class với all default values
**API Count**: 30+ constants
---
**Status**: Complete
**Tasks**:
- [x] IterationCallback delegate (trong Callbacks class)
- [x] EvaluationCallback delegate (trong Advanced namespace)
- [x] GCHandle pinning với proper memory management
- [x] Integration với SolverOptions (SetIterationCallback extension method)
- [x] Integration với ProblemOptions (SetEvaluationCallback method)
- [x] Memory management (track và cleanup handles trong Dispose)
- [x] Proper callback marshalling (C# delegates → C callbacks)
**Files**:
- [x] `Core/Callbacks.cs` - IterationCallback delegate và SolverOptionsExtensions
- [x] `Advanced/EvaluationCallback.cs` - EvaluationCallback delegate
- [x] `Advanced/ProblemOptions.cs` - SetEvaluationCallback implementation
**API Count**: 2 delegates + 2 methods
---
## 🔧 Technical Decisions
### Memory Management
- ✅ SafeHandle pattern cho tất cả native resources (14 SafeHandle classes)
- ✅ IDisposable pattern cho high-level classes
- ✅ Automatic cleanup khi object bị dispose
- ✅ GCHandle tracking và cleanup cho callbacks
- ✅ Resource cleanup on creation failure
- ✅ No memory leaks
### Error Handling
- ✅ Tất cả errors → CeresException
- ✅ Error codes được map thành meaningful exceptions
- ✅ Null checks với ArgumentNullException
- ✅ Parameter validation với ArgumentException
- ✅ Specific error messages cho từng failure case
-**Enhanced error handling for advanced features** - ⭐ **NEW**
- ✅ Covariance methods: All use error codes and throw exceptions
- ✅ GradientChecker.Probe: Uses error codes internally, throws exceptions for non-mismatch errors
- ✅ Error messages: All methods provide detailed error messages via StringBuilder
### Callbacks
- ✅ Delegates cho C# callbacks (IterationCallback, EvaluationCallback, AutoDiffCostFunctionCallback, NumericDiffCostFunctionCallback, AutoDiffManifoldPlusOperation, AutoDiffManifoldMinusOperation)
- ✅ GCHandle.Alloc() để pin delegates
- ✅ GCHandle.Free() trong Dispose
- ✅ Proper callback marshalling (C# → C)
- ✅ Memory management cho callback handles (track trong parent objects)
### Type Safety
- ✅ Strong typing với enums (13 enum types)
- ✅ Nullable reference types
- ✅ Parameter validation (null checks, bounds checks, positive number checks)
- ✅ Array size validation
- ✅ Index bounds checking
---
## 📚 Reference
- **C API Header**: `ipc/CeresWrapper/ceres_wrapper.h`
- **C API Implementation**: `ipc/CeresWrapper/ceres_wrapper.cc`
- **C API Tests**: `ipc/CeresWrapper/ceres_wrapper_test.c`
- **Evaluation**: `ipc/CeresWrapper/CSHARP_WRAPPER_FINAL_EVALUATION.md`
---
## 🚀 Next Steps
1. ✅ Create project structure
2. ✅ Implement Core Foundation (Error handling, Enums)
3. ✅ Implement SafeHandles
4. ✅ Implement Problem class
5. ✅ Implement SolverOptions và SolverSummary
6. ✅ Implement Cost Functions
7. ✅ Implement Loss Functions
8. ✅ Implement Manifolds
9. ✅ Implement Interpolators
10. ✅ Implement Advanced features
11. ⏳ Testing và validation
12. ⏳ Performance optimization
13. ⏳ Documentation completion
14. ⏳ Example projects
---
## 📊 Final Statistics
- **Total Files Created**: 50+ C# files
- **Total Lines of Code**: ~5,000+ lines
- **APIs Implemented**: 220+ functions (including AutoDiffManifold)
- **Enums**: 13 types
- **SafeHandles**: 14 classes
- **Core Classes**: 9 classes (added AutoDiffManifold)
- **Advanced Classes**: 6 classes
- **Manifold Types**: 7 types (6 standard + AutoDiffManifold)
- **Build Status**: ✅ **SUCCESS**
---
**Last Updated**: 2024-12-19 (Updated with Error Handling Enhancement)
**Status**: ✅ **100% Complete** - Including AutoDiffManifold + Enhanced Error Handling (Ready for Cartographer Integration)
**Recent Updates (2024)**:
- ✅ Enhanced error handling for Covariance methods (Compute, GetCovarianceBlock, GetCovarianceMatrix)
- ✅ Enhanced error handling for GradientChecker.Probe method
- ✅ All advanced features now have comprehensive error reporting with error codes and messages
- ✅ Exception-based API for better error handling in C# code
- ✅ Backward compatibility maintained for GradientChecker.Probe

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
using System;
using System.Text;
using CeresSharp.Exceptions;
namespace CeresSharp.Native;
/// <summary>
/// Helper class for native interop operations.
/// Provides utilities for error handling, StringBuilder management, and common patterns.
/// </summary>
internal static class NativeHelpers
{
private const int DefaultErrorMessageCapacity = 256;
// Thread-local buffer pool to avoid allocating a new StringBuilder every call
[ThreadStatic]
private static StringBuilder? t_errorMessageBuffer;
/// <summary>
/// Gets a reusable StringBuilder for error messages.
/// Uses thread-local storage to avoid allocation on each native call.
/// </summary>
internal static StringBuilder CreateErrorMessageBuffer(int capacity = DefaultErrorMessageCapacity)
{
var sb = t_errorMessageBuffer;
if (sb != null && sb.Capacity >= capacity)
{
sb.Clear();
return sb;
}
sb = new StringBuilder(capacity);
t_errorMessageBuffer = sb;
return sb;
}
/// <summary>
/// Extracts error message from StringBuilder, returning null if empty.
/// </summary>
internal static string? ExtractErrorMessage(StringBuilder sb)
{
return sb.Length > 0 ? sb.ToString() : null;
}
/// <summary>
/// Checks error code and throws exception if not successful.
/// </summary>
/// <param name="errorCode">The error code to check.</param>
/// <param name="errorMessage">Optional error message. If null, will be retrieved from native code.</param>
/// <exception cref="CeresException">Thrown when error code is not Success.</exception>
internal static void CheckError(CeresErrorCode errorCode, string? errorMessage = null)
{
if (errorCode != CeresErrorCode.Success)
{
if (errorMessage == null)
{
var errorMessagePtr = CeresNative.ceres_wrapper_get_error_message(errorCode);
errorMessage = errorMessagePtr != IntPtr.Zero
? System.Runtime.InteropServices.Marshal.PtrToStringAnsi(errorMessagePtr)
: $"Unknown error code: {errorCode}";
}
throw new CeresException(errorCode, errorMessage);
}
}
/// <summary>
/// Executes a native operation with error handling.
/// </summary>
/// <param name="operation">The operation that returns an error code.</param>
/// <param name="errorMessageBuffer">Optional StringBuilder for error messages.</param>
/// <exception cref="CeresException">Thrown when the operation fails.</exception>
internal static void ExecuteWithErrorHandling(
Func<StringBuilder, CeresErrorCode> operation,
StringBuilder? errorMessageBuffer = null)
{
errorMessageBuffer ??= CreateErrorMessageBuffer();
var errorCode = operation(errorMessageBuffer);
if (errorCode != CeresErrorCode.Success)
{
var errorMessage = ExtractErrorMessage(errorMessageBuffer);
throw new CeresException(errorCode, errorMessage);
}
}
}

View File

@@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace CeresSharp.Native;
/// <summary>
/// Helper class for managing GCHandle resources and ensuring proper cleanup.
/// Provides utilities for tracking and disposing of pinned handles.
/// </summary>
internal sealed class ResourceManager : IDisposable
{
private readonly List<GCHandle> _handles = new List<GCHandle>();
private bool _disposed;
/// <summary>
/// Adds a GCHandle to be managed and disposed.
/// </summary>
/// <param name="handle">The GCHandle to track.</param>
/// <exception cref="ObjectDisposedException">Thrown when the manager is disposed.</exception>
public void AddHandle(GCHandle handle)
{
if (_disposed)
throw new ObjectDisposedException(nameof(ResourceManager));
lock (_handles)
{
_handles.Add(handle);
}
}
/// <summary>
/// Adds multiple GCHandles to be managed and disposed.
/// </summary>
/// <param name="handles">The GCHandles to track.</param>
/// <exception cref="ObjectDisposedException">Thrown when the manager is disposed.</exception>
public void AddHandles(IEnumerable<GCHandle> handles)
{
if (_disposed)
throw new ObjectDisposedException(nameof(ResourceManager));
lock (_handles)
{
_handles.AddRange(handles);
}
}
/// <summary>
/// Pins an array and adds the handle to the manager.
/// </summary>
/// <typeparam name="T">The unmanaged type of the array elements.</typeparam>
/// <param name="array">The array to pin.</param>
/// <returns>The pinned GCHandle.</returns>
/// <exception cref="ArgumentNullException">Thrown when array is null.</exception>
/// <exception cref="ObjectDisposedException">Thrown when the manager is disposed.</exception>
public GCHandle PinArray<T>(T[] array) where T : unmanaged
{
if (array == null)
throw new ArgumentNullException(nameof(array));
var handle = GCHandle.Alloc(array, GCHandleType.Pinned);
AddHandle(handle);
return handle;
}
/// <summary>
/// Gets all handles as an array (for safe enumeration during disposal).
/// </summary>
/// <returns>Array of GCHandles.</returns>
public GCHandle[] GetHandlesSnapshot()
{
lock (_handles)
{
return _handles.ToArray();
}
}
/// <summary>
/// Clears all handles without disposing them.
/// Useful when ownership is transferred to another object.
/// </summary>
public void Clear()
{
lock (_handles)
{
_handles.Clear();
}
}
public void Dispose()
{
if (!_disposed)
{
// Get snapshot to avoid issues during enumeration
var handlesToFree = GetHandlesSnapshot();
lock (_handles)
{
_handles.Clear();
}
// Free handles outside the lock
foreach (var handle in handlesToFree)
{
try
{
if (handle.IsAllocated)
handle.Free();
}
catch
{
// Ignore errors during cleanup - handles may already be freed
}
}
_disposed = true;
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace CeresSharp.Native.SafeHandles;
/// <summary>
/// Base class for all Ceres native resource handles.
/// </summary>
public abstract class BaseSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
protected BaseSafeHandle() : base(true)
{
}
protected BaseSafeHandle(IntPtr handle, bool ownsHandle) : base(ownsHandle)
{
SetHandle(handle);
}
private volatile bool _released = false;
protected override bool ReleaseHandle()
{
// Prevent double-free by checking if already released
// Use volatile read/write for thread safety in finalizer
if (_released || IsInvalid)
return true; // Already released or invalid
try
{
// Add try-catch to prevent crash in finalizer thread
// If native handle is already freed or invalid, this will fail gracefully
ReleaseNativeHandle(handle);
_released = true;
return true;
}
catch
{
// Ignore errors in finalizer - native handle may already be freed
// Mark as released to prevent retry
_released = true;
// Return true to indicate handle was "released" (even if it failed)
return true;
}
}
/// <summary>
/// Releases the native handle. Must be implemented by derived classes.
/// </summary>
protected abstract void ReleaseNativeHandle(IntPtr handle);
}

View File

@@ -0,0 +1,33 @@
using System;
using CeresSharp.Native;
namespace CeresSharp.Native.SafeHandles;
/// <summary>
/// Safe handle for Ceres Context.
/// </summary>
internal sealed class ContextHandle : BaseSafeHandle
{
private ContextHandle() : base()
{
}
private ContextHandle(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle)
{
}
public static ContextHandle Create()
{
var handle = CeresNative.ceres_wrapper_create_context();
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.OutOfMemory, "Failed to create context");
}
return new ContextHandle(handle, true);
}
protected override void ReleaseNativeHandle(IntPtr handle)
{
CeresNative.ceres_wrapper_free_context(handle);
}
}

View File

@@ -0,0 +1,48 @@
using System;
using CeresSharp.Native;
namespace CeresSharp.Native.SafeHandles;
/// <summary>
/// Safe handle for Ceres CostFunction.
/// </summary>
internal sealed class CostFunctionHandle : BaseSafeHandle
{
private volatile bool _ownedByProblem = false;
private CostFunctionHandle() : base()
{
}
private CostFunctionHandle(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle)
{
}
public static CostFunctionHandle Create(IntPtr handle)
{
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.NullPointer, "Cost function handle is null");
}
return new CostFunctionHandle(handle, true);
}
/// <summary>
/// Marks this cost function as owned by a Problem.
/// When owned by Problem, the native handle should NOT be freed here
/// because Problem/Ceres will free it when Problem is destroyed.
/// </summary>
internal void MarkOwnedByProblem() => _ownedByProblem = true;
protected override void ReleaseNativeHandle(IntPtr handle)
{
if (_ownedByProblem)
{
// Problem owns this cost function - Ceres will delete it when Problem is freed.
// Do NOT free here to avoid double-free.
return;
}
// Standalone cost function - we must free it ourselves
CeresNative.ceres_wrapper_free_autodiff_cost_function(handle);
}
}

View File

@@ -0,0 +1,43 @@
using System;
using CeresSharp.Native;
namespace CeresSharp.Native.SafeHandles;
/// <summary>
/// Safe handle for Ceres Covariance.
/// </summary>
internal sealed class CovarianceHandle : BaseSafeHandle
{
private CovarianceHandle() : base()
{
}
private CovarianceHandle(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle)
{
}
public static CovarianceHandle Create()
{
var handle = CeresNative.ceres_wrapper_create_covariance();
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.OutOfMemory, "Failed to create covariance");
}
return new CovarianceHandle(handle, true);
}
public static CovarianceHandle CreateWithOptions(IntPtr options)
{
var handle = CeresNative.ceres_wrapper_create_covariance_with_options(options);
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.OutOfMemory, "Failed to create covariance with options");
}
return new CovarianceHandle(handle, true);
}
protected override void ReleaseNativeHandle(IntPtr handle)
{
CeresNative.ceres_wrapper_free_covariance(handle);
}
}

View File

@@ -0,0 +1,33 @@
using System;
using CeresSharp.Native;
namespace CeresSharp.Native.SafeHandles;
/// <summary>
/// Safe handle for Ceres CovarianceOptions.
/// </summary>
internal sealed class CovarianceOptionsHandle : BaseSafeHandle
{
private CovarianceOptionsHandle() : base()
{
}
private CovarianceOptionsHandle(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle)
{
}
public static CovarianceOptionsHandle Create()
{
var handle = CeresNative.ceres_wrapper_create_covariance_options();
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.OutOfMemory, "Failed to create covariance options");
}
return new CovarianceOptionsHandle(handle, true);
}
protected override void ReleaseNativeHandle(IntPtr handle)
{
CeresNative.ceres_wrapper_free_covariance_options(handle);
}
}

View File

@@ -0,0 +1,32 @@
using System;
using CeresSharp.Native;
namespace CeresSharp.Native.SafeHandles;
/// <summary>
/// Safe handle for Ceres GradientChecker.
/// </summary>
internal sealed class GradientCheckerHandle : BaseSafeHandle
{
private GradientCheckerHandle() : base()
{
}
private GradientCheckerHandle(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle)
{
}
public static GradientCheckerHandle Create(IntPtr handle)
{
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.NullPointer, "Gradient checker handle is null");
}
return new GradientCheckerHandle(handle, true);
}
protected override void ReleaseNativeHandle(IntPtr handle)
{
CeresNative.ceres_wrapper_free_gradient_checker(handle);
}
}

View File

@@ -0,0 +1,33 @@
using System;
using CeresSharp.Native;
namespace CeresSharp.Native.SafeHandles;
/// <summary>
/// Safe handle for Ceres GradientCheckerOptions.
/// </summary>
internal sealed class GradientCheckerOptionsHandle : BaseSafeHandle
{
private GradientCheckerOptionsHandle() : base()
{
}
private GradientCheckerOptionsHandle(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle)
{
}
public static GradientCheckerOptionsHandle Create()
{
var handle = CeresNative.ceres_wrapper_create_gradient_checker_options();
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.OutOfMemory, "Failed to create gradient checker options");
}
return new GradientCheckerOptionsHandle(handle, true);
}
protected override void ReleaseNativeHandle(IntPtr handle)
{
CeresNative.ceres_wrapper_free_gradient_checker_options(handle);
}
}

View File

@@ -0,0 +1,51 @@
using System;
using CeresSharp.Native;
namespace CeresSharp.Native.SafeHandles;
/// <summary>
/// Safe handle for Ceres Interpolator (BiCubic or Cubic).
/// </summary>
internal sealed class InterpolatorHandle : BaseSafeHandle
{
private readonly bool _isBicubic;
private InterpolatorHandle() : base()
{
}
private InterpolatorHandle(IntPtr handle, bool ownsHandle, bool isBicubic) : base(handle, ownsHandle)
{
_isBicubic = isBicubic;
}
public static InterpolatorHandle CreateBicubic(IntPtr handle)
{
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.NullPointer, "BiCubic interpolator handle is null");
}
return new InterpolatorHandle(handle, true, true);
}
public static InterpolatorHandle CreateCubic(IntPtr handle)
{
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.NullPointer, "Cubic interpolator handle is null");
}
return new InterpolatorHandle(handle, true, false);
}
protected override void ReleaseNativeHandle(IntPtr handle)
{
if (_isBicubic)
{
CeresNative.ceres_wrapper_free_bicubic_interpolator(handle);
}
else
{
CeresNative.ceres_wrapper_free_cubic_interpolator(handle);
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using CeresSharp.Native;
namespace CeresSharp.Native.SafeHandles;
/// <summary>
/// Safe handle for Ceres LossFunction.
/// </summary>
internal sealed class LossFunctionHandle : BaseSafeHandle
{
private volatile bool _ownedByProblem = false;
private LossFunctionHandle() : base()
{
}
private LossFunctionHandle(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle)
{
}
public static LossFunctionHandle Create(IntPtr handle)
{
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.NullPointer, "Loss function handle is null");
}
return new LossFunctionHandle(handle, true);
}
/// <summary>
/// Marks this loss function as owned by a Problem.
/// When owned by Problem, the native handle should NOT be freed here
/// because Problem/Ceres will free it when Problem is destroyed.
/// </summary>
internal void MarkOwnedByProblem() => _ownedByProblem = true;
protected override void ReleaseNativeHandle(IntPtr handle)
{
if (_ownedByProblem)
{
// Problem owns this loss function - Ceres will delete it when Problem is freed.
// Do NOT free here to avoid double-free.
return;
}
// Standalone loss function - we must free it ourselves
CeresNative.ceres_wrapper_free_loss_function(handle);
}
}

View File

@@ -0,0 +1,32 @@
using System;
using CeresSharp.Native;
namespace CeresSharp.Native.SafeHandles;
/// <summary>
/// Safe handle for Ceres Manifold.
/// </summary>
internal sealed class ManifoldHandle : BaseSafeHandle
{
private ManifoldHandle() : base()
{
}
private ManifoldHandle(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle)
{
}
public static ManifoldHandle Create(IntPtr handle)
{
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.NullPointer, "Manifold handle is null");
}
return new ManifoldHandle(handle, true);
}
protected override void ReleaseNativeHandle(IntPtr handle)
{
CeresNative.ceres_wrapper_free_manifold(handle);
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Runtime.InteropServices;
using CeresSharp.Native;
namespace CeresSharp.Native.SafeHandles;
/// <summary>
/// Safe handle for Ceres Problem.
/// Note: Uses official C API (ceres_create_problem/ceres_free_problem).
/// </summary>
internal sealed class ProblemHandle : BaseSafeHandle
{
private ProblemHandle() : base()
{
}
private ProblemHandle(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle)
{
}
public static ProblemHandle Create()
{
// Note: ceres_create_problem is from official C API, not wrapper
// We'll need to import it separately or use wrapper function if available
var handle = CeresNative.ceres_create_problem();
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.OutOfMemory, "Failed to create problem");
}
return new ProblemHandle(handle, true);
}
public static ProblemHandle CreateWithOptions(IntPtr problemOptions)
{
var handle = CeresNative.ceres_wrapper_create_problem_with_options(problemOptions);
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.OutOfMemory, "Failed to create problem with options");
}
return new ProblemHandle(handle, true);
}
protected override void ReleaseNativeHandle(IntPtr handle)
{
// Note: ceres_free_problem is from official C API
// Add null check to prevent crash if handle is invalid
if (handle != IntPtr.Zero)
{
try
{
CeresNative.ceres_free_problem(handle);
}
catch
{
// Ignore errors - handle may already be freed or invalid
// This can happen in finalizer thread
}
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using CeresSharp.Native;
namespace CeresSharp.Native.SafeHandles;
/// <summary>
/// Safe handle for Ceres ProblemOptions.
/// </summary>
internal sealed class ProblemOptionsHandle : BaseSafeHandle
{
private ProblemOptionsHandle() : base()
{
}
private ProblemOptionsHandle(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle)
{
}
public static ProblemOptionsHandle Create()
{
var handle = CeresNative.ceres_wrapper_create_problem_options();
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.OutOfMemory, "Failed to create problem options");
}
return new ProblemOptionsHandle(handle, true);
}
protected override void ReleaseNativeHandle(IntPtr handle)
{
CeresNative.ceres_wrapper_free_problem_options(handle);
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Runtime.InteropServices;
using CeresSharp.Native;
namespace CeresSharp.Native.SafeHandles;
/// <summary>
/// Safe handle for Ceres SolverOptions.
/// </summary>
internal sealed class SolverOptionsHandle : BaseSafeHandle
{
private SolverOptionsHandle() : base()
{
}
private SolverOptionsHandle(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle)
{
}
public static SolverOptionsHandle Create()
{
var handle = CeresNative.ceres_wrapper_create_solver_options();
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.OutOfMemory, "Failed to create solver options");
}
return new SolverOptionsHandle(handle, true);
}
protected override void ReleaseNativeHandle(IntPtr handle)
{
CeresNative.ceres_wrapper_free_solver_options(handle);
}
}

View File

@@ -0,0 +1,33 @@
using System;
using CeresSharp.Native;
namespace CeresSharp.Native.SafeHandles;
/// <summary>
/// Safe handle for Ceres SolverSummary.
/// </summary>
internal sealed class SolverSummaryHandle : BaseSafeHandle
{
private SolverSummaryHandle() : base()
{
}
internal SolverSummaryHandle(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle)
{
}
public static SolverSummaryHandle Create()
{
var handle = CeresNative.ceres_wrapper_create_solver_summary();
if (handle == IntPtr.Zero)
{
throw new Exceptions.CeresException(Exceptions.CeresErrorCode.OutOfMemory, "Failed to create solver summary");
}
return new SolverSummaryHandle(handle, true);
}
protected override void ReleaseNativeHandle(IntPtr handle)
{
CeresNative.ceres_wrapper_free_solver_summary(handle);
}
}

View File

@@ -0,0 +1,144 @@
using System;
using System.Runtime.InteropServices;
namespace CeresSharp.Native;
/// <summary>
/// Helper class for unsafe code patterns and pointer operations.
/// Provides utilities for array pinning and pointer conversions.
/// </summary>
internal static class UnsafeHelpers
{
/// <summary>
/// Executes an operation with a pinned array.
/// </summary>
/// <typeparam name="T">The unmanaged type of the array elements.</typeparam>
/// <param name="array">The array to pin.</param>
/// <param name="operation">The operation to execute with the pinned pointer.</param>
/// <exception cref="ArgumentNullException">Thrown when array is null.</exception>
internal static void WithPinnedArray<T>(T[] array, Action<IntPtr> operation) where T : unmanaged
{
if (array == null)
throw new ArgumentNullException(nameof(array));
unsafe
{
fixed (T* ptr = array)
{
operation((IntPtr)ptr);
}
}
}
/// <summary>
/// Executes an operation with a pinned array and returns a result.
/// </summary>
/// <typeparam name="T">The unmanaged type of the array elements.</typeparam>
/// <typeparam name="TResult">The return type.</typeparam>
/// <param name="array">The array to pin.</param>
/// <param name="operation">The operation to execute with the pinned pointer.</param>
/// <returns>The result of the operation.</returns>
/// <exception cref="ArgumentNullException">Thrown when array is null.</exception>
internal static TResult WithPinnedArray<T, TResult>(T[] array, Func<IntPtr, TResult> operation) where T : unmanaged
{
if (array == null)
throw new ArgumentNullException(nameof(array));
unsafe
{
fixed (T* ptr = array)
{
return operation((IntPtr)ptr);
}
}
}
/// <summary>
/// Executes an operation with multiple pinned arrays.
/// </summary>
/// <typeparam name="T">The unmanaged type of the array elements.</typeparam>
/// <param name="arrays">The arrays to pin.</param>
/// <param name="operation">The operation to execute with the pinned pointers.</param>
/// <exception cref="ArgumentNullException">Thrown when arrays is null or contains null elements.</exception>
internal static void WithPinnedArrays<T>(T[][] arrays, Action<IntPtr[]> operation) where T : unmanaged
{
if (arrays == null)
throw new ArgumentNullException(nameof(arrays));
unsafe
{
var pointers = new IntPtr[arrays.Length];
var pins = new GCHandle[arrays.Length];
try
{
for (int i = 0; i < arrays.Length; i++)
{
if (arrays[i] == null)
throw new ArgumentException($"Array at index {i} is null", nameof(arrays));
pins[i] = GCHandle.Alloc(arrays[i], GCHandleType.Pinned);
pointers[i] = pins[i].AddrOfPinnedObject();
}
operation(pointers);
}
finally
{
foreach (var pin in pins)
{
if (pin.IsAllocated)
pin.Free();
}
}
}
}
/// <summary>
/// Pins an array and returns a GCHandle that must be freed by the caller.
/// </summary>
/// <typeparam name="T">The unmanaged type of the array elements.</typeparam>
/// <param name="array">The array to pin.</param>
/// <returns>A GCHandle that pins the array.</returns>
/// <exception cref="ArgumentNullException">Thrown when array is null.</exception>
internal static GCHandle PinArray<T>(T[] array) where T : unmanaged
{
if (array == null)
throw new ArgumentNullException(nameof(array));
return GCHandle.Alloc(array, GCHandleType.Pinned);
}
/// <summary>
/// Copies data from an IntPtr array to a managed array of arrays.
/// </summary>
/// <param name="parameterPtrs">Pointer to array of IntPtr pointers.</param>
/// <param name="parameterBlockSizes">Array of sizes for each parameter block.</param>
/// <returns>Array of parameter blocks.</returns>
internal static double[][] CopyParameterBlocks(IntPtr parameterPtrs, int[] parameterBlockSizes)
{
if (parameterPtrs == IntPtr.Zero)
throw new ArgumentException("Parameter pointers cannot be zero", nameof(parameterPtrs));
if (parameterBlockSizes == null || parameterBlockSizes.Length == 0)
throw new ArgumentException("Parameter block sizes cannot be null or empty", nameof(parameterBlockSizes));
var parameterBlocks = new double[parameterBlockSizes.Length][];
unsafe
{
var paramPtrs = (IntPtr*)parameterPtrs;
for (int i = 0; i < parameterBlockSizes.Length; i++)
{
var size = parameterBlockSizes[i];
if (size <= 0)
throw new ArgumentException($"Parameter block size at index {i} must be positive", nameof(parameterBlockSizes));
parameterBlocks[i] = new double[size];
Marshal.Copy(paramPtrs[i], parameterBlocks[i], 0, size);
}
}
return parameterBlocks;
}
}

File diff suppressed because it is too large Load Diff