Last active
July 7, 2022 03:42
-
-
Save Vake93/884df22dd4936f6df2a2469ef04551c3 to your computer and use it in GitHub Desktop.
Fertilizer Ratio Finder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Models; | |
var slsSpecification = new Specification(C: 20.00f, N: 1.00f, P: 0.50f, K: 1.00f, Ca: 0.70f, Mg: 0.50f); | |
var pineapplePeel = new Sample(Name: "Pineapple Peel" , Seasonal: true , C: 59.43f, N: 0.80f, P: 0.21f, K: 2.00f, Ca: 0.39f, Mg: 0.23f); | |
var bananaStem = new Sample(Name: "Banana Stem" , Seasonal: false, C: 59.75f, N: 0.62f, P: 0.22f, K: 7.43f, Ca: 0.58f, Mg: 0.33f); | |
var bananaPeel = new Sample(Name: "Banana Peel" , Seasonal: false, C: 81.29f, N: 0.04f, P: 0.09f, K: 5.98f, Ca: 0.60f, Mg: 0.36f); | |
var breadfruit = new Sample(Name: "Breadfruit" , Seasonal: true , C: 82.36f, N: 1.06f, P: 0.06f, K: 1.58f, Ca: 1.12f, Mg: 0.25f); | |
var jackfruit = new Sample(Name: "Jackfruit" , Seasonal: true , C: 70.79f, N: 1.12f, P: 0.15f, K: 1.21f, Ca: 0.52f, Mg: 0.36f); | |
var tea = new Sample(Name: "Tea" , Seasonal: false, C: 86.04f, N: 3.14f, P: 0.13f, K: 0.38f, Ca: 0.76f, Mg: 0.38f); | |
var avocadoPeel = new Sample(Name: "Avocado Peel" , Seasonal: true , C: 84.20f, N: 0.72f, P: 0.26f, K: 0.79f, Ca: 0.12f, Mg: 0.26f); | |
var mangoPeel = new Sample(Name: "Mango Peel" , Seasonal: true , C: 56.04f, N: 0.11f, P: 0.37f, K: 0.75f, Ca: 0.42f, Mg: 0.34f); | |
var onionPeel = new Sample(Name: "Onion Peel" , Seasonal: false, C: 59.25f, N: 0.21f, P: 0.06f, K: 0.61f, Ca: 1.60f, Mg: 0.41f); | |
var marigoldLeaves = new Sample(Name: "Marigold Leaves" , Seasonal: false, C: 46.53f, N: 0.45f, P: 0.13f, K: 2.95f, Ca: 2.72f, Mg: 0.91f); | |
var gliricidiaLeaves = new Sample(Name: "Gliricidia Leaves", Seasonal: false, C: 86.06f, N: 0.04f, P: 0.23f, K: 1.30f, Ca: 2.27f, Mg: 0.72f); | |
var scrapedCoconut = new Sample(Name: "Scraped Coconut" , Seasonal: false, C: 96.02f, N: 0.12f, P: 0.04f, K: 0.16f, Ca: 0.08f, Mg: 0.14f); | |
var eggShells = new Sample(Name: "Egg Shells" , Seasonal: false, C: 04.53f, N: 0.09f, P: 0.07f, K: 0.03f, Ca: 6.06f, Mg: 0.59f); | |
while (true) | |
{ | |
Console.Clear(); | |
Console.WriteLine("Optimizing..."); | |
var mixture = OptimizeRatios(bananaStem, bananaPeel, onionPeel, marigoldLeaves, gliricidiaLeaves, scrapedCoconut, eggShells, pineapplePeel, tea); | |
//var mixture = OptimizeRatios(bananaStem, bananaPeel, onionPeel, marigoldLeaves, gliricidiaLeaves, scrapedCoconut, eggShells, breadfruit, tea); | |
//var mixture = OptimizeRatios(bananaStem, bananaPeel, onionPeel, marigoldLeaves, gliricidiaLeaves, scrapedCoconut, eggShells, jackfruit, tea); | |
//var mixture = OptimizeRatios(bananaStem, bananaPeel, onionPeel, marigoldLeaves, gliricidiaLeaves, scrapedCoconut, eggShells, avocadoPeel, tea); | |
//var mixture = OptimizeRatios(bananaStem, bananaPeel, onionPeel, marigoldLeaves, gliricidiaLeaves, scrapedCoconut, eggShells, mangoPeel, tea); | |
Console.Clear(); | |
PrettyPrint(mixture); | |
Console.WriteLine("Press Q to exit, any other key to retry."); | |
var input = Console.ReadKey(); | |
if (input.KeyChar == 'Q' || input.KeyChar == 'q') | |
{ | |
break; | |
} | |
} | |
void PrettyPrint(CandidateMixture mixture) | |
{ | |
Console.WriteLine($"Target : {slsSpecification}"); | |
Console.WriteLine($"Actual : {mixture.GetSpecification()}"); | |
Console.WriteLine(); | |
Console.WriteLine($"Weights in {Math.Round(mixture.Weight, 2):0.00}g sample:"); | |
foreach (var component in mixture.MixtureComponents.OrderBy(mc => mc.WeightInGrams)) | |
{ | |
var (_, top) = Console.GetCursorPosition(); | |
var seasonal = component.Sample.Seasonal ? "(S)" : "(NS)"; | |
Console.SetCursorPosition(5, top); | |
Console.Write(component.Sample.Name); | |
Console.SetCursorPosition(30, top); | |
Console.Write(seasonal); | |
Console.SetCursorPosition(40, top); | |
Console.Write($"{component.WeightInGrams:00.00}g"); | |
Console.WriteLine(); | |
} | |
Console.WriteLine(); | |
Console.WriteLine(); | |
} | |
CandidateMixture OptimizeRatios(params Sample[] components) | |
{ | |
const double MAX_TEMPERATURE = 2000000000; | |
const double COOLING_RATE = 0.0001; | |
var parentMixture = GetRandomMixture(components); | |
var bestMixture = parentMixture; | |
var bestEnergy = CalculateEnergy(bestMixture, slsSpecification); | |
var currentMixture = parentMixture; | |
var temperature = MAX_TEMPERATURE; | |
var historicalRatios = new HashSet<string>(); | |
while (temperature > 1) | |
{ | |
var newMixture = UpdateMixture(currentMixture, historicalRatios); | |
var newEnergy = CalculateEnergy(newMixture, slsSpecification); | |
if (newEnergy <= bestEnergy) | |
{ | |
bestMixture = newMixture; | |
bestEnergy = newEnergy; | |
currentMixture = newMixture; | |
} | |
temperature *= 1 - COOLING_RATE; | |
} | |
return bestMixture; | |
} | |
CandidateMixture GetRandomMixture(params Sample[] components) | |
{ | |
const float MAX_WEIGHT = 100.0f; | |
if (components.Length == 0) | |
{ | |
throw new ArgumentException("One or more components required"); | |
} | |
var remainingWeight = MAX_WEIGHT; | |
var randomeMixture = new CandidateMixture(); | |
foreach (var component in components) | |
{ | |
var componentWeight = (float)Math.Round(Random.Shared.NextSingle() * remainingWeight, 2); | |
randomeMixture.AddComponent(new MixtureComponent(component, componentWeight)); | |
remainingWeight -= componentWeight; | |
} | |
return randomeMixture; | |
} | |
CandidateMixture UpdateMixture(CandidateMixture mixture, HashSet<string> historicalRatios) | |
{ | |
historicalRatios.Add(mixture.GetRatioString()); | |
var components = mixture.MixtureComponents.ToArray(); | |
const float MAX_WEIGHT = 100.0f; | |
var remainingWeight = MAX_WEIGHT; | |
var modifiedMixture = new CandidateMixture(); | |
while (true) | |
{ | |
var randomOrderedComponents = components.OrderBy(_ => Random.Shared.NextSingle()).ToArray(); | |
for (int i = 0; i < randomOrderedComponents.Length; i++) | |
{ | |
var sample = randomOrderedComponents[i].Sample; | |
// if this is the last component we take the remaining weight as the weight of the component | |
var componentWeight = (i == randomOrderedComponents.Length - 1) ? | |
remainingWeight : | |
(float)Math.Round(Random.Shared.NextSingle() * remainingWeight, 2); | |
modifiedMixture.AddComponent(new MixtureComponent(sample, componentWeight)); | |
remainingWeight -= componentWeight; | |
} | |
var ratio = modifiedMixture.GetRatioString(); | |
if (historicalRatios.Contains(ratio)) | |
{ | |
modifiedMixture.ResetComponents(); | |
continue; | |
} | |
break; | |
} | |
return modifiedMixture; | |
} | |
float CalculateEnergy(CandidateMixture mixture, Specification targetSpecification) | |
{ | |
const int PENTALTY = 1000; | |
var energy = 0f; | |
var specification = mixture.GetSpecification(); | |
var seasonalComponent = mixture.MixtureComponents.FirstOrDefault(c => c.Sample.Seasonal); | |
// Seasonal Component should be at least 1% w/w | |
if (seasonalComponent is not null && seasonalComponent.WeightInGrams < 1.0f) | |
energy += PENTALTY; | |
// All components should be greater than 0.1% w/w | |
var unusedComponentCount = mixture.MixtureComponents.Count(c => c.WeightInGrams <= 0.1f); | |
energy += PENTALTY * unusedComponentCount; | |
//Checks if the target specification is met | |
if (targetSpecification.C > specification.C) | |
energy += (targetSpecification.C - specification.C) * PENTALTY; | |
if (targetSpecification.N > specification.N) | |
energy += (targetSpecification.N - specification.N) * PENTALTY; | |
if (targetSpecification.P > specification.P) | |
energy += (targetSpecification.P - specification.P) * PENTALTY; | |
if (targetSpecification.K > specification.K) | |
energy += (targetSpecification.K - specification.K) * PENTALTY; | |
if (targetSpecification.Ca > specification.Ca) | |
energy += (targetSpecification.Ca - specification.Ca) * PENTALTY; | |
if (targetSpecification.Mg > specification.Mg) | |
energy += (targetSpecification.Mg - specification.Mg) * PENTALTY; | |
return energy; | |
} | |
namespace Models | |
{ | |
public record Specification(float C, float N, float P, float K, float Ca, float Mg) | |
{ | |
public override string ToString() => $"C: {C:00.00}%, N: {N:00.00}%, P: {P:00.00}%, K: {K:00.00}%, Ca: {Ca:00.00}%, Mg: {Mg:00.00}%"; | |
} | |
public record Sample(string Name, bool Seasonal, float C, float N, float P, float K, float Ca, float Mg); | |
public record MixtureComponent(Sample Sample, float WeightInGrams) | |
{ | |
public float C => (Sample.C / 100) * WeightInGrams; | |
public float N => (Sample.N / 100) * WeightInGrams; | |
public float P => (Sample.P / 100) * WeightInGrams; | |
public float K => (Sample.K / 100) * WeightInGrams; | |
public float Ca => (Sample.Ca / 100) * WeightInGrams; | |
public float Mg => (Sample.Mg / 100) * WeightInGrams; | |
public override int GetHashCode() => HashCode.Combine(Sample.Name, C, N, P, K, Ca, Mg); | |
} | |
public class CandidateMixture | |
{ | |
private readonly HashSet<MixtureComponent> _mixtureComponents; | |
public CandidateMixture() | |
{ | |
_mixtureComponents = new HashSet<MixtureComponent>(); | |
} | |
public IReadOnlyCollection<MixtureComponent> MixtureComponents => _mixtureComponents; | |
public float Weight => _mixtureComponents.Sum(c => c.WeightInGrams); | |
public void AddComponent(MixtureComponent component) | |
{ | |
if (_mixtureComponents.Contains(component)) | |
{ | |
throw new ArgumentException("Same mixture component already exists"); | |
} | |
_mixtureComponents.Add(component); | |
} | |
public void ResetComponents() => _mixtureComponents.Clear(); | |
public string GetRatioString() => string.Join('/', _mixtureComponents.OrderBy(mc => mc.Sample.Name).Select(mc => mc.WeightInGrams.ToString("00.00"))); | |
public Specification GetSpecification() | |
{ | |
var c = 0f; | |
var n = 0f; | |
var p = 0f; | |
var k = 0f; | |
var ca = 0f; | |
var mg = 0f; | |
var totalWeight = 0f; | |
foreach (var component in _mixtureComponents) | |
{ | |
c += component.C; | |
n += component.N; | |
p += component.P; | |
k += component.K; | |
ca += component.Ca; | |
mg += component.Mg; | |
totalWeight += component.WeightInGrams; | |
} | |
return new( | |
(float)Math.Round(c / totalWeight * 100, 2), | |
(float)Math.Round(n / totalWeight * 100, 2), | |
(float)Math.Round(p / totalWeight * 100, 2), | |
(float)Math.Round(k / totalWeight * 100, 2), | |
(float)Math.Round(ca / totalWeight * 100, 2), | |
(float)Math.Round(mg / totalWeight * 100, 2)); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment