LotSizingDataModel.Checker 2.0.1
Reusable validation engine for structural, mathematical and objective consistency checks.
Loading...
Searching...
No Matches
SolutionStructuralChecker.cs
Go to the documentation of this file.
5using LotSizingDataModel.Instance;
6using LotSizingDataModel.Solution;
7using LotSizingDataModel.Solution.Decisions;
8using LotSizingDataModel.Solution.Validation;
9
11
12/// <summary>
13/// Performs solver-independent structural validation of a
14/// <see cref="LotSizingSolution"/> against a
15/// <see cref="LotSizingInstance"/>.
16/// </summary>
17/// <remarks>
18/// <para>
19/// Existing validation logic from LotSizingDataModel.Solution is reused
20/// instead of being duplicated.
21/// </para>
22/// <para>
23/// In addition, this checker creates the expected zero-valued solution
24/// structure through <see cref="LotSizingSolution.CreateFor"/> and compares
25/// its decision keys with the candidate solution. This detects missing
26/// decisions as well as unexpected decisions.
27/// </para>
28/// <para>
29/// Numerical-domain errors are intentionally left for the domain checker
30/// introduced in the next package.
31/// </para>
32/// </remarks>
33public sealed class SolutionStructuralChecker :
35{
36 /// <inheritdoc/>
38 LotSizingInstance instance,
39 LotSizingSolution solution,
41 {
42 ArgumentNullException.ThrowIfNull(instance);
43 ArgumentNullException.ThrowIfNull(solution);
44 ArgumentNullException.ThrowIfNull(options);
45
46 options.EnsureValid();
47
48 var result =
50 {
51 Level =
52 SolutionCheckLevel.Structural,
53 IsStructurallyValid =
54 true,
55 AreVariableDomainsValid =
56 false,
57 IsFeasible =
58 false,
59 IsObjectiveConsistent =
60 false
61 };
62
63 try
64 {
65 AddFilteredExistingValidationIssues(
66 instance,
67 solution,
68 options,
69 result);
70
71 foreach (
72 SolutionCheckIssue schedulingIssue
73 in new MacroMicroSchedulingStructuralValidator().Validate(instance, solution))
74 {
75 result.AddIssue(schedulingIssue);
76 }
77
78 CompareExpectedDecisionStructure(
79 instance,
80 solution,
81 result);
82 }
83 catch (Exception exception)
84 {
85 result.AddIssue(
87 {
88 Severity =
90 Kind =
91 SolutionCheckIssueKind.CheckFailure,
92 Message =
93 "Structural solution checking failed: " +
94 exception.Message
95 });
96 }
97
98 result.IsStructurallyValid =
99 !result.Issues.Any(
100 issue =>
101 issue.Severity ==
103
104 return result;
105 }
106
107 private static void AddFilteredExistingValidationIssues(
108 LotSizingInstance instance,
109 LotSizingSolution solution,
110 SolutionCheckOptions options,
111 SolutionCheckResult result)
112 {
113 var validator =
114 new LotSizingSolutionValidator(
115 options.ZeroTolerance);
116
117 IReadOnlyList<SolutionValidationIssue> sourceIssues =
118 validator.Validate(
119 solution,
120 instance.SupplyChain);
121
122 foreach (
123 SolutionValidationIssue sourceIssue
124 in sourceIssues)
125 {
126 if (!IsStructuralIssue(
127 sourceIssue.Code))
128 {
129 continue;
130 }
131
132 result.AddIssue(
134 {
135 Severity =
136 sourceIssue.Severity ==
137 SolutionValidationSeverity.Error
139 : SolutionCheckSeverity.Warning,
140
141 Kind =
142 SolutionCheckIssueKind.Structural,
143
144 DomainKey =
145 string.IsNullOrWhiteSpace(
146 sourceIssue.Path)
147 ? null
148 : sourceIssue.Path,
149
150 Message =
151 $"{sourceIssue.Code}: " +
152 sourceIssue.Message
153 });
154 }
155 }
156
157 private static bool IsStructuralIssue(
158 string code)
159 {
160 if (string.IsNullOrWhiteSpace(code))
161 {
162 return false;
163 }
164
165 /*
166 * SOL001-SOL004 describe solution identity, horizon and
167 * presence of decisions. SOL005 is deliberately NOT copied
168 * from the legacy solution validator: this checker evaluates
169 * completeness itself by comparing every mandatory decision
170 * key with LotSizingSolution.CreateFor. At feasibility/full
171 * level, MathematicalSolutionValueProjector additionally
172 * requires a value for every variable of the generated
173 * mathematical model, including optional capacity extensions.
174 *
175 * Consequently, propagating SOL005 ("completeness has not
176 * been evaluated") would be incorrect and would duplicate a
177 * check that is actually performed by this independent checker.
178 * SOL006-SOL007 concern evaluation semantics and are deliberately
179 * not treated as structural issues here.
180 */
181 if (code is
182 "SOL001" or
183 "SOL002" or
184 "SOL003" or
185 "SOL004" or
186 "INS001" or
187 "DEC001")
188 {
189 return true;
190 }
191
192 /*
193 * Duplicate-decision codes end in 02.
194 * Internal numerical validity codes ending in 01
195 * are handled by the dedicated domain checker.
196 */
197 if (code.StartsWith(
198 "DEC",
199 StringComparison.Ordinal) &&
200 code.EndsWith(
201 "02",
202 StringComparison.Ordinal))
203 {
204 return true;
205 }
206
207 /*
208 * REFxxx codes verify that item, routing, warehouse,
209 * supplier, distribution-center, work-center and
210 * transport-resource references exist in the instance.
211 */
212 return code.StartsWith(
213 "REF",
214 StringComparison.Ordinal);
215 }
216
217 private static void CompareExpectedDecisionStructure(
218 LotSizingInstance instance,
219 LotSizingSolution solution,
220 SolutionCheckResult result)
221 {
222 LotSizingSolution expected =
223 LotSizingSolution.CreateFor(
224 instance.SupplyChain,
225 instanceIdentifier:
226 solution.InstanceIdentifier,
227 name:
228 "Expected checker structure",
229 instanceFingerprint:
230 solution.InstanceFingerprint,
231 validateSupplyChain:
232 true);
233
234 CompareKeySets(
235 "production",
236 expected.ProductionDecisions.Select(
237 GetProductionKey),
238 solution.ProductionDecisions.Select(
239 GetProductionKey),
240 result);
241
242 CompareKeySets(
243 "inventory",
244 expected.InventoryDecisions.Select(
245 GetInventoryKey),
246 solution.InventoryDecisions.Select(
247 GetInventoryKey),
248 result);
249
250 CompareKeySets(
251 "transport",
252 expected.TransportDecisions.Select(
253 GetTransportKey),
254 solution.TransportDecisions.Select(
255 GetTransportKey),
256 result);
257
258 CompareKeySets(
259 "purchase",
260 expected.PurchaseDecisions.Select(
261 GetPurchaseKey),
262 solution.PurchaseDecisions.Select(
263 GetPurchaseKey),
264 result);
265
266 CompareKeySets(
267 "distribution",
268 expected.DistributionDecisions.Select(
269 GetDistributionKey),
270 solution.DistributionDecisions.Select(
271 GetDistributionKey),
272 result);
273
274 /*
275 * Resource-capacity decision families are deliberately not
276 * compared against LotSizingSolution.CreateFor here.
277 *
278 * CreateFor materializes one capacity-decision container for
279 * each physical work center / warehouse / transport resource,
280 * even when the corresponding capacity extension is not part
281 * of the mathematical problem. Such containers are therefore
282 * optional at the structural level.
283 *
284 * When a generated formulation actually contains a work-center,
285 * warehouse or transport-resource capacity variable, the
286 * MathematicalSolutionValueProjector is the authoritative layer:
287 * it requires the matching decision and reports a
288 * MissingVariableValue issue when it is absent. This keeps
289 * structural checking aligned with the variables that really
290 * exist in the selected formulation instead of with the mere
291 * existence of a physical resource.
292 */
293 }
294
295 private static void CompareKeySets(
296 string familyName,
297 IEnumerable<string> expectedKeys,
298 IEnumerable<string> actualKeys,
299 SolutionCheckResult result)
300 {
301 HashSet<string> expected =
302 expectedKeys.ToHashSet(
303 StringComparer.Ordinal);
304
305 HashSet<string> actual =
306 actualKeys.ToHashSet(
307 StringComparer.Ordinal);
308
309 foreach (
310 string missingKey
311 in expected
312 .Except(
313 actual,
314 StringComparer.Ordinal)
315 .OrderBy(
316 key => key,
317 StringComparer.Ordinal))
318 {
319 result.AddIssue(
320 new SolutionCheckIssue
321 {
322 Severity =
324
325 Kind =
326 SolutionCheckIssueKind.Structural,
327
328 DomainKey =
329 missingKey,
330
331 Message =
332 $"Missing {familyName} decision " +
333 $"'{missingKey}'."
334 });
335 }
336
337 foreach (
338 string unexpectedKey
339 in actual
340 .Except(
341 expected,
342 StringComparer.Ordinal)
343 .OrderBy(
344 key => key,
345 StringComparer.Ordinal))
346 {
347 result.AddIssue(
348 new SolutionCheckIssue
349 {
350 Severity =
352
353 Kind =
354 SolutionCheckIssueKind.Structural,
355
356 DomainKey =
357 unexpectedKey,
358
359 Message =
360 $"Unexpected {familyName} decision " +
361 $"'{unexpectedKey}'."
362 });
363 }
364 }
365
366 private static string GetProductionKey(
367 ProductionDecision decision)
368 {
369 return
370 $"production|routing={decision.RoutingId}";
371 }
372
373 private static string GetInventoryKey(
374 InventoryDecision decision)
375 {
376 return
377 $"inventory|item={decision.ItemId}" +
378 $"|warehouse={GetWarehouseKey(decision.Warehouse)}";
379 }
380
381 private static string GetTransportKey(
382 TransportDecision decision)
383 {
384 return
385 $"transport|item={decision.ItemId}" +
386 $"|resource={decision.TransportResourceId}" +
387 $"|origin={GetWarehouseKey(decision.Origin)}" +
388 $"|destination={GetWarehouseKey(decision.Destination)}";
389 }
390
391 private static string GetPurchaseKey(
392 PurchaseDecision decision)
393 {
394 return
395 $"purchase|supplier={decision.SupplierId}" +
396 $"|item={decision.ItemId}" +
397 $"|destination=" +
398 $"{GetWarehouseKey(decision.DestinationWarehouse)}";
399 }
400
401 private static string GetDistributionKey(
402 DistributionDecision decision)
403 {
404 return
405 $"distribution|center={decision.DistributionCenterId}" +
406 $"|item={decision.ItemId}" +
407 $"|warehouse={GetWarehouseKey(decision.Warehouse)}";
408 }
409
410 private static string GetWarehouseKey(
411 LotSizingDataModel.Core.PhysicalModel.WarehouseReference warehouse)
412 {
413 return
414 $"{warehouse.Kind}:{warehouse.ReferenceId}";
415 }
416}
Configures generic lot-sizing solution checking.
double ZeroTolerance
Gets or sets the absolute tolerance used to identify numerical zero.
Describes one issue detected by a lot-sizing solution checker.
Contains the complete result of checking one lot-sizing solution.
IReadOnlyList< SolutionCheckIssue > Issues
Gets all checker diagnostics.
void AddIssue(SolutionCheckIssue issue)
Adds one issue to the result.
Performs solver-independent structural validation of a LotSizingSolution against a LotSizingInstance.
SolutionCheckResult Check(LotSizingInstance instance, LotSizingSolution solution, SolutionCheckOptions options)
Checks solution structure, decision keys, planning horizons, duplicate decisions, entity references,...
Defines structural validation of a lot-sizing solution against a lot-sizing instance.
SolutionCheckLevel
Defines how deeply a lot-sizing solution must be checked.
SolutionCheckSeverity
Defines the severity of a solution-check issue.
@ Error
Error that invalidates at least one checked property of the solution.
SolutionCheckIssueKind
Identifies the category of an issue detected while checking a solution.