From b29f05eec64d69665eac36b1692d971877e0a2c6 Mon Sep 17 00:00:00 2001 From: Darryl Melander Date: Mon, 23 Jun 2025 16:55:56 -0600 Subject: [PATCH] Expose some HiGHS callbacks as .NET events. Several HiGHS callbacks are each exposed as its own event in the C# HighsLpSolver class. The event names are as follows: kCallbackLogging => LogMessageReceived kCallbackMipLogging => MipStatusReported kCallbackMipImprovingSolution => MipImprovingSolutionFound kCallbackMipInterrupt => MipInterruptCheck kCallbackSimplexInterrupt => SimplexInterruptCheck kCallbackIpmInterrupt => IpmInterruptCheck Clients subscribe to and unsubscribe from these events as they would for any other event, using syntax such as `lp.LogMessageReceived += MyLoggingFunc`. Subscribers don't need to call Highs_startCallback or Highs_stopCallback. The events are written so that they automatically call startCallback when the first client subscribes to the event, and automatically call stopCallback when the last client unsubscribes from the event. The interrupt check events include a `user_interrupt` flag in their event args. If an event handler sets this flag, it is passed back to HiGHS via the cbDataIn callback function argument, which causes the event's algorithm to stop. --- highs/interfaces/highs_csharp_api.cs | 362 +++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) diff --git a/highs/interfaces/highs_csharp_api.cs b/highs/interfaces/highs_csharp_api.cs index e06f602750..f037ec2eb3 100644 --- a/highs/interfaces/highs_csharp_api.cs +++ b/highs/interfaces/highs_csharp_api.cs @@ -73,6 +73,31 @@ public enum HighsIntegrality kImplicitInteger = 4, } +/// A category of log message +public enum HighsLogType +{ + Info = 1, + Detailed, + Verbose, + Warning, + Error +} + +/// A category of callback +internal enum HighsCallbackType +{ + Logging = 0, + SimplexInterrupt = 1, + IpmInterrupt = 2, + MipSolution = 3, + MipImprovingSolution = 4, + MipLogging = 5, + MipInterrupt = 6, + MipGetCutPool = 7, + MipDefineLazyConstraints = 8, +} + + public class HighsModel { public HighsObjectiveSense sense; @@ -180,10 +205,26 @@ public class HighsLpSolver : IDisposable { private IntPtr highs; + /// Read-only access to Highs instance pointer + /// Allows sub-classes to do meaningful things using the HiGHS C API. + protected IntPtr HighsObject => this.highs; + private bool _disposed; private const string highslibname = "highs"; + /// Signature of functions that can be called by HiGHS when callback events occur + private delegate void CallbackDelegate( + HighsCallbackType cbType, IntPtr messagePtr, [In] ref HighsCallbackDataOut cbDataOut, + ref HighsCallbackDataIn cbDataIn, IntPtr cbUserData); + + /// Pointer to function that is called when HiGHS callbacks occur + private CallbackDelegate _cbDelegate; + + /// A C function pointer to the event-generating callback delegate + /// This property's primary purpose is to improve ability to test callback-triggered events + protected IntPtr CallbackFunctionPtr => Marshal.GetFunctionPointerForDelegate(_cbDelegate); + [DllImport(highslibname)] private static extern int Highs_call( Int32 numcol, @@ -593,6 +634,15 @@ private static extern int Highs_getBasisTransposeSolve( [DllImport(highslibname)] private static extern int Highs_writeOptionsDeviations(IntPtr highs, string filename); + [DllImport(highslibname)] + private static extern int Highs_setCallback(IntPtr highs, IntPtr cbFuncPtr, IntPtr cbUserData); + + [DllImport(highslibname)] + private static extern int Highs_startCallback(IntPtr highs, HighsCallbackType cbType); + + [DllImport(highslibname)] + private static extern int Highs_stopCallback(IntPtr highs, HighsCallbackType cbType); + public static HighsStatus call(HighsModel model, ref HighsSolution sol, ref HighsBasis bas, ref HighsModelStatus modelstatus) { int nc = model.colcost.Length; @@ -635,6 +685,8 @@ public static HighsStatus call(HighsModel model, ref HighsSolution sol, ref High public HighsLpSolver() { this.highs = HighsLpSolver.Highs_create(); + _cbDelegate = this.callbackFunction; + Highs_setCallback(this.highs, Marshal.GetFunctionPointerForDelegate(_cbDelegate), IntPtr.Zero); } ~HighsLpSolver() @@ -656,6 +708,7 @@ protected virtual void Dispose(bool disposing) } HighsLpSolver.Highs_destroy(this.highs); + this._cbDelegate = null; this._disposed = true; } @@ -1096,8 +1149,317 @@ public HighsStatus writeOptionsDeviations(string filename) { return (HighsStatus)Highs_writeOptionsDeviations(this.highs, filename); } + +#region "Callbacks as events" + private HighsStatus startCallback(HighsCallbackType cbType) + { + return (HighsStatus)Highs_startCallback(this.highs, cbType); + } + + private HighsStatus stopCallback(HighsCallbackType cbType) + { + return (HighsStatus)Highs_stopCallback(this.highs, cbType); + } + + private void callbackFunction(HighsCallbackType cbType, IntPtr messagePtr, + [In] ref HighsCallbackDataOut cbDataOut, + [In, Out] ref HighsCallbackDataIn cbDataIn, IntPtr cbUserData) + { + switch (cbType) + { + case HighsCallbackType.Logging: + // We receive the message as an IntPtr instead of a string so that the marshaller + // doesn't attempt to free the C string. + string message = Marshal.PtrToStringAnsi(messagePtr); + + var loggingEventData = new LoggingEventArgs(cbDataOut.log_type, message); + _innerLogReceived?.Invoke(this, loggingEventData); + break; + + case HighsCallbackType.MipImprovingSolution: + var mipImpArgs = new MipEventArgs(cbDataOut); + _innerMipImproving?.Invoke(this, mipImpArgs); + break; + + case HighsCallbackType.MipLogging: + var mipLogArgs = new MipEventArgs(cbDataOut); + _innerMipLogging?.Invoke(this, mipLogArgs); + break; + + case HighsCallbackType.MipInterrupt: + case HighsCallbackType.IpmInterrupt: + case HighsCallbackType.SimplexInterrupt: + var interruptArgs = new InterruptCheckEventArgs(cbDataOut); + var evnt = cbType == HighsCallbackType.MipInterrupt ? _innerMipInterrupt : + cbType == HighsCallbackType.SimplexInterrupt ? _innerSimplexInterrupt : + cbType == HighsCallbackType.IpmInterrupt ? _innerIpmInterrupt : + null; + evnt?.Invoke(this, interruptArgs); + if (interruptArgs.InterruptSolver) + cbDataIn.user_interrupt = 1; + break; + + default: + break; + } + } + + // Expose callbacks as .NET events. + // Event declarations use custom add/remove accessors to automatically start and stop + // the relevant callbacks when the number of listeners moves between 0 and 1. + + // kCallbackLogging as an event + private readonly object _logReceivedLockObject = new object(); + private EventHandler _innerLogReceived; + /// Occurs when a log message is generated by HiGHS + public event EventHandler LogMessageReceived + { + add + { + lock (_logReceivedLockObject) + { + // If this is the first subscription to the event, start the callback + if (_innerLogReceived == null) + this.startCallback(HighsCallbackType.Logging); + _innerLogReceived += value; + } + } + remove + { + lock (_logReceivedLockObject) + { + _innerLogReceived -= value; + // If this was the last subscription to the event, stop the callback + if (_innerLogReceived == null) + this.stopCallback(HighsCallbackType.Logging); + } + } + } + + // kCallbackMipImprovingSolution as an event + private readonly object _mipImprovingLockObject = new object(); + private EventHandler _innerMipImproving; + /// Occurs when the MIP solver identifies an improving integer feasible solution + public event EventHandler MipImprovingSolutionFound + { + add + { + lock (_mipImprovingLockObject) + { + if (_innerMipImproving == null) + this.startCallback(HighsCallbackType.MipImprovingSolution); + _innerMipImproving += value; + } + } + remove + { + lock (_mipImprovingLockObject) + { + _innerMipImproving -= value; + if (_innerMipImproving == null) + this.stopCallback(HighsCallbackType.MipImprovingSolution); + } + } + } + + // kCallbackMipLogging as an event + private readonly object _mipLoggingLockObject = new object(); + private EventHandler _innerMipLogging; + /// Occurs when the MIP solver receives a MIP status report + public event EventHandler MipStatusReported + { + add + { + lock (_mipLoggingLockObject) + { + if (_innerMipLogging == null) + this.startCallback(HighsCallbackType.MipLogging); + _innerMipLogging += value; + } + } + remove + { + lock (_mipLoggingLockObject) + { + _innerMipLogging -= value; + if (_innerMipLogging == null) + this.stopCallback(HighsCallbackType.MipLogging); + } + } + } + + // kCallbackMipInterrupt as an event + private readonly object _mipInterruptLockObject = new object(); + private EventHandler _innerMipInterrupt; + /// Occurs when the solver checks whether MIP stopping criteria have been satisfied + /// If the client wishes to terminate the solve, set the event's user_interrupt to true + public event EventHandler MipInterruptCheck + { + add + { + lock (_mipInterruptLockObject) + { + if (_innerMipInterrupt == null) + { + this.startCallback(HighsCallbackType.MipInterrupt); + } + _innerMipInterrupt += value; + } + } + remove + { + lock (_mipInterruptLockObject) + { + _innerMipInterrupt -= value; + if (_innerMipInterrupt == null) + { + this.stopCallback(HighsCallbackType.MipInterrupt); + } + } + } + } + + // kCallbackIpmInterrupt as an event + private readonly object _ipmInterruptLockObject = new object(); + private EventHandler _innerIpmInterrupt; + /// Occurs when the solver checks whether MIP stopping criteria have been satisfied + /// If the client wishes to terminate the solve, set the event's user_interrupt to true + public event EventHandler IpmInterruptCheck + { + add + { + lock (_ipmInterruptLockObject) + { + if (_innerIpmInterrupt == null) + { + this.startCallback(HighsCallbackType.IpmInterrupt); + } + _innerIpmInterrupt += value; + } + } + remove + { + lock (_ipmInterruptLockObject) + { + _innerIpmInterrupt -= value; + if (_innerIpmInterrupt == null) + { + this.stopCallback(HighsCallbackType.IpmInterrupt); + } + } + } + } + + // kCallbackSimplexInterrupt as an event + private readonly object _simplexInterruptLockObject = new object(); + private EventHandler _innerSimplexInterrupt; + /// Occurs when the solver checks whether MIP stopping criteria have been satisfied + /// If the client wishes to terminate the solve, set the event's user_interrupt to true + public event EventHandler SimplexInterruptCheck + { + add + { + lock (_simplexInterruptLockObject) + { + if (_innerSimplexInterrupt == null) + { + this.startCallback(HighsCallbackType.SimplexInterrupt); + } + _innerSimplexInterrupt += value; + } + } + remove + { + lock (_simplexInterruptLockObject) + { + _innerSimplexInterrupt -= value; + if (_innerSimplexInterrupt == null) + { + this.stopCallback(HighsCallbackType.SimplexInterrupt); + } + } + } + } +#endregion } + /// Data passed to the callback function from HiGHS + [StructLayout(LayoutKind.Sequential)] + internal struct HighsCallbackDataOut + { + private IntPtr _ignore; + public HighsLogType log_type; + public double running_time; + public int simplex_iteration_count; + public int ipm_iteration_count; + public int pdlp_iteration_count; + public double objective_function_value; + public long mip_node_count; + public long mip_total_lp_iterations; + public double mip_primal_bound; + public double mip_dual_bound; + public double mip_gap; + // Additional fields omitted, .NET marshaller will just ignore any fields beyond this point + } + + /// Data passed from the callback function to HiGHS + [StructLayout(LayoutKind.Sequential)] + internal struct HighsCallbackDataIn + { + public int user_interrupt; + } + + /// Data for message logging events + public class LoggingEventArgs : EventArgs + { + /// The type/level of log message + public HighsLogType LogType { get; } + /// The log message + public string Message { get; } + + public LoggingEventArgs(HighsLogType log_type, string message) + { + this.LogType = log_type; + this.Message = message; + } + } + + /// Data for MIP-related events + public class MipEventArgs : EventArgs + { + /// The execution time in seconds + public double RunningTime { get; } + /// The objective function value of the best integer feasible solution found so far + public double ObjectiveFunctionValue { get; } + /// The number of MIP nodes explored so far + public long MipNodeCount { get; } + /// The primal bound + public double MipPrimalBound { get; } + /// The dual bound + public double MipDualBound { get; } + /// The relative difference between the primal and dual bounds + public double MipGap { get; } + + internal MipEventArgs(HighsCallbackDataOut data) + { + this.RunningTime = data.running_time; + this.ObjectiveFunctionValue = data.objective_function_value; + this.MipNodeCount = data.mip_node_count; + this.MipPrimalBound = data.mip_primal_bound; + this.MipDualBound = data.mip_dual_bound; + this.MipGap = data.mip_gap; + } + } + + public class InterruptCheckEventArgs : EventArgs + { + internal InterruptCheckEventArgs(HighsCallbackDataOut data) + {} + + /// Whether to interrupt the solver operation currently in progress + public bool InterruptSolver { get; set; } = false; + } + /// /// The solution info. ///