style: apply CSharpier formatting to all C# sources
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.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Benchmark
|
||||
{
|
||||
@@ -48,13 +48,28 @@ namespace OpenNest.Benchmark
|
||||
/// engine owns its own multi-plate/size strategy; this harness no
|
||||
/// longer picks plate sizes on the engine's behalf.
|
||||
/// </summary>
|
||||
public NestJob BuildNestJob(int maxPlates, double salvageRate = 0, double minimumSalvageDimension = 0)
|
||||
public NestJob BuildNestJob(
|
||||
int maxPlates,
|
||||
double salvageRate = 0,
|
||||
double minimumSalvageDimension = 0
|
||||
)
|
||||
{
|
||||
var parts = Requests.Select(r =>
|
||||
DrawingJobMapper.FromDrawing(r.Drawing.Id.ToString(), r.Drawing, r.Quantity));
|
||||
var stock = CandidateSizes.Select(size =>
|
||||
new NestPlateStock(size.ToString(1), size, null, PartSpacing, EdgeSpacing, Quadrant));
|
||||
return new NestJob(parts, stock, new NestJobOptions("Default", maxPlates, salvageRate, minimumSalvageDimension));
|
||||
DrawingJobMapper.FromDrawing(r.Drawing.Id.ToString(), r.Drawing, r.Quantity)
|
||||
);
|
||||
var stock = CandidateSizes.Select(size => new NestPlateStock(
|
||||
size.ToString(1),
|
||||
size,
|
||||
null,
|
||||
PartSpacing,
|
||||
EdgeSpacing,
|
||||
Quadrant
|
||||
));
|
||||
return new NestJob(
|
||||
parts,
|
||||
stock,
|
||||
new NestJobOptions("Default", maxPlates, salvageRate, minimumSalvageDimension)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,13 @@ namespace OpenNest.Benchmark
|
||||
/// <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)
|
||||
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);
|
||||
|
||||
@@ -32,15 +37,28 @@ namespace OpenNest.Benchmark
|
||||
{
|
||||
foreach (var engineInfo in engines)
|
||||
{
|
||||
results.Add(RunOne(job, engineInfo, salvageRate, minimumSalvageDimension, outputDirectory));
|
||||
results.Add(
|
||||
RunOne(
|
||||
job,
|
||||
engineInfo,
|
||||
salvageRate,
|
||||
minimumSalvageDimension,
|
||||
outputDirectory
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static JobResult RunOne(BenchmarkJob job, NestingEngineInfo engineInfo,
|
||||
double salvageRate, double minimumSalvageDimension, string outputDirectory)
|
||||
private static JobResult RunOne(
|
||||
BenchmarkJob job,
|
||||
NestingEngineInfo engineInfo,
|
||||
double salvageRate,
|
||||
double minimumSalvageDimension,
|
||||
string outputDirectory
|
||||
)
|
||||
{
|
||||
var requested = job.TotalRequestedQuantity;
|
||||
var sw = Stopwatch.StartNew();
|
||||
@@ -53,18 +71,25 @@ namespace OpenNest.Benchmark
|
||||
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()))
|
||||
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)>(
|
||||
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);
|
||||
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 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
|
||||
@@ -83,23 +108,48 @@ namespace OpenNest.Benchmark
|
||||
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.");
|
||||
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
|
||||
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 }));
|
||||
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();
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
|
||||
namespace OpenNest.Benchmark
|
||||
{
|
||||
@@ -17,8 +17,11 @@ namespace OpenNest.Benchmark
|
||||
/// </summary>
|
||||
public static class JobLoader
|
||||
{
|
||||
public static List<BenchmarkJob> Load(string inputPath, IReadOnlyList<Size> sheetSizeOverrides = null,
|
||||
double? partSpacingOverride = null)
|
||||
public static List<BenchmarkJob> Load(
|
||||
string inputPath,
|
||||
IReadOnlyList<Size> sheetSizeOverrides = null,
|
||||
double? partSpacingOverride = null
|
||||
)
|
||||
{
|
||||
var files = ResolveFiles(inputPath);
|
||||
var jobs = new List<BenchmarkJob>();
|
||||
@@ -33,7 +36,9 @@ namespace OpenNest.Benchmark
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[JobLoader] Skipping '{file}': failed to read ({ex.Message})");
|
||||
Console.Error.WriteLine(
|
||||
$"[JobLoader] Skipping '{file}': failed to read ({ex.Message})"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -41,24 +46,29 @@ namespace OpenNest.Benchmark
|
||||
|
||||
if (requests.Count == 0)
|
||||
{
|
||||
Console.Error.WriteLine($"[JobLoader] Skipping '{file}': no drawings with quantity > 0");
|
||||
Console.Error.WriteLine(
|
||||
$"[JobLoader] Skipping '{file}': no drawings with quantity > 0"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
var template = ResolvePlateTemplate(nest);
|
||||
var sizes = sheetSizeOverrides != null && sheetSizeOverrides.Count > 0
|
||||
? sheetSizeOverrides.ToList()
|
||||
: ResolveSheetSizes(nest);
|
||||
var sizes =
|
||||
sheetSizeOverrides != null && sheetSizeOverrides.Count > 0
|
||||
? sheetSizeOverrides.ToList()
|
||||
: ResolveSheetSizes(nest);
|
||||
|
||||
jobs.Add(new BenchmarkJob
|
||||
{
|
||||
SourceFile = file,
|
||||
CandidateSizes = sizes,
|
||||
EdgeSpacing = template.EdgeSpacing,
|
||||
PartSpacing = partSpacingOverride ?? template.PartSpacing,
|
||||
Quadrant = template.Quadrant,
|
||||
Requests = requests,
|
||||
});
|
||||
jobs.Add(
|
||||
new BenchmarkJob
|
||||
{
|
||||
SourceFile = file,
|
||||
CandidateSizes = sizes,
|
||||
EdgeSpacing = template.EdgeSpacing,
|
||||
PartSpacing = partSpacingOverride ?? template.PartSpacing,
|
||||
Quadrant = template.Quadrant,
|
||||
Requests = requests,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return jobs;
|
||||
@@ -68,7 +78,8 @@ namespace OpenNest.Benchmark
|
||||
{
|
||||
if (Directory.Exists(inputPath))
|
||||
{
|
||||
return Directory.GetFiles(inputPath, "*.nest", SearchOption.AllDirectories)
|
||||
return Directory
|
||||
.GetFiles(inputPath, "*.nest", SearchOption.AllDirectories)
|
||||
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
@@ -92,21 +103,25 @@ namespace OpenNest.Benchmark
|
||||
|
||||
var constraints = drawing.Constraints;
|
||||
|
||||
requests.Add(new DrawingRequest
|
||||
{
|
||||
Drawing = drawing,
|
||||
Quantity = qty,
|
||||
Priority = drawing.Priority,
|
||||
StepAngle = constraints?.StepAngle ?? 0,
|
||||
RotationStart = constraints?.StartAngle ?? 0,
|
||||
RotationEnd = constraints?.EndAngle ?? 0,
|
||||
});
|
||||
requests.Add(
|
||||
new DrawingRequest
|
||||
{
|
||||
Drawing = drawing,
|
||||
Quantity = qty,
|
||||
Priority = drawing.Priority,
|
||||
StepAngle = constraints?.StepAngle ?? 0,
|
||||
RotationStart = constraints?.StartAngle ?? 0,
|
||||
RotationEnd = constraints?.EndAngle ?? 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return requests;
|
||||
}
|
||||
|
||||
private static (Spacing EdgeSpacing, double PartSpacing, int Quadrant) ResolvePlateTemplate(Nest nest)
|
||||
private static (Spacing EdgeSpacing, double PartSpacing, int Quadrant) ResolvePlateTemplate(
|
||||
Nest nest
|
||||
)
|
||||
{
|
||||
var source = nest.Plates?.FirstOrDefault();
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Benchmark
|
||||
{
|
||||
@@ -31,8 +31,10 @@ namespace OpenNest.Benchmark
|
||||
/// identity must never be inferred from Name, which is only incidentally seeded from the
|
||||
/// originating NestJobPart id) to its original quantity limit and display name.
|
||||
/// </summary>
|
||||
public static ValidationResult Validate(List<(Plate Plate, List<Part> Parts)> plateRuns,
|
||||
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements)
|
||||
public static ValidationResult Validate(
|
||||
List<(Plate Plate, List<Part> Parts)> plateRuns,
|
||||
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements
|
||||
)
|
||||
{
|
||||
var result = new ValidationResult();
|
||||
var allParts = plateRuns.SelectMany(pr => pr.Parts).ToList();
|
||||
@@ -55,8 +57,11 @@ namespace OpenNest.Benchmark
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ValidateQuantities(List<Part> parts,
|
||||
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements, ValidationResult result)
|
||||
private static void ValidateQuantities(
|
||||
List<Part> parts,
|
||||
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements,
|
||||
ValidationResult result
|
||||
)
|
||||
{
|
||||
var placedCounts = parts
|
||||
.GroupBy<Part, Drawing>(p => p.BaseDrawing, ReferenceEqualityComparer.Instance)
|
||||
@@ -66,20 +71,27 @@ namespace OpenNest.Benchmark
|
||||
{
|
||||
if (!requirements.TryGetValue(drawing, out var requirement))
|
||||
{
|
||||
result.Violations.Add($"Placed drawing '{drawing.Name}' which was not requested for this job");
|
||||
result.Violations.Add(
|
||||
$"Placed drawing '{drawing.Name}' which was not requested for this job"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (placed > requirement.Quantity)
|
||||
{
|
||||
result.Violations.Add(
|
||||
$"'{requirement.Name}': placed {placed} across all plates but only {requirement.Quantity} were requested");
|
||||
$"'{requirement.Name}': placed {placed} across all plates but only {requirement.Quantity} were requested"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateBounds(List<Part> parts, Plate plate,
|
||||
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements, ValidationResult result)
|
||||
private static void ValidateBounds(
|
||||
List<Part> parts,
|
||||
Plate plate,
|
||||
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements,
|
||||
ValidationResult result
|
||||
)
|
||||
{
|
||||
var workArea = plate.WorkArea();
|
||||
|
||||
@@ -95,8 +107,9 @@ namespace OpenNest.Benchmark
|
||||
if (outLeft || outBottom || outRight || outTop)
|
||||
{
|
||||
result.Violations.Add(
|
||||
$"'{DisplayName(part, requirements)}' at ({part.Location.X:F2},{part.Location.Y:F2}) falls outside the work area " +
|
||||
$"of a {plate.Size} plate");
|
||||
$"'{DisplayName(part, requirements)}' at ({part.Location.X:F2},{part.Location.Y:F2}) falls outside the work area "
|
||||
+ $"of a {plate.Size} plate"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,7 +123,11 @@ namespace OpenNest.Benchmark
|
||||
/// to return false negatives on real, complex production geometry, so
|
||||
/// this check does not depend on it.
|
||||
/// </summary>
|
||||
private static void ValidateAreaBudget(List<Part> parts, Plate plate, ValidationResult result)
|
||||
private static void ValidateAreaBudget(
|
||||
List<Part> parts,
|
||||
Plate plate,
|
||||
ValidationResult result
|
||||
)
|
||||
{
|
||||
var workArea = plate.WorkArea();
|
||||
var budget = workArea.Width * workArea.Length;
|
||||
@@ -119,13 +136,18 @@ namespace OpenNest.Benchmark
|
||||
if (placedArea > budget + Tolerance.Epsilon)
|
||||
{
|
||||
result.Violations.Add(
|
||||
$"Combined placed area ({placedArea:F2}) on a {plate.Size} plate exceeds its work area ({budget:F2}) - " +
|
||||
"parts must overlap even though the polygon overlap check did not flag a pair");
|
||||
$"Combined placed area ({placedArea:F2}) on a {plate.Size} plate exceeds its work area ({budget:F2}) - "
|
||||
+ "parts must overlap even though the polygon overlap check did not flag a pair"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSpacing(List<Part> parts, double spacing,
|
||||
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements, ValidationResult result)
|
||||
private static void ValidateSpacing(
|
||||
List<Part> parts,
|
||||
double spacing,
|
||||
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements,
|
||||
ValidationResult result
|
||||
)
|
||||
{
|
||||
var worldPolygons = new Polygon[parts.Count];
|
||||
var inflatedPolygons = new Polygon[parts.Count];
|
||||
@@ -133,7 +155,10 @@ namespace OpenNest.Benchmark
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
{
|
||||
worldPolygons[i] = WorldPolygon(parts[i], 0);
|
||||
inflatedPolygons[i] = spacing > Tolerance.Epsilon ? WorldPolygon(parts[i], spacing) : worldPolygons[i];
|
||||
inflatedPolygons[i] =
|
||||
spacing > Tolerance.Epsilon
|
||||
? WorldPolygon(parts[i], spacing)
|
||||
: worldPolygons[i];
|
||||
}
|
||||
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
@@ -149,7 +174,8 @@ namespace OpenNest.Benchmark
|
||||
if (Collision.HasOverlap(inflatedPolygons[i], worldPolygons[j]))
|
||||
{
|
||||
result.Violations.Add(
|
||||
$"'{DisplayName(parts[i], requirements)}' and '{DisplayName(parts[j], requirements)}' are closer than the required spacing ({spacing:F3})");
|
||||
$"'{DisplayName(parts[i], requirements)}' and '{DisplayName(parts[j], requirements)}' are closer than the required spacing ({spacing:F3})"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,8 +184,13 @@ namespace OpenNest.Benchmark
|
||||
/// <summary>Friendly name for a violation message, falling back to the materialized
|
||||
/// Drawing's own Name (the raw partId string) if this part wasn't in requirements at all -
|
||||
/// that mismatch is already reported by ValidateQuantities, so this is display-only.</summary>
|
||||
private static string DisplayName(Part part, IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements) =>
|
||||
requirements.TryGetValue(part.BaseDrawing, out var requirement) ? requirement.Name : part.BaseDrawing.Name;
|
||||
private static string DisplayName(
|
||||
Part part,
|
||||
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements
|
||||
) =>
|
||||
requirements.TryGetValue(part.BaseDrawing, out var requirement)
|
||||
? requirement.Name
|
||||
: part.BaseDrawing.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Extracts a part's perimeter as a world-space polygon, optionally inflated
|
||||
@@ -168,7 +199,8 @@ namespace OpenNest.Benchmark
|
||||
/// </summary>
|
||||
private static Polygon WorldPolygon(Part part, double inflateBy)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(part.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
.ToList();
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using OpenNest;
|
||||
using OpenNest.Benchmark;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using OpenNest;
|
||||
using OpenNest.Benchmark;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
return BenchmarkConsole.Run(args);
|
||||
|
||||
@@ -37,7 +37,9 @@ static class BenchmarkConsole
|
||||
|
||||
if (jobs.Count == 0)
|
||||
{
|
||||
Console.Error.WriteLine("No benchmark jobs found (no .nest files with any drawing quantity > 0).");
|
||||
Console.Error.WriteLine(
|
||||
"No benchmark jobs found (no .nest files with any drawing quantity > 0)."
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -49,13 +51,22 @@ static class BenchmarkConsole
|
||||
if (options.EngineNames.Count > 0)
|
||||
{
|
||||
engines = engines
|
||||
.Where(e => options.EngineNames.Any(n => n.Equals(e.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
.Where(e =>
|
||||
options.EngineNames.Any(n =>
|
||||
n.Equals(e.Name, StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
)
|
||||
.ToList();
|
||||
|
||||
if (engines.Count == 0)
|
||||
{
|
||||
Console.Error.WriteLine("None of the requested engines are registered. Available: " +
|
||||
string.Join(", ", NestingEngineRegistry.AvailableEngines.Select(e => e.Name)));
|
||||
Console.Error.WriteLine(
|
||||
"None of the requested engines are registered. Available: "
|
||||
+ string.Join(
|
||||
", ",
|
||||
NestingEngineRegistry.AvailableEngines.Select(e => e.Name)
|
||||
)
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -65,12 +76,20 @@ static class BenchmarkConsole
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
var sizes = string.Join(", ", job.CandidateSizes.Select(s => s.ToString(1)));
|
||||
Console.WriteLine($" {job.Name}: {job.Requests.Count} drawing(s), {job.TotalRequestedQuantity} part(s) requested, candidate sizes: {sizes}");
|
||||
Console.WriteLine(
|
||||
$" {job.Name}: {job.Requests.Count} drawing(s), {job.TotalRequestedQuantity} part(s) requested, candidate sizes: {sizes}"
|
||||
);
|
||||
}
|
||||
|
||||
Console.WriteLine($"Engines: {string.Join(", ", engines.Select(e => e.Name))}");
|
||||
|
||||
var results = BenchmarkRunner.Run(jobs, engines, options.SalvageRate, options.MinimumSalvageDimension, options.OutputDirectory);
|
||||
var results = BenchmarkRunner.Run(
|
||||
jobs,
|
||||
engines,
|
||||
options.SalvageRate,
|
||||
options.MinimumSalvageDimension,
|
||||
options.OutputDirectory
|
||||
);
|
||||
|
||||
Report.PrintDetailed(results);
|
||||
Report.PrintSummary(results);
|
||||
@@ -103,7 +122,10 @@ static class BenchmarkConsole
|
||||
|
||||
case "--engines" when i + 1 < args.Length:
|
||||
o.EngineNames = args[++i]
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Split(
|
||||
',',
|
||||
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
|
||||
)
|
||||
.ToList();
|
||||
break;
|
||||
|
||||
@@ -112,10 +134,16 @@ static class BenchmarkConsole
|
||||
break;
|
||||
|
||||
case "--salvage-rate" when i + 1 < args.Length:
|
||||
o.SalvageRate = double.Parse(args[++i], System.Globalization.CultureInfo.InvariantCulture);
|
||||
o.SalvageRate = double.Parse(
|
||||
args[++i],
|
||||
System.Globalization.CultureInfo.InvariantCulture
|
||||
);
|
||||
break;
|
||||
case "--min-salvage-dimension" when i + 1 < args.Length:
|
||||
o.MinimumSalvageDimension = double.Parse(args[++i], System.Globalization.CultureInfo.InvariantCulture);
|
||||
o.MinimumSalvageDimension = double.Parse(
|
||||
args[++i],
|
||||
System.Globalization.CultureInfo.InvariantCulture
|
||||
);
|
||||
break;
|
||||
case "--output" when i + 1 < args.Length:
|
||||
o.OutputDirectory = args[++i];
|
||||
@@ -139,7 +167,12 @@ static class BenchmarkConsole
|
||||
{
|
||||
var sizes = new List<Size>();
|
||||
|
||||
foreach (var token in arg.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
foreach (
|
||||
var token in arg.Split(
|
||||
',',
|
||||
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
|
||||
)
|
||||
)
|
||||
{
|
||||
if (Size.TryParse(token, out var size))
|
||||
sizes.Add(size);
|
||||
@@ -152,29 +185,63 @@ static class BenchmarkConsole
|
||||
|
||||
private static void PrintUsage()
|
||||
{
|
||||
Console.Error.WriteLine("OpenNest.Benchmark - compare registered whole-job nesting engines on a set of .nest files");
|
||||
Console.Error.WriteLine(
|
||||
"OpenNest.Benchmark - compare registered whole-job nesting engines on a set of .nest files"
|
||||
);
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("For each .nest file, every drawing with quantity > 0 is nested (mixed together),");
|
||||
Console.Error.WriteLine("once per registered INestingEngine. Each engine is handed the full job - every");
|
||||
Console.Error.WriteLine("requested part and the whole pool of candidate sheet sizes - and owns its own");
|
||||
Console.Error.WriteLine("multi-plate/size strategy: how many plates it uses, of which sizes, and how");
|
||||
Console.Error.WriteLine("demand splits across them. Scoring: aggregate material utilization across every");
|
||||
Console.Error.WriteLine("plate used, then (if everything requested was placed) fewer plates as the");
|
||||
Console.Error.WriteLine("tie-break. An invalid layout (out of bounds, overlapping, or over-quantity), a");
|
||||
Console.Error.WriteLine("thrown exception, or a run exceeding its time budget all score zero.");
|
||||
Console.Error.WriteLine(
|
||||
"For each .nest file, every drawing with quantity > 0 is nested (mixed together),"
|
||||
);
|
||||
Console.Error.WriteLine(
|
||||
"once per registered INestingEngine. Each engine is handed the full job - every"
|
||||
);
|
||||
Console.Error.WriteLine(
|
||||
"requested part and the whole pool of candidate sheet sizes - and owns its own"
|
||||
);
|
||||
Console.Error.WriteLine(
|
||||
"multi-plate/size strategy: how many plates it uses, of which sizes, and how"
|
||||
);
|
||||
Console.Error.WriteLine(
|
||||
"demand splits across them. Scoring: aggregate material utilization across every"
|
||||
);
|
||||
Console.Error.WriteLine(
|
||||
"plate used, then (if everything requested was placed) fewer plates as the"
|
||||
);
|
||||
Console.Error.WriteLine(
|
||||
"tie-break. An invalid layout (out of bounds, overlapping, or over-quantity), a"
|
||||
);
|
||||
Console.Error.WriteLine(
|
||||
"thrown exception, or a run exceeding its time budget all score zero."
|
||||
);
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("Usage:");
|
||||
Console.Error.WriteLine(" OpenNest.Benchmark <file.nest | folder> [options]");
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("Options:");
|
||||
Console.Error.WriteLine(" --sheet-sizes W1xL1,W2xL2,... Candidate sheet-size pool for the whole nest");
|
||||
Console.Error.WriteLine(" (default: the distinct sizes already in each file)");
|
||||
Console.Error.WriteLine(" --spacing <value> Override part spacing for every job");
|
||||
Console.Error.WriteLine(" --engines Name1,Name2,... Only benchmark these registered engines (default: all)");
|
||||
Console.Error.WriteLine(" --csv <path> Write a flat CSV of all results");
|
||||
Console.Error.WriteLine(" --salvage-rate <0..1> Fraction of eligible offcut area credited (default 0)");
|
||||
Console.Error.WriteLine(" --min-salvage-dimension <value> Both offcut dimensions must qualify; 0 disables credit");
|
||||
Console.Error.WriteLine(" --output <directory> Save valid layouts as .nest plus detailed JSON reports");
|
||||
Console.Error.WriteLine(
|
||||
" --sheet-sizes W1xL1,W2xL2,... Candidate sheet-size pool for the whole nest"
|
||||
);
|
||||
Console.Error.WriteLine(
|
||||
" (default: the distinct sizes already in each file)"
|
||||
);
|
||||
Console.Error.WriteLine(
|
||||
" --spacing <value> Override part spacing for every job"
|
||||
);
|
||||
Console.Error.WriteLine(
|
||||
" --engines Name1,Name2,... Only benchmark these registered engines (default: all)"
|
||||
);
|
||||
Console.Error.WriteLine(
|
||||
" --csv <path> Write a flat CSV of all results"
|
||||
);
|
||||
Console.Error.WriteLine(
|
||||
" --salvage-rate <0..1> Fraction of eligible offcut area credited (default 0)"
|
||||
);
|
||||
Console.Error.WriteLine(
|
||||
" --min-salvage-dimension <value> Both offcut dimensions must qualify; 0 disables credit"
|
||||
);
|
||||
Console.Error.WriteLine(
|
||||
" --output <directory> Save valid layouts as .nest plus detailed JSON reports"
|
||||
);
|
||||
Console.Error.WriteLine(" --help Show this message");
|
||||
}
|
||||
|
||||
|
||||
@@ -28,19 +28,27 @@ namespace OpenNest.Benchmark
|
||||
var ranked = jobGroup.OrderBy(r => r, Comparer<JobResult>.Create(Compare)).ToList();
|
||||
var best = ranked.Count > 0 ? ranked[0] : null;
|
||||
|
||||
Console.WriteLine($"{"Engine",-16} {"Result",-9} {"Parts",-10} {"Util%",-8} {"Plates",-18} {"Time(ms)",-9} Notes");
|
||||
Console.WriteLine(
|
||||
$"{"Engine", -16} {"Result", -9} {"Parts", -10} {"Util%", -8} {"Plates", -18} {"Time(ms)", -9} Notes"
|
||||
);
|
||||
|
||||
foreach (var r in ranked)
|
||||
{
|
||||
var isWinner = best != null && Compare(r, best) == 0 && r.Valid;
|
||||
var marker = isWinner ? "*" : " ";
|
||||
var status = r.Crashed ? "CRASH" : r.Valid ? "ok" : "INVALID";
|
||||
var status =
|
||||
r.Crashed ? "CRASH"
|
||||
: r.Valid ? "ok"
|
||||
: "INVALID";
|
||||
var partsCol = $"{r.PartsPlaced}/{r.PartsRequested}";
|
||||
var utilCol = r.Valid ? $"{r.Utilization * 100:F1}" : "-";
|
||||
var platesCol = r.PlatesUsed > 0 ? $"{r.PlatesUsed} ({SizeSummary(r.SizeBreakdown)})" : "-";
|
||||
var platesCol =
|
||||
r.PlatesUsed > 0 ? $"{r.PlatesUsed} ({SizeSummary(r.SizeBreakdown)})" : "-";
|
||||
var notes = r.Crashed ? r.Error : string.Join("; ", r.Violations.Take(2));
|
||||
|
||||
Console.WriteLine($"{marker}{r.EngineName,-15} {status,-9} {partsCol,-10} {utilCol,-8} {platesCol,-18} {r.ElapsedMs,-9} {notes}");
|
||||
Console.WriteLine(
|
||||
$"{marker}{r.EngineName, -15} {status, -9} {partsCol, -10} {utilCol, -8} {platesCol, -18} {r.ElapsedMs, -9} {notes}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,30 +76,47 @@ namespace OpenNest.Benchmark
|
||||
|
||||
var wins = CountWins(results);
|
||||
|
||||
Console.WriteLine($"{"Engine",-16} {"Jobs",-6} {"Valid",-7} {"Complete",-9} {"Wins",-6} {"AvgUtil%",-10} {"Plates",-8} {"TotalTime(ms)",-14}");
|
||||
Console.WriteLine(
|
||||
$"{"Engine", -16} {"Jobs", -6} {"Valid", -7} {"Complete", -9} {"Wins", -6} {"AvgUtil%", -10} {"Plates", -8} {"TotalTime(ms)", -14}"
|
||||
);
|
||||
|
||||
foreach (var e in byEngine)
|
||||
{
|
||||
var avgUtil = e.Jobs > 0 ? e.TotalUtilization / e.Jobs * 100 : 0;
|
||||
var winCount = wins.TryGetValue(e.Engine, out var w) ? w : 0;
|
||||
Console.WriteLine($"{e.Engine,-16} {e.Jobs,-6} {e.Valid,-7} {e.FullyPlaced,-9} {winCount,-6} {avgUtil,-10:F1} {e.TotalPlates,-8} {e.TotalTimeMs,-14}");
|
||||
Console.WriteLine(
|
||||
$"{e.Engine, -16} {e.Jobs, -6} {e.Valid, -7} {e.FullyPlaced, -9} {winCount, -6} {avgUtil, -10:F1} {e.TotalPlates, -8} {e.TotalTimeMs, -14}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteCsv(string path, List<JobResult> results)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Job,Engine,Valid,Crashed,FullyPlaced,PartsPlaced,PartsRequested,Utilization,PlatesUsed,SizeBreakdown,ElapsedMs,Notes");
|
||||
sb.AppendLine(
|
||||
"Job,Engine,Valid,Crashed,FullyPlaced,PartsPlaced,PartsRequested,Utilization,PlatesUsed,SizeBreakdown,ElapsedMs,Notes"
|
||||
);
|
||||
|
||||
foreach (var r in results)
|
||||
{
|
||||
var notes = r.Crashed ? r.Error : string.Join(" | ", r.Violations);
|
||||
sb.AppendLine(string.Join(",",
|
||||
Csv(r.JobName), Csv(r.EngineName), r.Valid, r.Crashed, r.FullyPlaced,
|
||||
r.PartsPlaced, r.PartsRequested,
|
||||
r.Utilization.ToString("F4", CultureInfo.InvariantCulture),
|
||||
r.PlatesUsed, Csv(SizeSummary(r.SizeBreakdown)),
|
||||
r.ElapsedMs, Csv(notes)));
|
||||
sb.AppendLine(
|
||||
string.Join(
|
||||
",",
|
||||
Csv(r.JobName),
|
||||
Csv(r.EngineName),
|
||||
r.Valid,
|
||||
r.Crashed,
|
||||
r.FullyPlaced,
|
||||
r.PartsPlaced,
|
||||
r.PartsRequested,
|
||||
r.Utilization.ToString("F4", CultureInfo.InvariantCulture),
|
||||
r.PlatesUsed,
|
||||
Csv(SizeSummary(r.SizeBreakdown)),
|
||||
r.ElapsedMs,
|
||||
Csv(notes)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
File.WriteAllText(path, sb.ToString());
|
||||
|
||||
Reference in New Issue
Block a user