Files
OpenNest/OpenNest.Engine/ML/BruteForceRunner.cs
T
ajandClaude Opus 5.5 5761eefd37 refactor(engine): merge the plate fillers into one PlateFiller
DefaultPlateFiller becomes PlateFiller(plate, policy, layout); PlateFillerBase
and StripPlateFiller are gone. PlateLayout.FillThenPack is the old
orchestrator path (Fill and both Remnants); PlateLayout.Strips is the old
StripPlateFiller.Nest, and Strip is Fill policy plus Strips layout.

Lifetimes are unchanged: a strip-layout filler runs every Fill, group Fill
and PackArea on a fresh inner filler and gives each strip fresh remnant
fillers, so learned angles and PlateNumber never leak into them. The
comparer, preferred direction, trim axis and angle recording now read the
policy directly. The base class's unsorted two-angle BuildAngles is dropped:
only DefaultPlateFiller's pipeline called BuildAngles, and Strip never ran it.

Layout-neutral: all 29 FillStackCharacterizationTests pins pass unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 23:44:51 -04:00

97 lines
3.6 KiB
C#

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using OpenNest.Engine.Jobs.Placement.Fillers;
namespace OpenNest.Engine.ML
{
public class BruteForceResult
{
public int PartCount { get; set; }
public double Utilization { get; set; }
public long TimeMs { get; set; }
public string LayoutData { get; set; }
public List<Part> PlacedParts { get; set; }
public string WinnerEngine { get; set; } = "";
public long WinnerTimeMs { get; set; }
public string RunnerUpEngine { get; set; } = "";
public int RunnerUpPartCount { get; set; }
public long RunnerUpTimeMs { get; set; }
public string ThirdPlaceEngine { get; set; } = "";
public int ThirdPlacePartCount { get; set; }
public long ThirdPlaceTimeMs { get; set; }
public List<AngleResult> AngleResults { get; set; } = new();
}
public static class BruteForceRunner
{
public static BruteForceResult Run(
Drawing drawing,
Plate plate,
bool forceFullAngleSweep = false
)
{
var filler = new PlateFiller(plate);
filler.ForceFullAngleSweep = forceFullAngleSweep;
var item = new NestItem { Drawing = drawing };
var sw = Stopwatch.StartNew();
var parts = filler.Fill(
item,
plate.WorkArea(),
null,
System.Threading.CancellationToken.None
);
sw.Stop();
if (parts == null || parts.Count == 0)
return null;
// Rank phase results — winner is explicit, runners-up sorted by count.
var winner = filler.PhaseResults.FirstOrDefault(r => r.Phase == filler.WinnerPhase);
var runnerUps = filler
.PhaseResults.Where(r => r.PartCount > 0 && r.Phase != filler.WinnerPhase)
.OrderByDescending(r => r.PartCount)
.ToList();
return new BruteForceResult
{
PartCount = parts.Count,
Utilization = CalculateUtilization(parts, plate.Area()),
TimeMs = sw.ElapsedMilliseconds,
LayoutData = SerializeLayout(parts),
PlacedParts = parts,
WinnerEngine = filler.WinnerPhase.ToString(),
WinnerTimeMs = winner?.TimeMs ?? 0,
RunnerUpEngine = runnerUps.Count > 0 ? runnerUps[0].Phase.ToString() : "",
RunnerUpPartCount = runnerUps.Count > 0 ? runnerUps[0].PartCount : 0,
RunnerUpTimeMs = runnerUps.Count > 0 ? runnerUps[0].TimeMs : 0,
ThirdPlaceEngine = runnerUps.Count > 1 ? runnerUps[1].Phase.ToString() : "",
ThirdPlacePartCount = runnerUps.Count > 1 ? runnerUps[1].PartCount : 0,
ThirdPlaceTimeMs = runnerUps.Count > 1 ? runnerUps[1].TimeMs : 0,
AngleResults = filler.AngleResults.ToList(),
};
}
private static string SerializeLayout(List<Part> parts)
{
var data = parts
.Select(p => new
{
X = p.Location.X,
Y = p.Location.Y,
R = p.Rotation,
})
.ToList();
return System.Text.Json.JsonSerializer.Serialize(data);
}
private static double CalculateUtilization(List<Part> parts, double plateArea)
{
if (plateArea <= 0)
return 0;
return parts.Sum(p => p.BaseDrawing.Area) / plateArea;
}
}
}