Files
factorio-calc/src/FactorioCalc.Solver/ResourceClassifier.cs
T
mikhail 1c735b124a 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
2026-07-02 14:46:14 +03:00

99 lines
3.3 KiB
C#

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();
}
}