LotSizingDataModel.Solver 2.0.1
Solver-independent modeling, execution, monitoring and adapter infrastructure.
Loading...
Searching...
No Matches
DefaultSolverDiscoveryService.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Diagnostics;
4using System.IO;
5using System.Linq;
6using System.Threading;
7using System.Threading.Tasks;
9
11
12/// <summary>
13/// Provides the default solver discovery implementation used
14/// by the generic solver infrastructure.
15/// </summary>
16/// <remarks>
17/// This service does not reference any vendor SDK. It discovers
18/// native solver installation candidates and managed adapter
19/// plugin assemblies using file-system conventions only.
20/// Vendor-specific validation remains the responsibility of
21/// the corresponding adapter plugin.
22/// </remarks>
23public sealed class DefaultSolverDiscoveryService :
25{
27 _candidateCollector;
28
29 /// <summary>
30 /// Initializes the default solver discovery service.
31 /// </summary>
37
38 /// <summary>
39 /// Initializes the default solver discovery service with an
40 /// explicitly supplied candidate collector.
41 /// </summary>
42 /// <param name="candidateCollector">
43 /// Collector used to locate native solver installation
44 /// candidates.
45 /// </param>
46 /// <exception cref="ArgumentNullException">
47 /// Thrown when <paramref name="candidateCollector"/> is
48 /// <see langword="null"/>.
49 /// </exception>
51 SolverDiscoveryCandidateCollector candidateCollector)
52 {
53 ArgumentNullException.ThrowIfNull(
54 candidateCollector);
55
56 _candidateCollector =
57 candidateCollector;
58 }
59
60 /// <summary>
61 /// Discovers native solver candidates and managed adapter
62 /// plugin assemblies.
63 /// </summary>
64 /// <param name="options">
65 /// Solver-discovery options.
66 /// </param>
67 /// <param name="cancellationToken">
68 /// Token used to cancel discovery.
69 /// </param>
70 /// <returns>
71 /// Complete normalized solver-discovery result.
72 /// </returns>
73 public ValueTask<SolverDiscoveryResult> DiscoverAsync(
75 CancellationToken cancellationToken = default)
76 {
77 ArgumentNullException.ThrowIfNull(
78 options);
79
80 options.EnsureValid();
81
82 var stopwatch =
83 Stopwatch.StartNew();
84
85 var result =
87
88 try
89 {
90 cancellationToken.ThrowIfCancellationRequested();
91
92 IReadOnlyList<SolverDiscoveryCandidate> candidates =
93 _candidateCollector.Collect(
94 options);
95
96 foreach (
98 in candidates)
99 {
100 cancellationToken.ThrowIfCancellationRequested();
101
102 result.Candidates.Add(
103 candidate);
104 }
105
106 DiscoverAdapterDescriptors(
107 options,
108 result,
109 cancellationToken);
110
111 BuildAvailabilityInformation(
112 candidates,
113 options,
114 result);
115
116 stopwatch.Stop();
117
118 result.CompletedAtUtc =
119 DateTime.UtcNow;
120
121 result.ElapsedSeconds =
122 stopwatch.Elapsed.TotalSeconds;
123
124 result.AddDiagnostic(
125 $"Solver discovery completed with " +
126 $"{result.Candidates.Count} installation " +
127 $"candidates, " +
128 $"{result.AdapterDescriptors.Count} adapter " +
129 $"descriptors, and " +
130 $"{result.AvailabilityInformation.Count} " +
131 "solver availability records.");
132
133 return ValueTask.FromResult(
134 result);
135 }
136 catch (OperationCanceledException)
137 {
138 stopwatch.Stop();
139
140 result.CompletedAtUtc =
141 DateTime.UtcNow;
142
143 result.ElapsedSeconds =
144 stopwatch.Elapsed.TotalSeconds;
145
146 result.AddDiagnostic(
147 "Solver discovery was cancelled.");
148
149 return ValueTask.FromResult(
150 result);
151 }
152 catch (Exception exception)
153 {
154 stopwatch.Stop();
155
156 result.CompletedAtUtc =
157 DateTime.UtcNow;
158
159 result.ElapsedSeconds =
160 stopwatch.Elapsed.TotalSeconds;
161
162 result.AddDiagnostic(
163 $"Solver discovery failed: {exception.Message}");
164
165 result.AddDiagnostic(
166 exception.ToString());
167
168 return ValueTask.FromResult(
169 result);
170 }
171 }
172
173 private static void DiscoverAdapterDescriptors(
176 CancellationToken cancellationToken)
177 {
178 foreach (
179 string directory
180 in GetAdapterSearchDirectories(
181 options))
182 {
183 cancellationToken.ThrowIfCancellationRequested();
184
185 if (!Directory.Exists(
186 directory))
187 {
188 continue;
189 }
190
191 SearchOption searchOption =
192 options.RecursiveAdapterSearch
193 ? SearchOption.AllDirectories
194 : SearchOption.TopDirectoryOnly;
195
196 IEnumerable<string> assemblyPaths;
197
198 try
199 {
200 assemblyPaths =
201 Directory.EnumerateFiles(
202 directory,
203 "LotSizingDataModel.Solver.*.dll",
204 searchOption);
205 }
206 catch (
207 Exception exception)
208 when (
209 exception is
210 UnauthorizedAccessException or
211 IOException)
212 {
213 result.AddDiagnostic(
214 $"Adapter directory '{directory}' could not " +
215 $"be scanned: {exception.Message}");
216
217 continue;
218 }
219
220 foreach (
221 string assemblyPath
222 in assemblyPaths)
223 {
224 cancellationToken.ThrowIfCancellationRequested();
225
226 if (!TryInferSolverKind(
227 assemblyPath,
228 out SolverKind solverKind))
229 {
230 continue;
231 }
232
233 string fullPath =
234 Path.GetFullPath(
235 assemblyPath);
236
237 if (result.AdapterDescriptors.Any(
238 descriptor =>
239 string.Equals(
240 descriptor.AssemblyPath,
241 fullPath,
242 StringComparison.OrdinalIgnoreCase)))
243 {
244 continue;
245 }
246
247 string shortName =
248 solverKind switch
249 {
250 SolverKind.Cplex =>
251 "Cplex",
252
253 SolverKind.Gurobi =>
254 "Gurobi",
255
256 SolverKind.Xpress =>
257 "Xpress",
258
259 SolverKind.CoinOrCbc =>
260 "CoinOr",
261
262 _ =>
263 solverKind.ToString()
264 };
265
266 var descriptor =
267 new SolverAdapterDescriptor
268 {
269 AdapterId =
270 $"LotSizingDataModel.Solver." +
271 $"{shortName}",
272
273 AdapterName =
274 $"{shortName} solver adapter",
275
276 AdapterVersion =
277 GetAssemblyVersionHint(
278 fullPath),
279
280 SolverKind =
281 solverKind,
282
283 AssemblyPath =
284 fullPath,
285
286 TypeName =
287 string.Empty
288 };
289
290 result.AdapterDescriptors.Add(
291 descriptor);
292 }
293 }
294 }
295
296 private static void BuildAvailabilityInformation(
297 IReadOnlyList<SolverDiscoveryCandidate> candidates,
298 SolverDiscoveryOptions options,
299 SolverDiscoveryResult result)
300 {
301 foreach (
302 SolverKind solverKind
303 in GetConcreteSolverKinds())
304 {
305 SolverDiscoveryCandidate? bestCandidate =
306 candidates
307 .Where(
308 candidate =>
309 candidate.SolverKind ==
310 solverKind &&
311 candidate.Exists)
312 .OrderBy(
313 candidate =>
314 candidate.Priority)
315 .FirstOrDefault();
316
317 var availability =
318 new SolverAvailabilityInfo(
319 solverKind,
320 bestCandidate is null
321 ? SolverAvailabilityStatus.NotInstalled
322 : options.ValidateLicenses
324 .AvailableWithLimitations
325 : SolverAvailabilityStatus.Available)
326 {
327 SolverName =
328 GetSolverName(
329 solverKind),
330
331 InstallationPath =
332 bestCandidate?.Path ??
333 string.Empty,
334
335 SolverVersion =
336 bestCandidate?.VersionHint ??
337 string.Empty
338 };
339
340 if (bestCandidate is null)
341 {
342 availability.AddDiagnostic(
343 "No existing installation candidate was " +
344 "found.");
345
346 result.AvailabilityInformation.Add(
347 availability);
348
349 continue;
350 }
351
352 availability.AddDiagnostic(
353 $"Installation candidate detected from " +
354 $"{bestCandidate.Source}: " +
355 $"'{bestCandidate.Path}'.");
356
357 if (options.ValidateLicenses)
358 {
359 availability.AddLimitation(
360 "Native solver and license validation is " +
361 "deferred to the vendor-specific adapter.");
362
363 availability.AddDiagnostic(
364 "The generic solver project intentionally " +
365 "does not load vendor SDKs during discovery.");
366 }
367
368 result.AvailabilityInformation.Add(
369 availability);
370 }
371 }
372
373 private static IReadOnlyList<string>
374 GetAdapterSearchDirectories(
375 SolverDiscoveryOptions options)
376 {
377 var directories =
378 new List<string>();
379
380 string applicationDirectory =
381 AppContext.BaseDirectory;
382
383 if (options.SearchApplicationDirectory)
384 {
385 directories.Add(
386 applicationDirectory);
387 }
388
389 if (options.SearchPluginSubdirectory)
390 {
391 directories.Add(
392 Path.Combine(
393 applicationDirectory,
394 options.PluginSubdirectoryName));
395 }
396
397 directories.AddRange(
398 options.AdapterSearchDirectories);
399
400 return directories
401 .Where(
402 directory =>
403 !string.IsNullOrWhiteSpace(
404 directory))
405 .Select(
406 directory =>
407 Path.GetFullPath(
408 Environment.ExpandEnvironmentVariables(
409 directory.Trim())))
410 .Distinct(
411 StringComparer.OrdinalIgnoreCase)
412 .ToArray();
413 }
414
415 private static bool TryInferSolverKind(
416 string assemblyPath,
417 out SolverKind solverKind)
418 {
419 string fileName =
420 Path.GetFileNameWithoutExtension(
421 assemblyPath);
422
423 if (fileName.EndsWith(
424 ".Cplex",
425 StringComparison.OrdinalIgnoreCase))
426 {
427 solverKind =
428 SolverKind.Cplex;
429
430 return true;
431 }
432
433 if (fileName.EndsWith(
434 ".Gurobi",
435 StringComparison.OrdinalIgnoreCase))
436 {
437 solverKind =
438 SolverKind.Gurobi;
439
440 return true;
441 }
442
443 if (fileName.EndsWith(
444 ".Xpress",
445 StringComparison.OrdinalIgnoreCase))
446 {
447 solverKind =
448 SolverKind.Xpress;
449
450 return true;
451 }
452
453 if (fileName.EndsWith(
454 ".CoinOr",
455 StringComparison.OrdinalIgnoreCase) ||
456 fileName.EndsWith(
457 ".CoinOrCbc",
458 StringComparison.OrdinalIgnoreCase) ||
459 fileName.EndsWith(
460 ".Cbc",
461 StringComparison.OrdinalIgnoreCase))
462 {
463 solverKind =
464 SolverKind.CoinOrCbc;
465
466 return true;
467 }
468
469 solverKind =
470 SolverKind.Unknown;
471
472 return false;
473 }
474
475 private static string GetAssemblyVersionHint(
476 string assemblyPath)
477 {
478 try
479 {
480 FileVersionInfo versionInfo =
481 FileVersionInfo.GetVersionInfo(
482 assemblyPath);
483
484 if (!string.IsNullOrWhiteSpace(
485 versionInfo.FileVersion))
486 {
487 return versionInfo.FileVersion;
488 }
489 }
490 catch (
491 Exception exception)
492 when (
493 exception is
494 IOException or
495 UnauthorizedAccessException or
496 ArgumentException)
497 {
498 _ =
499 exception;
500 }
501
502 return "1.0.0";
503 }
504
505 private static string GetSolverName(
506 SolverKind solverKind)
507 {
508 return solverKind switch
509 {
510 SolverKind.Cplex =>
511 "IBM ILOG CPLEX",
512
513 SolverKind.Gurobi =>
514 "Gurobi Optimizer",
515
516 SolverKind.Xpress =>
517 "FICO Xpress Optimizer",
518
519 SolverKind.CoinOrCbc =>
520 "COIN-OR CBC",
521
522 _ =>
523 solverKind.ToString()
524 };
525 }
526
527 private static IReadOnlyList<SolverKind>
528 GetConcreteSolverKinds()
529 {
530 return new[]
531 {
532 SolverKind.Cplex,
533 SolverKind.Gurobi,
534 SolverKind.Xpress,
535 SolverKind.CoinOrCbc
536 };
537 }
538}
DefaultSolverDiscoveryService(SolverDiscoveryCandidateCollector candidateCollector)
Initializes the default solver discovery service with an explicitly supplied candidate collector.
DefaultSolverDiscoveryService()
Initializes the default solver discovery service.
ValueTask< SolverDiscoveryResult > DiscoverAsync(SolverDiscoveryOptions options, CancellationToken cancellationToken=default)
Discovers native solver candidates and managed adapter plugin assemblies.
Collects candidate native solver locations from explicit paths, environment variables,...
Represents one candidate solver installation or adapter location discovered on the current computer.
Configures how solver adapter plugins and native solver installations are discovered on the current c...
void EnsureValid()
Validates the solver-discovery options.
Represents the complete result of solver and adapter discovery on the current computer.
List< SolverAdapterDescriptor > AdapterDescriptors
Gets the solver adapter descriptors discovered during the scan.
void AddDiagnostic(string message)
Adds a diagnostic message.
Defines a service that discovers solver adapter plugins and native solver installations on the curren...
SolverKind
Identifies a supported mathematical optimization solver.
Definition SolverKind.cs:12
SolverAvailabilityStatus
Describes the availability state of a solver adapter on the current computer.