using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; namespace OpenNest.Benchmark { /// /// Runs every candidate engine against every job. Each engine is a full /// INestingEngine: it owns its own plate/size selection and multi-plate /// strategy for the whole job, rather than being handed one already-sized /// plate at a time by this harness. A per-run timeout guards against a /// runaway or hanging engine — cooperative cancellation, so it reliably /// stops engines built on NestJobRunner (all four built-ins) but can't /// forcibly interrupt an engine that never checks its token. /// public static class BenchmarkRunner { /// Physical-sheet cap passed to every job's NestJobOptions.MaxPlates. private const int MaxPlates = 40; /// Wall-clock budget for one engine solving one job. private static readonly TimeSpan SolveTimeout = TimeSpan.FromMinutes(5); public static List Run(List jobs, IReadOnlyList engines) { var results = new List(jobs.Count * engines.Count); foreach (var job in jobs) { foreach (var engineInfo in engines) { results.Add(RunOne(job, engineInfo)); } } return results; } private static JobResult RunOne(BenchmarkJob job, NestingEngineInfo engineInfo) { var requested = job.TotalRequestedQuantity; var sw = Stopwatch.StartNew(); try { var nestJob = job.BuildNestJob(MaxPlates); var engine = engineInfo.Factory(); using var cts = new CancellationTokenSource(SolveTimeout); var jobResult = engine.Solve(nestJob, null, cts.Token); var materialized = NestResultMaterializer.Materialize(nestJob, jobResult); var plateRuns = materialized.Nest.Plates .Select(plate => (Plate: plate, Parts: plate.Parts.ToList())) .ToList(); var requirements = job.Requests.ToDictionary( r => materialized.DrawingsByPartId[r.Drawing.Id.ToString()], r => (r.Drawing.Name, r.Quantity), ReferenceEqualityComparer.Instance); var validation = NestValidator.Validate(plateRuns, requirements); var totalPlaced = plateRuns.Sum(pr => pr.Parts.Count); var placedArea = validation.Valid ? plateRuns.Sum(pr => pr.Parts.Sum(p => p.BaseDrawing.Area)) : 0; var plateArea = plateRuns.Sum(pr => pr.Plate.Area()); var sizeBreakdown = plateRuns .GroupBy(pr => pr.Plate.Size.ToString(1)) .OrderByDescending(g => g.Count()) .ToDictionary(g => g.Key, g => g.Count()); sw.Stop(); return new JobResult { EngineName = engineInfo.Name, JobName = job.Name, Valid = validation.Valid, Violations = validation.Violations, PartsPlaced = totalPlaced, PartsRequested = requested, PlacedArea = placedArea, PlateArea = plateArea, PlatesUsed = plateRuns.Count, SizeBreakdown = sizeBreakdown, ElapsedMs = sw.ElapsedMilliseconds, }; } catch (OperationCanceledException) { sw.Stop(); return new JobResult { EngineName = engineInfo.Name, JobName = job.Name, Valid = false, PartsRequested = requested, ElapsedMs = sw.ElapsedMilliseconds, Error = $"Timed out after {SolveTimeout.TotalMinutes:F0} minute(s)", }; } catch (Exception ex) { sw.Stop(); return new JobResult { EngineName = engineInfo.Name, JobName = job.Name, Valid = false, PartsRequested = requested, ElapsedMs = sw.ElapsedMilliseconds, Error = $"{ex.GetType().Name}: {ex.Message}", }; } } } }