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. ///