refactor: DFS solver → matrix-based linear algebra (MathNet.Numerics)
- Rewrite ProductionSolver: system of linear equations A×x=b via LU/QR decomposition - Add ResourceClassifier: classify resources as target/intermediate/raw - Add RecipeMatrixBuilder: build coefficient matrix and RHS vector - Add DI container (Microsoft.Extensions.DependencyInjection) in CLI - Fix resource flow calculation (RecipeRate is total, not per-machine) - Fix recipes.json: moduleCategory → machineCategory (recipe #19) - Update tests: 30 tests covering solver, classifier, matrix builder - Update README: document matrix model, architecture, principles
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
using FactorioCalc.Domain;
|
||||
|
||||
namespace FactorioCalc.Solver;
|
||||
|
||||
/// <summary>
|
||||
/// Classifies resources into targets (user-requested), craftable (have producers),
|
||||
/// and raw (must be extracted from external sources).
|
||||
/// </summary>
|
||||
public sealed class ResourceClassifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Resources that have at least one recipe producing them.
|
||||
/// </summary>
|
||||
public IReadOnlySet<int> Craftable { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Resources explicitly requested by the user.
|
||||
/// </summary>
|
||||
public IReadOnlySet<int> Targets { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Craftable resources not directly targeted — produced only as intermediates.
|
||||
/// </summary>
|
||||
public IReadOnlySet<int> Intermediate { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Resources with no known producer — must come from mining, gathering, etc.
|
||||
/// </summary>
|
||||
public IReadOnlySet<int> Raw { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// All resource IDs touched by the problem (targets + every ingredient in relevant recipes).
|
||||
/// </summary>
|
||||
public IReadOnlySet<int> AllResourceIds { get; private set; } = default!;
|
||||
|
||||
public void Classify(
|
||||
IReadOnlyDictionary<int, Recipe> recipes,
|
||||
IReadOnlyCollection<ProductionTarget> targets)
|
||||
{
|
||||
var targetIds = new HashSet<int>(targets.Select(t => t.ResourceId));
|
||||
|
||||
// Find every resource that some recipe produces
|
||||
var craftable = new HashSet<int>();
|
||||
foreach (var recipe in recipes.Values)
|
||||
{
|
||||
foreach (var product in recipe.Products)
|
||||
craftable.Add(product.ResourceId);
|
||||
}
|
||||
|
||||
var raw = new HashSet<int>();
|
||||
var allIds = new HashSet<int>(targetIds);
|
||||
|
||||
foreach (var rid in targetIds)
|
||||
{
|
||||
if (!craftable.Contains(rid))
|
||||
raw.Add(rid);
|
||||
}
|
||||
|
||||
// Walk ingredients of every recipe that produces a target or craftable resource
|
||||
// to discover all resources involved
|
||||
var visited = new HashSet<int>();
|
||||
var queue = new Queue<int>(targetIds);
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var rid = queue.Dequeue();
|
||||
if (visited.Contains(rid))
|
||||
continue;
|
||||
visited.Add(rid);
|
||||
allIds.Add(rid);
|
||||
|
||||
if (craftable.Contains(rid))
|
||||
{
|
||||
foreach (var recipe in recipes.Values)
|
||||
{
|
||||
if (recipe.Products.Any(p => p.ResourceId == rid))
|
||||
{
|
||||
foreach (var ing in recipe.Ingredients)
|
||||
{
|
||||
allIds.Add(ing.ResourceId);
|
||||
if (!visited.Contains(ing.ResourceId) && !craftable.Contains(ing.ResourceId))
|
||||
raw.Add(ing.ResourceId);
|
||||
queue.Enqueue(ing.ResourceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var intermediate = craftable.Where(r => !targetIds.Contains(r) && allIds.Contains(r)).ToHashSet();
|
||||
|
||||
Craftable = craftable.AsReadOnly();
|
||||
Targets = targetIds.AsReadOnly();
|
||||
Intermediate = intermediate.AsReadOnly();
|
||||
Raw = raw.AsReadOnly();
|
||||
AllResourceIds = allIds.AsReadOnly();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user