Initial commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
1127
srcs/RobotNet10/RobotApp/Communication/CeresSharp/README.md
Normal file
1127
srcs/RobotNet10/RobotApp/Communication/CeresSharp/README.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user