Skip to content

Instantly share code, notes, and snippets.

@Vake93
Last active July 7, 2022 03:42
Show Gist options
  • Save Vake93/884df22dd4936f6df2a2469ef04551c3 to your computer and use it in GitHub Desktop.
Save Vake93/884df22dd4936f6df2a2469ef04551c3 to your computer and use it in GitHub Desktop.
Fertilizer Ratio Finder
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