using System; using System.Runtime.InteropServices; using System.Text; using CeresSharp.Native; using CeresSharp.Native.SafeHandles; using CeresSharp; namespace CeresSharp.Advanced; /// /// Gradient checker for validating cost function gradients. /// public sealed class GradientChecker : IDisposable { private readonly GradientCheckerHandle _handle; private bool _disposed; /// /// Creates a new GradientChecker instance. /// /// The cost function to check. /// Array of manifolds (can be null if no manifolds). /// The gradient checker options. 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); } } /// /// Gets the native handle. /// internal GradientCheckerHandle Handle => _handle; /// /// Probes gradients at the given parameters. /// /// Array of parameter block arrays. /// Relative precision for comparison. /// True if gradients match, false otherwise. /// Thrown if the operation fails (other than gradient mismatch). 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(); } } } } /// /// Probes gradients at the given parameters and returns error message if they don't match. /// /// Array of parameter block arrays. /// Relative precision for comparison. /// Output error message if gradients don't match. /// True if gradients match, false otherwise. /// Thrown if the operation fails (other than gradient mismatch). 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; } } }