/// <summary> /// Funkcja z pomocą której następuje uruchomienie solvera i wykonanie obliczeń optymalizujących cenę /// </summary> /// <param name="alloys">Lista stopów biorących udział w procesie wytapiania</param> /// <param name="smelt">Wytop który chcemy uzyskać</param> private async void calculate_price(List <Alloy> alloys, List <Smelt> smelt) { int[] tabVar = new int[alloys.Count]; //il stopow w wytopie int[] tabEq = new int[31]; //il pierwiastkow + war + f. celu try { //warunek nieujemnosci zmiennych for (int i = 0; i < tabVar.Count(); i++) { solver.AddVariable(alloys.ElementAt(i).name, out tabVar[i]); solver.SetBounds(tabVar[i], 0, Rational.PositiveInfinity); } //E(Aij*Xij>bi) i=1,2,..,n for (int i = 0; i < tabEq.Count() - 2; i++) { solver.AddRow("r" + i.ToString(), out tabEq[i]); for (int j = 0; j < selectedAlloys.Count; j++) { //zmienna, kazda zawartosc pierwiastka x jego wsp. parowania solver.SetCoefficient(tabEq[i], tabVar[j], selectedAlloys.ElementAt(j).tabOfElements[i] * (1 - smelt.ElementAt(0).evoporation[i] / 100)); } solver.SetBounds(tabEq[i], smelt.ElementAt(0).min_Norm[i] * smelt.ElementAt(0).Weight, smelt.ElementAt(0).max_Norm[i] * smelt.ElementAt(0).Weight); } solver.AddRow("weightCond", out tabEq[29]); //war masowy solver.AddRow("cost", out tabEq[30]); //f celu for (int i = 0; i < alloys.Count; i++) { solver.SetCoefficient(tabEq[29], tabVar[i], 1); if (alloys.ElementAt(i).Price == 0) { solver.SetCoefficient(tabEq[30], tabVar[i], 1); } else { solver.SetCoefficient(tabEq[30], tabVar[i], alloys.ElementAt(i).Price); } } solver.SetBounds(tabEq[29], smelt.ElementAt(0).Weight, smelt.ElementAt(0).Weight); solver.AddGoal(tabEq[30], 1, true); //po obliczeniach pokaż ekran z wynikami await Navigation.PushAsync(new ProcessResults(solver, alloys, smelt, SwitchPriceWeight.IsToggled)); } catch (Exception ex) { await DisplayAlert("Error", "Cos w solverze nie tak:\n" + ex.ToString(), "OK"); } }
public ORECalculate(bool usefullORE) { useFullORE = usefullORE; oreDictionary = new OREDictionary(); solver = new SimplexSolver(); foreach (var kv in oreDictionary.ORE_Dictionary) { if (usefullORE || kv.Value.isMain) { int id = 0; solver.AddVariable(kv.Key, out id); solver.SetBounds(id, 0, Rational.PositiveInfinity); kv.Value.id = id; } } solver.AddRow("a", out a); solver.AddRow("b", out b); solver.AddRow("c", out c); solver.AddRow("d", out d); solver.AddRow("e", out e); solver.AddRow("f", out f); solver.AddRow("g", out g); solver.AddRow("h", out h); solver.AddRow("cost", out cost); solver.AddGoal(cost, 1, true); }
public void Generate() { // TODO: Int32 is overloading SimplexSolver solver = new SimplexSolver(); mealPortions = new List <int>(); int Calories, cost, Proteins, Carbohydrates, Fats; solver.AddRow("calories", out Calories); solver.SetIntegrality(Calories, true); solver.AddRow("proteins", out Proteins); solver.SetIntegrality(Calories, true); solver.AddRow("carbohydrates", out Carbohydrates); solver.SetIntegrality(Calories, true); solver.AddRow("fats", out Fats); solver.SetIntegrality(Fats, true); solver.AddRow("cost", out cost); Random rand = new Random(); foreach (Meal m in this.meals) { int portions = new int(); solver.AddVariable(m.Name, out portions); solver.SetBounds(portions, 0, 5); solver.SetIntegrality(portions, true); solver.SetCoefficient(Calories, portions, (int)m.Calories_Per_Portions); int prots = ((int)(m.Proteins * m.Portion_Size / 100) - this.proteins) * (-1); solver.SetCoefficient(Proteins, portions, prots); int carbs = ((int)(m.Carbohydrates * m.Portion_Size / 100) - this.carbohydrates) * (-1); solver.SetCoefficient(Carbohydrates, portions, carbs); int fa = ((int)(m.Fats * m.Portion_Size / 100) - this.fats) * (-1); solver.SetCoefficient(Fats, portions, fa); solver.SetCoefficient(cost, portions, rand.Next(0, 100)); mealPortions.Add(portions); } solver.SetBounds(Calories, this.calories, this.calories); //solver.SetBounds(Proteins, 0, int.MaxValue); //solver.SetBounds(Carbohydrates, 0, int.MaxValue); //solver.SetBounds(Fats, 0, int.MaxValue); solver.AddGoal(cost, 1, true); solver.Solve(new SimplexSolverParams()); for (int i = 0; i < this.mealPortions.Count; i++) { this.mealPortions[i] = (int)solver.GetValue(this.mealPortions[i]).ToDouble(); } }
static void Main(string[] args) { SimplexSolver solver = new SimplexSolver(); int savid, vzvid; solver.AddVariable("Saudi", out savid); solver.AddVariable("Ven", out vzvid); solver.SetBounds(savid, 0, 9000); solver.SetBounds(vzvid, 0, 6000); int c1rid, c2rid, c3rid, goalrid; solver.AddRow("c1", out c1rid); solver.AddRow("c2", out c2rid); solver.AddRow("c3", out c3rid); solver.AddRow("goal", out goalrid); // add coefficients to constraint rows solver.SetCoefficient(c1rid, savid, 0.3); solver.SetCoefficient(c1rid, vzvid, 0.4); solver.SetBounds(c1rid, 2000, Rational.PositiveInfinity); solver.SetCoefficient(c2rid, savid, 0.4); solver.SetCoefficient(c2rid, vzvid, 0.2); solver.SetBounds(c2rid, 1500, Rational.PositiveInfinity); solver.SetCoefficient(c3rid, savid, 0.2); solver.SetCoefficient(c3rid, vzvid, 0.3); solver.SetBounds(c3rid, 500, Rational.PositiveInfinity); // add objective (goal) to model and specify minimization (==true) solver.SetCoefficient(goalrid, savid, 20); solver.SetCoefficient(goalrid, vzvid, 15); solver.AddGoal(goalrid, 1, true); solver.Solve(new SimplexSolverParams()); Console.WriteLine("SA {0}, VZ {1}, C1 {2}, C2 {3}, C3 {4}, Goal {5}", solver.GetValue(savid).ToDouble(), solver.GetValue(vzvid).ToDouble(), solver.GetValue(c1rid).ToDouble(), solver.GetValue(c2rid).ToDouble(), solver.GetValue(c3rid).ToDouble(), solver.GetValue(goalrid).ToDouble()); Console.ReadLine(); }
public void Get() { SimplexSolver solver = new SimplexSolver(); double[] estimatedProfitOfProjectX = new double[] { 1, 1.8, 1.6, 0.8, 1.4 }; double[] capitalRequiredForProjectX = new double[] { 6, 12, 10, 4, 8 }; double availableCapital = 20; int[] chooseProjectX = new int[5]; int profit; solver.AddRow("profit", out profit); solver.AddGoal(profit, 0, false); int expenditure; solver.AddRow("expenditure", out expenditure); solver.SetBounds(expenditure, 0, availableCapital); for (int i = 0; i < 5; i++) { solver.AddVariable(string.Format("project{0}", i), out chooseProjectX[i]); solver.SetBounds(chooseProjectX[i], 0, 1); solver.SetIntegrality(chooseProjectX[i], true); solver.SetCoefficient(profit, chooseProjectX[i], estimatedProfitOfProjectX[i]); solver.SetCoefficient(expenditure, chooseProjectX[i], capitalRequiredForProjectX[i]); } SimplexSolverParams param = new SimplexSolverParams(); param.MixedIntegerGenerateCuts = true; solver.Solve(param); Console.WriteLine(solver.MipResult); for (int i = 0; i < 5; i++) { Console.WriteLine("Project {0} is {1} selected.", i, solver.GetValue(chooseProjectX[i]) == 1 ? "" : "not "); } Console.WriteLine("The estimated total profit is: ${0} million.", (double)solver.GetValue(profit).ToDouble()); Console.WriteLine("The total expenditure is: ${0} million.", solver.GetValue(expenditure).ToDouble()); }
/// <summary> /// Solves a sample (randomly generated?) cutting stock problem. /// Given a bolt of cloth of fixed width, and demand for cut strips of the cloth, determine the min "loss" cut patterns to use and how many /// of them. /// Loss is defined as the scrap thrown away. /// It is acceptable to have extra cut widths made. They do not contribute to the cost. (this may be unrealistic in the real world) /// Solver runs by 1st creating an enumeration of possible cut patterns using a CspSolver, then choosing between the patterns and selecting a qty of the patterns such that the /// amount of scrap is minimized and all demand is met using the SimplexSolver MIP code. /// /// In an industrial case, there would likely be more constraints in the generation of the cut patterns. There can be other restrictions such as "these can't be done together" /// or "these MUST be done together (matching pattern or color?)". This can easily be added to the CspSolver model. /// Also, there are likely other characteristics of the cuts or the master problem which would need adaptations. /// /// Further, the limit on the columns generated is implemented in a very arbitrary order. It is more likely that some ordering of the /// value of the columns is needed. In most industrial occurances, the dual variables from the LP relaxation would likely be used to /// guide the generation of columns in an interative fasion rather than a one-time shot at the beginning. /// /// YMMV /// </summary> public static void ShortCuttingStock() { Console.WriteLine("*** Short Cutting Stock ***"); int NumItems = 5; // how many cut widths to generate int ClothWidth = 40; // width of the stock to cut the widths from double efficiency = 0.7; // reject cut patterns less than this % used of the clothwidth int maxPatterns = 100; // max # of patterns to generate bool verbose = true; // set this to true if you want some (useful?) output bool saveMpsFile = false; // set this to true if you want it to save an mps file in c:\\temp\\cutstock.mps int itemSizeMin = 5; // minimum size for random generation of cut int itemSizeMax = 10; // maximum size for random generation of cut int itemDemandMin = 10; // minimum random demand for each cut int itemDemandMax = 40; // maximum random demand for each cut int seed = 12447; // use System.DateTime.Now.Millisecond; instead if you want a random problem. if (verbose) { System.Console.WriteLine(String.Format("Random seed={0}\tmaxWidth={1}", seed, ClothWidth)); } Random rand = new Random(seed); int[] cuts = new int[NumItems]; int[] demand = new int[NumItems]; // item weights and demands for (int cnt = 0; cnt < NumItems; cnt++) { cuts[cnt] = rand.Next(itemSizeMin, itemSizeMax);; demand[cnt] = rand.Next(itemDemandMin, itemDemandMax); if (verbose) { System.Console.WriteLine(String.Format("item[{0}]\tweight={1}\tdemand={2}", cnt, cuts[cnt], demand[cnt])); } } List <int[]> patterns; SolveKnapsack(maxPatterns, cuts, ClothWidth, efficiency, out patterns); SimplexSolver solver2 = new SimplexSolver(); int vId = 0; int[] usage = new int[patterns.Count]; // construct rows that make sure that the demand is met for each kind of cut for (int cnt = 0; cnt < NumItems; cnt++) { solver2.AddRow(String.Format("item{0}", cnt), out vId); solver2.SetBounds(vId, demand[cnt], Rational.PositiveInfinity); } int patCnt = 0; if (verbose) { System.Console.WriteLine(String.Format("Generated {0} patterns", patterns.Count)); } // set usage coeffs (A matrix entries) -- put the patterns as columns in the MIP. Dictionary <int, int> patIdForCol = new Dictionary <int, int>(); foreach (int[] pattern in patterns) { int pId = 0; String varName = String.Format("Pattern{0}", patCnt); solver2.AddVariable(varName, out pId); patIdForCol[pId] = patCnt; solver2.SetIntegrality(pId, true); solver2.SetBounds(pId, 0, Rational.PositiveInfinity); for (int cnt = 0; cnt < NumItems; cnt++) { solver2.SetCoefficient(cnt, pId, pattern[cnt]); // set the coefficient in the matrix // accumulate the quantity used for this pattern. It will be used to figure out the scrap later. usage[patCnt] += pattern[cnt] * cuts[cnt]; } patCnt++; } // set objective coeffs. --- the cost is the scrap solver2.AddRow("Scrap", out vId); for (int cnt = 0; cnt < patterns.Count; cnt++) { int colId = solver2.GetIndexFromKey(String.Format("Pattern{0}", cnt)); solver2.SetCoefficient(vId, colId, (ClothWidth - usage[cnt])); } solver2.AddGoal(vId, 0, true); // invoke the IP solver. SimplexSolverParams parms = new SimplexSolverParams(); parms.MixedIntegerGenerateCuts = true; parms.MixedIntegerPresolve = true; if (saveMpsFile) { MpsWriter writer = new MpsWriter(solver2); using (TextWriter textWriter = new StreamWriter(File.OpenWrite("c:\\temp\\cutstock.mps"))) { writer.WriteMps(textWriter, true); } } solver2.Solve(parms); if (solver2.LpResult == LinearResult.Optimal && solver2.MipResult == LinearResult.Optimal) { //Rational[] solutionVals = solver2.GetValues(); int goalIndex = 0; // output if desired. if (verbose) { System.Console.WriteLine("Solver complete, printing cut plan."); foreach (int cnt in solver2.VariableIndices) { Rational val = solver2.GetValue(cnt); if (val != 0) { if (solver2.IsGoal(cnt)) { goalIndex = cnt; System.Console.WriteLine(String.Format("Goal:{0}\t: {1}\t", val, solver2.GetKeyFromIndex(cnt))); } else if (solver2.IsRow(cnt)) { System.Console.WriteLine(String.Format("{0}:\tValue= {1}\t", solver2.GetKeyFromIndex(cnt), val)); } else { System.Console.Write(String.Format("{0}\tQuantity={1}:\t", solver2.GetKeyFromIndex(cnt), val)); for (int cnt2 = 0; cnt2 < NumItems; cnt2++) { System.Console.Write(String.Format("{0} ", patterns[patIdForCol[cnt]][cnt2])); } System.Console.WriteLine(String.Format("\tUsage:{0} / {2} efficiency={1}%", usage[cnt - NumItems], (int)(100 * (double)usage[cnt - NumItems] / (double)ClothWidth), ClothWidth)); } } } System.Console.WriteLine(String.Format("Total scrap={0}", solver2.GetSolutionValue(goalIndex))); } } else { System.Console.WriteLine("Generated problem is infeasible. It is likely that more generated columns are needed."); } Console.WriteLine(); }
public List <Bag.Item> LinearProgramming(List <Item> items) { List <Bag.Item> choosenItems = new List <Bag.Item>(); int itemsCount = items.Count; double capacity = MaxWeightAllowed; SimplexSolver solver = new SimplexSolver(); //double[] estimatedProfitOfProjectX = new double[] { 1, 1.8, 1.6, 0.8, 1.4 }; //double[] capitalRequiredForProjectX = new double[] { 6, 12, 10, 4, 8 }; //double availableCapital = 20; //int[] chooseProjectX = new int[5]; double[] values = new double[itemsCount]; double[] weights = new double[itemsCount]; for (int i = 0; i < itemsCount; i++) { values[i] = items[i].Value; weights[i] = items[i].Weight; } int[] chosenItems = new int[itemsCount]; //int profit; //solver.AddRow("profit", out profit); //solver.AddGoal(profit, 0, false); int MaximumValue; solver.AddRow("MaximumValue", out MaximumValue); solver.AddGoal(MaximumValue, 0, false); //int expenditure; //solver.AddRow("expenditure", out expenditure); //solver.SetBounds(expenditure, 0, availableCapital); int MaximumWeight; solver.AddRow("MaximumWeight", out MaximumWeight); solver.SetBounds(MaximumWeight, 0, capacity); //for (int i = 0; i < 5; i++) //{ // solver.AddVariable(string.Format("project{0}", i), // out chooseProjectX[i]); // solver.SetBounds(chooseProjectX[i], 0, 1); // solver.SetIntegrality(chooseProjectX[i], true); // solver.SetCoefficient(profit, chooseProjectX[i], // estimatedProfitOfProjectX[i]); // solver.SetCoefficient(expenditure, chooseProjectX[i], // capitalRequiredForProjectX[i]); //} for (int i = 0; i < itemsCount; i++) { solver.AddVariable(string.Format("Item{0}", i), out chosenItems[i]); solver.SetBounds(chosenItems[i], 0, 1); solver.SetIntegrality(chosenItems[i], true); solver.SetCoefficient(MaximumValue, chosenItems[i], values[i]); solver.SetCoefficient(MaximumWeight, chosenItems[i], weights[i]); } //SimplexSolverParams param = new SimplexSolverParams(); //param.MixedIntegerGenerateCuts = true; //solver.Solve(param); SimplexSolverParams param = new SimplexSolverParams(); param.MixedIntegerGenerateCuts = true; solver.Solve(param); //Console.WriteLine(solver.MipResult); //for (int i = 0; i < 5; i++) //{ // Console.WriteLine("Project {0} is {1} selected.", i, // solver.GetValue(chooseProjectX[i]) == 1 ? "" : "not "); //} //Console.WriteLine("The estimated total profit is: ${0} million.", // (double)solver.GetValue(profit).ToDouble()); //Console.WriteLine("The total expenditure is: ${0} million.", // solver.GetValue(expenditure).ToDouble()); //string result = ""; for (int i = 0; i < itemsCount; i++) { if (solver.GetValue(chosenItems[i]) == 1) { //result += "Selected Item " + i + " "; choosenItems.Add(items[i]); } } //result += " value "+(double)solver.GetValue(MaximumValue).ToDouble()+ " weight "+ solver.GetValue(MaximumWeight).ToDouble(); return(choosenItems); }
public SimplexSolver PrepareSimplexSolver(int maxAgents, out Dictionary <Models.Shift, int> shiftsExt, out int vidGoal) { var SX = new SimplexSolver(); var maxRq = HalfHourRequirements.Max(x => x.RequiredForce); var sumRq = HalfHourRequirements.Sum(x => x.RequiredForce); shiftsExt = null; ////goal: total no of agents //int ridgoal; //SX.AddRow(key: "goal", vid: out ridgoal); //SX.SetBounds(ridgoal, lower: 0, upper: maxAgents); //total agents must range from 0 to max agents available //SX.AddGoal(vid: ridgoal, pri: 1, fMinimize: true); //minimize total number of agents for that day //goal: total no of agents, minimize overtime int ridgoal; SX.AddRow(key: "goal", vid: out ridgoal); SX.SetBounds(ridgoal, lower: 0, upper: Rational.PositiveInfinity); // upper: maxAgents); //total agents must range from 0 to max agents available SX.AddGoal(vid: ridgoal, pri: 2, fMinimize: true); //minimize total number of agents for that day //goal2: Minimize excess force int ridexcess; SX.AddRow(key: "excess", vid: out ridexcess); SX.SetBounds(ridexcess, lower: 0, upper: sumRq); //allow excess from 0 to sum of all requirements SX.AddGoal(vid: ridexcess, pri: 1, fMinimize: true); //try to minimize excess time var shiftsX = new Dictionary <Models.Shift, int>(); int i = 0; Shifts.ForEach(x => { int vid; SX.AddVariable(key: string.Format("{0}. {1} {2:hh}:{2:mm} - {3:hh}:{3:mm}", i + 1, x.Name, x.Start, x.End), vid: out vid); //the no of agents on each shift may not exceed to max requirements SX.SetBounds(vid: vid, lower: 0, upper: maxRq); shiftsX.Add(x, vid); //SX.SetCoefficient(vidRow: ridgoal, vidVar: vid, num: 1); //normal weight: all shifts are the same SX.SetCoefficient(vidRow: ridgoal, vidVar: vid, num: x.Duration.Hours <= 8 ? 1 : 10); //weights per shift: overtime = *10 more expensive than normal shifts i++; }); //Constraint - Agents from every active shift on every half hour must be >= requirement for that half hour HalfHourRequirements.ForEach(hh => { List <int> vidShiftsActive = new List <int>(); foreach (var entry in shiftsX) { if (entry.Key.IncludesHalfHour(hh.Start)) { vidShiftsActive.Add(entry.Value); } } //add constraint for sum of force of active shifts on that halfhour //if we need agents but no shifts exists for a halfhour, do not add a constraint if (vidShiftsActive.Count > 0) { int ridHalf; SX.AddRow(key: string.Format("{0:hh}:{0:mm} [{1}]", hh.Start, hh.RequiredForce), vid: out ridHalf); //specify whichs shifts contributes to half hour's total force vidShiftsActive.ForEach(vidShift => { SX.SetCoefficient(vidRow: ridHalf, vidVar: vidShift, num: 1); SX.SetCoefficient(vidRow: ridexcess, vidVar: vidShift, num: 1); }); //each 30' span, must have at least as many required agents SX.SetBounds(vid: ridHalf, lower: hh.RequiredForce, upper: Rational.PositiveInfinity); } }); shiftsExt = shiftsX; vidGoal = ridgoal; return(SX); }
public static RecipeGraph FromLibrary(Library library, IEnumerable <Item> inputs, IEnumerable <ItemAmount> outputs, Func <Item, double> costFunction) { if (library == null) { throw new ArgumentNullException("library"); } if (inputs == null) { throw new ArgumentNullException("inputs"); } if (outputs == null) { throw new ArgumentNullException("outputs"); } if (costFunction == null) { throw new ArgumentNullException("costFunction"); } var solver = new SimplexSolver(); var itemRows = new Dictionary <Item, int>(); var recipeVars = new Dictionary <Recipe, int>(); var wasteGoals = new Dictionary <Item, ILinearGoal>(); foreach (var item in library.Items) { int id; solver.AddRow(item, out id); itemRows.Add(item, id); solver.SetBounds(id, 0, Rational.PositiveInfinity); if (!inputs.Contains(item)) { wasteGoals.Add(item, solver.AddGoal(id, 1, true)); } } // Bound output to requested values foreach (var itemAmount in outputs) { var item = itemAmount.Item; var amount = itemAmount.Amount; solver.SetBounds(itemRows[item], amount, amount); } foreach (var recipe in library.Recipes) { int id; solver.AddVariable(recipe, out id); recipeVars.Add(recipe, id); solver.SetBounds(id, 0, Rational.PositiveInfinity); foreach (var input in recipe.Ingredients) { solver.SetCoefficient(itemRows[input.Item], id, -input.Amount); } foreach (var output in recipe.Results) { solver.SetCoefficient(itemRows[output.Item], id, output.Amount); } } // Add input minimize goals foreach (var item in inputs) { var row = itemRows[item]; solver.SetBounds(row, Rational.NegativeInfinity, 0); solver.AddGoal(row, (int)(10000 * costFunction(item)), false); } solver.Solve(new SimplexSolverParams()); if (solver.SolutionQuality != Microsoft.SolverFoundation.Services.LinearSolutionQuality.Exact) { throw new InvalidOperationException("Cannot solve problem"); } List <Tuple <Recipe, double> > usedRecipes = new List <Tuple <Recipe, double> >(); foreach (var recipe in library.Recipes) { var value = (double)solver.GetValue(recipeVars[recipe]); if (value > 0) { usedRecipes.Add(new Tuple <Recipe, double>(recipe, value)); } } var sortedRecipes = usedRecipes.SortTopological((a, b) => a.Item1.Results.Select((i) => i.Item).Intersect(b.Item1.Ingredients.Select((i) => i.Item)).Any(), true); List <SourceStep> inputSteps = new List <SourceStep>(); List <SinkStep> outputSteps = new List <SinkStep>(); List <SinkStep> wasteSteps = new List <SinkStep>(); Dictionary <Item, FlowStep> itemSteps = new Dictionary <Item, FlowStep>(); List <FlowStep> flowSteps = new List <FlowStep>(); List <TransformStep> transformSteps = new List <TransformStep>(); foreach (var item in library.Items) { var value = (double)solver.GetValue(itemRows[item]); if (value > 0) { var sink = new SinkStep(new ItemAmount(item, value / 2)); if (outputs.Select((o) => o.Item).Contains(item)) { outputSteps.Add(sink); } else { wasteSteps.Add(sink); } itemSteps.Add(item, sink); } else if (value < 0) { var source = new SourceStep(new ItemAmount(item, -value)); inputSteps.Add(source); itemSteps.Add(item, source); } } foreach (var recipe in sortedRecipes) { foreach (var result in recipe.Item1.Results) { var item = result.Item; if (!itemSteps.ContainsKey(item)) { var flowstep = new FlowStep(new ItemAmount(item, 0)); itemSteps.Add(item, flowstep); flowSteps.Add(flowstep); } } } foreach (var recipe in sortedRecipes) { var previous = recipe.Item1.Ingredients.Select((i) => i.Item); var step = new TransformStep(recipe.Item1, recipe.Item2); foreach (var item in previous) { var prev = itemSteps[item]; step.Previous.Add(prev); } foreach (var amount in recipe.Item1.Results) { var item = amount.Item; itemSteps[item].Previous.Add(step); itemSteps[item].Item += amount * recipe.Item2; } transformSteps.Add(step); } return(new RecipeGraph(wasteSteps, inputSteps, outputSteps, flowSteps, transformSteps)); }