Repo-wide sweep with the pinned CSharpier 1.3.0 tool. Whitespace and line-wrapping only; OpenNest.Engine.Tests (109) and OpenNest.IO.Tests pass after reformat, full solution builds 0 errors. Added .csharpierignore so csproj/config XML keeps its existing layout (CSharpier's XML wrapping churns attributes with zero benefit). Formatting is now enforceable: dotnet csharpier check . passes.
200 lines
8.2 KiB
C#
200 lines
8.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
|
|
namespace OpenNest.Benchmark
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static class BenchmarkRunner
|
|
{
|
|
/// <summary>Physical-sheet cap passed to every job's NestJobOptions.MaxPlates.</summary>
|
|
private const int MaxPlates = 40;
|
|
|
|
/// <summary>Wall-clock budget for one engine solving one job.</summary>
|
|
private static readonly TimeSpan SolveTimeout = TimeSpan.FromMinutes(5);
|
|
|
|
public static List<JobResult> Run(
|
|
List<BenchmarkJob> jobs,
|
|
IReadOnlyList<NestingEngineInfo> engines,
|
|
double salvageRate = 0,
|
|
double minimumSalvageDimension = 0,
|
|
string outputDirectory = null
|
|
)
|
|
{
|
|
var results = new List<JobResult>(jobs.Count * engines.Count);
|
|
|
|
foreach (var job in jobs)
|
|
{
|
|
foreach (var engineInfo in engines)
|
|
{
|
|
results.Add(
|
|
RunOne(
|
|
job,
|
|
engineInfo,
|
|
salvageRate,
|
|
minimumSalvageDimension,
|
|
outputDirectory
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private static JobResult RunOne(
|
|
BenchmarkJob job,
|
|
NestingEngineInfo engineInfo,
|
|
double salvageRate,
|
|
double minimumSalvageDimension,
|
|
string outputDirectory
|
|
)
|
|
{
|
|
var requested = job.TotalRequestedQuantity;
|
|
var sw = Stopwatch.StartNew();
|
|
|
|
try
|
|
{
|
|
var nestJob = job.BuildNestJob(MaxPlates, salvageRate, minimumSalvageDimension);
|
|
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<
|
|
DrawingRequest,
|
|
Drawing,
|
|
(string Name, int Quantity)
|
|
>(
|
|
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());
|
|
|
|
if (validation.Valid && outputDirectory != null)
|
|
{
|
|
System.IO.Directory.CreateDirectory(outputDirectory);
|
|
// Keep names and job metadata for a useful inspectable output; never modify source.
|
|
var source = new OpenNest.IO.NestReader(job.SourceFile).Read();
|
|
materialized.Nest.Name = source.Name;
|
|
materialized.Nest.Units = source.Units;
|
|
materialized.Nest.Material = source.Material;
|
|
materialized.Nest.Thickness = source.Thickness;
|
|
materialized.Nest.SalvageRate = salvageRate;
|
|
foreach (var request in job.Requests)
|
|
materialized.DrawingsByPartId[request.Drawing.Id.ToString()].Name = request
|
|
.Drawing
|
|
.Name;
|
|
var path = System.IO.Path.Combine(
|
|
outputDirectory,
|
|
$"{job.Name}-{engineInfo.Name}.nest"
|
|
);
|
|
if (
|
|
System.IO.Path.GetFullPath(path)
|
|
== System.IO.Path.GetFullPath(job.SourceFile)
|
|
)
|
|
throw new InvalidOperationException(
|
|
"Output must not overwrite the source nest."
|
|
);
|
|
new OpenNest.IO.NestWriter(materialized.Nest).Write(path);
|
|
var report = new
|
|
{
|
|
Source = job.SourceFile,
|
|
Engine = engineInfo.Name,
|
|
jobResult.Status,
|
|
jobResult.StopReason,
|
|
Requested = requested,
|
|
Placed = totalPlaced,
|
|
SheetArea = plateArea,
|
|
PlacedArea = placedArea,
|
|
SalvageRate = salvageRate,
|
|
MinimumSalvageDimension = minimumSalvageDimension,
|
|
EstimatedNetArea = jobResult.Plates.Sum(p =>
|
|
StockLadderNestingEngine.EstimateNetArea(nestJob, p)
|
|
),
|
|
Fulfillment = jobResult.Fulfillment,
|
|
StockUsage = jobResult.StockUsage,
|
|
Plates = jobResult.Plates,
|
|
validation.Violations,
|
|
};
|
|
System.IO.File.WriteAllText(
|
|
System.IO.Path.ChangeExtension(path, ".json"),
|
|
System.Text.Json.JsonSerializer.Serialize(
|
|
report,
|
|
new System.Text.Json.JsonSerializerOptions { WriteIndented = true }
|
|
)
|
|
);
|
|
}
|
|
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}",
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|