LotSizingDataModel.Checker 2.0.1
Reusable validation engine for structural, mathematical and objective consistency checks.
Loading...
Searching...
No Matches
SolutionScientificProvenanceChecker.cs
Go to the documentation of this file.
1using LotSizingDataModel.Instance;
2using LotSizingDataModel.Instance.Notation.Matching;
3using LotSizingDataModel.Instance.Scientific;
4using LotSizingDataModel.Solution;
5using LotSizingDataModel.Solution.Metadata.Scientific;
6using LotSizingDataModel.Solver.Formulation.Scientific;
7using LotSizingDataModel.Solver.Resolution.Scientific;
8using LotSizingDataModel.Solver.Common;
9
11
12/// <summary>
13/// Re-evaluates a solution's recorded scientific provenance against the
14/// current instance and current formulation capability catalog.
15/// </summary>
17{
18 private readonly ScientificClassificationEngine _classificationEngine;
19 private readonly UniversalNotationMatcher _notationMatcher;
20 private readonly ScientificFormulationCompatibilityService
21 _formulationCompatibilityService;
22
24 : this(
25 new ScientificClassificationEngine(),
26 new UniversalNotationMatcher(),
27 new ScientificFormulationCompatibilityService())
28 {
29 }
30
32 ScientificClassificationEngine classificationEngine,
33 UniversalNotationMatcher notationMatcher,
34 ScientificFormulationCompatibilityService
35 formulationCompatibilityService)
36 {
37 _classificationEngine =
38 classificationEngine ??
39 throw new ArgumentNullException(nameof(classificationEngine));
40
41 _notationMatcher =
42 notationMatcher ??
43 throw new ArgumentNullException(nameof(notationMatcher));
44
45 _formulationCompatibilityService =
46 formulationCompatibilityService ??
47 throw new ArgumentNullException(
48 nameof(formulationCompatibilityService));
49 }
50
52 LotSizingInstance instance,
53 LotSizingSolution solution)
54 {
55 ArgumentNullException.ThrowIfNull(instance);
56 ArgumentNullException.ThrowIfNull(solution);
57
58 SolutionScientificProvenanceReadResult read =
59 SolutionScientificProvenanceCodec.Read(
60 solution.GenerationMetadata);
61
62 if (
63 read.Kind ==
64 SolutionScientificProvenanceReadKind.Missing)
65 {
66 return Result(
68 provenance: null,
69 classification: null,
70 compatibility: null,
71 Diagnostic(
72 "LSDM-PROV-001",
74 "solution.generationMetadata",
75 "Scientific solution provenance is not recorded."));
76 }
77
78 if (
79 read.Kind ==
80 SolutionScientificProvenanceReadKind.Invalid)
81 {
82 return Result(
84 provenance: null,
85 classification: null,
86 compatibility: null,
87 Diagnostic(
88 "LSDM-PROV-002",
90 "solution.generationMetadata",
91 read.Diagnostic));
92 }
93
94 SolutionScientificProvenance provenance =
95 read.Provenance!;
96
97 ScientificClassificationResult classification =
98 _classificationEngine.Analyze(instance);
99
100 if (
101 classification.IsBlocked ||
102 classification.Descriptor is null)
103 {
104 return Result(
106 provenance,
107 classification,
108 compatibility: null,
109 Diagnostic(
110 "LSDM-PROV-003",
112 "instance",
113 "Current instance scientific classification is blocked."));
114 }
115
116 var diagnostics =
117 new List<SolutionScientificProvenanceDiagnostic>();
118
121
122 UniversalNotationMatchResult notationMatch;
123
124 try
125 {
126 notationMatch =
127 _notationMatcher.Match(
128 classification.Descriptor,
129 provenance.DetectedNotation);
130 }
131 catch (Exception exception)
132 when (
133 exception is FormatException or
134 NotSupportedException or
135 ArgumentException)
136 {
137 return Result(
139 provenance,
140 classification,
141 compatibility: null,
142 Diagnostic(
143 "LSDM-PROV-004",
145 "solution.provenance.detectedNotation",
146 "Recorded detected notation is invalid: " +
147 exception.Message));
148 }
149
150 switch (notationMatch.Kind)
151 {
152 case UniversalNotationMatchKind.Exact:
153 break;
154
155 case UniversalNotationMatchKind.Compatible:
156 aggregate =
157 Merge(
158 aggregate,
160
161 diagnostics.Add(
162 Diagnostic(
163 "LSDM-PROV-010",
165 "solution.provenance.detectedNotation",
166 "Recorded notation remains compatible but is less " +
167 "specific than current detected semantics."));
168 break;
169
170 case UniversalNotationMatchKind.Incomplete:
171 aggregate =
172 Merge(
173 aggregate,
175
176 diagnostics.Add(
177 Diagnostic(
178 "LSDM-PROV-011",
180 "solution.provenance.detectedNotation",
181 "Current analysis lacks information required to fully " +
182 "revalidate the recorded notation."));
183 break;
184
185 case UniversalNotationMatchKind.Contradiction:
186 aggregate =
187 Merge(
188 aggregate,
190
191 diagnostics.Add(
192 Diagnostic(
193 "LSDM-PROV-012",
195 "solution.provenance.detectedNotation",
196 "Recorded detected notation contradicts current " +
197 "instance semantics."));
198 break;
199
200 default:
201 throw new ArgumentOutOfRangeException();
202 }
203
204 if (classification.PrimaryProblemClass is null)
205 {
206 aggregate =
207 Merge(
208 aggregate,
210
211 diagnostics.Add(
212 Diagnostic(
213 "LSDM-PROV-020",
215 "classification.problemClass",
216 "Current instance has no unique canonical problem class."));
217 }
218 else if (
219 !classification.PrimaryProblemClass.Definition.Code.Equals(
220 provenance.CanonicalProblemClassCode,
221 StringComparison.OrdinalIgnoreCase))
222 {
223 aggregate =
224 Merge(
225 aggregate,
227
228 diagnostics.Add(
229 Diagnostic(
230 "LSDM-PROV-021",
232 "solution.provenance.problemClassCode",
233 $"Recorded problem class " +
234 $"'{provenance.CanonicalProblemClassCode}' differs from " +
235 $"current class " +
236 $"'{classification.PrimaryProblemClass.Definition.Code}'."));
237 }
238 else if (
239 !classification.PrimaryProblemClass.Kind.ToString().Equals(
240 provenance.ProblemClassMatchKind,
241 StringComparison.Ordinal))
242 {
243 aggregate =
244 Merge(
245 aggregate,
247
248 diagnostics.Add(
249 Diagnostic(
250 "LSDM-PROV-022",
252 "solution.provenance.problemClassMatchKind",
253 "Canonical problem class is unchanged, but core/extension " +
254 "match status has changed."));
255 }
256
257 ScientificFormulationCompatibilityResult compatibility =
258 _formulationCompatibilityService.Assess(
259 classification,
260 provenance.FormulationId);
261
262 switch (compatibility.Kind)
263 {
264 case ScientificFormulationCompatibilityKind.Compatible:
265 break;
266
267 case ScientificFormulationCompatibilityKind.Undetermined:
268 case ScientificFormulationCompatibilityKind.Blocked:
269 aggregate =
270 Merge(
271 aggregate,
273
274 diagnostics.Add(
275 Diagnostic(
276 "LSDM-PROV-030",
278 "solution.provenance.formulationId",
279 "Current scientific formulation compatibility cannot " +
280 "be established completely."));
281 break;
282
283 case ScientificFormulationCompatibilityKind.Incompatible:
284 aggregate =
285 Merge(
286 aggregate,
288
289 diagnostics.Add(
290 Diagnostic(
291 "LSDM-PROV-031",
293 "solution.provenance.formulationId",
294 "Recorded formulation is scientifically incompatible " +
295 "with the current instance classification."));
296 break;
297
298 default:
299 throw new ArgumentOutOfRangeException();
300 }
301
302 if (
303 compatibility.Profile is not null &&
304 !string.IsNullOrWhiteSpace(provenance.FormulationFamily) &&
305 !compatibility.Profile.FormulationFamily.Equals(
306 provenance.FormulationFamily,
307 StringComparison.Ordinal))
308 {
309 aggregate =
310 Merge(
311 aggregate,
313
314 diagnostics.Add(
315 Diagnostic(
316 "LSDM-PROV-032",
318 "solution.provenance.formulationFamily",
319 "The formulation identifier is unchanged but its current " +
320 "scientific family label differs from the recorded one."));
321 }
322
323 if (provenance.IsLegacySchema)
324 {
325 aggregate =
326 Merge(
327 aggregate,
329
330 diagnostics.Add(
331 Diagnostic(
332 "LSDM-PROV-040",
334 "solution.provenance.schemaVersion",
335 "Legacy scientific provenance schema v1 has no explicit " +
336 "solution-method/backend evidence."));
337 }
338 else
339 {
340 ScientificSolutionMethodDefinition? method =
341 ScientificSolutionMethodCatalog.Find(
342 provenance.SolutionMethodId);
343
344 if (method is null)
345 {
346 aggregate =
347 Merge(
348 aggregate,
350
351 diagnostics.Add(
352 Diagnostic(
353 "LSDM-PROV-041",
355 "solution.provenance.solutionMethodId",
356 $"Recorded solution method " +
357 $"'{provenance.SolutionMethodId}' is not present in " +
358 "the current scientific method catalog."));
359 }
360 else if (
361 classification.PrimaryProblemClass is not null &&
362 !method.IsApplicableTo(
363 classification.PrimaryProblemClass.Definition.Id))
364 {
365 aggregate =
366 Merge(
367 aggregate,
369
370 diagnostics.Add(
371 Diagnostic(
372 "LSDM-PROV-042",
374 "solution.provenance.solutionMethodId",
375 $"Recorded solution method '{method.MethodId}' is not " +
376 "applicable to the current canonical problem class."));
377 }
378
379 if (
380 !Enum.TryParse(
381 provenance.SolverBackendKind,
382 ignoreCase: true,
383 out SolverKind backendKind))
384 {
385 aggregate =
386 Merge(
387 aggregate,
389
390 diagnostics.Add(
391 Diagnostic(
392 "LSDM-PROV-043",
394 "solution.provenance.solverBackendKind",
395 $"Recorded solver backend " +
396 $"'{provenance.SolverBackendKind}' is unknown."));
397 }
398 else if (method is not null)
399 {
400 ScientificSolverBackendDefinition? backend =
401 ScientificSolverBackendCatalog.Find(
402 backendKind);
403
404 if (
405 method.RequiresMilpBackend &&
406 (
407 backend is null ||
408 !backend.Supports(method)
409 ))
410 {
411 aggregate =
412 Merge(
413 aggregate,
415
416 diagnostics.Add(
417 Diagnostic(
418 "LSDM-PROV-044",
420 "solution.provenance.solverBackendKind",
421 $"Recorded backend '{backendKind}' is not " +
422 $"compatible with recorded solution method " +
423 $"'{method.MethodId}'."));
424 }
425 }
426 }
427
428 if (diagnostics.Count == 0)
429 {
430 diagnostics.Add(
431 Diagnostic(
432 "LSDM-PROV-100",
434 "solution.provenance",
435 "Recorded scientific provenance is coherent with the " +
436 "current instance and formulation capability catalog."));
437 }
438
440 aggregate,
441 provenance,
442 classification,
443 compatibility,
444 diagnostics);
445 }
446
447 private static SolutionScientificProvenanceCheckKind Merge(
450 {
451 static int Rank(
453 value switch
454 {
461 _ => throw new ArgumentOutOfRangeException(
462 nameof(value),
463 value,
464 null)
465 };
466
467 return Rank(candidate) > Rank(current)
468 ? candidate
469 : current;
470 }
471
472 private static SolutionScientificProvenanceDiagnostic Diagnostic(
473 string code,
475 string path,
476 string message) =>
477 new(
478 code,
479 severity,
480 path,
481 message);
482
483 private static SolutionScientificProvenanceCheckResult Result(
485 SolutionScientificProvenance? provenance,
486 ScientificClassificationResult? classification,
487 ScientificFormulationCompatibilityResult? compatibility,
488 params SolutionScientificProvenanceDiagnostic[] diagnostics) =>
489 new(
490 kind,
491 provenance,
492 classification,
493 compatibility,
494 diagnostics);
495}
SolutionScientificProvenanceChecker(ScientificClassificationEngine classificationEngine, UniversalNotationMatcher notationMatcher, ScientificFormulationCompatibilityService formulationCompatibilityService)
SolutionScientificProvenanceCheckResult Check(LotSizingInstance instance, LotSizingSolution solution)