"""
This file contains the more advanced functions on solutions and solubility used in the project.
In particular, the file contains functions to:
- Check the solubility of a solution
- Analyse the nutriments in the solution
- Update the solution after 1 day of growth
- Plot the evolution of the solution
"""
#Imports
import pandas as pd
import math
import matplotlib.pyplot as plt
import os
import numpy as np
from .Basic_functions import get_molar_mass, get_Ksp, get_Q_solubility, salt2ions, dict_salts_trad
# ----------------- Analyse the solution -----------------
#Check the solubility of the solution
[docs]
def check_solubility(salts_dict: dict, input_type: str = "salt", output_type: str = "bool") -> bool:
"""
Check if the salts mixture is soluble.
Args:
salts_dict (dict): Dictionary with keys as salt names and values as concentrations [g/L].
input_type (str): Type of input data, either "salt" or "ion".
output_type (str): Type of output data, either "bool", "analysis", or "update".
Returns:
bool or str: True if salts are soluble (if output_type is "bool").
Analysis of precipitated salts or indication of solubility (if output_type is "analysis").
"""
# Validate input and output types
valid_input_types = {"salt", "ion"}
valid_output_types = {"bool", "analysis", "update"}
if input_type not in valid_input_types:
raise ValueError("Invalid input type. Please choose either 'salt' or 'ion'.")
if output_type not in valid_output_types:
raise ValueError("Invalid output type. Please choose either 'bool', 'analysis', or 'update'.")
#Handle type of input and convert to mol/L
if input_type == "salt":
salts_dict_molar= {salt: concentration / get_molar_mass(salt) for salt, concentration in salts_dict.items()}
ions = salt2ions(salts_dict_molar, volume = 1)
if input_type == "ion":
ions = {ion: concentration / get_molar_mass(ion) for ion, concentration in salts_dict.items()}
#Initialize variables
soluble = True
precipitate = []
salts = dict_salts_trad.keys()
#Compare Q and Ksp for each salt is the Ksp value is provided
for salt in salts:
try:
Ksp = get_Ksp(salt)
Q = get_Q_solubility(salt, ions)
if Q > Ksp and set(dict_salts_trad[salt].keys()).issubset(set(ions.keys())):
soluble = False
precipitate.append(salt)
except:
pass
#Handle type of output
if output_type == "bool":
return soluble
elif output_type == "analysis":
if soluble:
return "all salts are soluble"
else:
text = f"The following salts precipitate: "
for salt in precipitate:
text+=str(salt)+" "
return text
elif output_type == "update":
return precipitate
#analyse the nutriments in the solution
[docs]
def analyse_nutriments(solution: dict, plant: dict, growth_time: float, volume: float, input_type_solution: str= 'salt') -> list:
"""
Returns the number of days a plant can grow with the given solution.
Args:
solution (dict): A dictionary containing salts or ions and their concentrations in the solution.
input_type (str): Type of input data, either "salt" or "ion".
plant (dict): A dictionary containing ions and their required quantity for plant growth.
growth_time (float): Expected growth time of the plant [days].
volume (float): Volume of the solution [L].
*Note: the unit of solution and plant must be the same. (either mol/L or g/L)
Returns:
list: A list containing three elements:
- bool: True if the solution contains enough nutriments for the plant, False otherwise.
- int: Minimum days of growth achievable with the given solution.
- str: Ion that limits the plant growth.
Raises:
ValueError: If the volume or growth time is non-positive.
"""
#Check validity of Args
if volume <= 0:
raise ValueError("Volume must be positive.")
if growth_time <= 0:
raise ValueError("Growth time must be positive.")
if input_type_solution not in {"salt", "ion"}:
raise ValueError("Invalid input type. Please choose either 'salt' or 'ion'.")
#Initialisation
if input_type_solution == "salt":
ions = salt2ions(solution, volume=volume)
if input_type_solution == "ion":
ions = {ion: solution[ion]*volume for ion in solution.keys()}
daily_plant_need = {ion: plant[ion]/growth_time for ion in plant.keys()}
limiting_ion = None
growth_limit = growth_time
#Determine limiting ion, minimum days of growth and if growth time exceeds the minimum days
for ion in ions:
if ion in daily_plant_need:
days = math.floor(ions[ion] / daily_plant_need[ion])
if days < growth_limit:
growth_limit = days
limiting_ion = ion
if set(plant.keys()).issubset(set(ions.keys())):
return [growth_limit == growth_time, growth_limit, limiting_ion]
else:
missing_ions = [ion for ion in plant.keys() if ion not in ions]
return [False, 0, missing_ions]
#Check is enaugh nutriments are present in the solution
[docs]
def check_supply_elements(ions_solution, plant) -> bool:
for ion in plant.keys():
if ion not in ions_solution:
raise ValueError("{ion} is not present in the solution but is required for the plant growth.")
elif plant[ion] > ions_solution[ion]:
return False
return True
# ----------------- Evolution of the solution -----------------
#Update the solution after 1 day of growth
[docs]
def update_sol(ions_solution: dict, plant: dict, volume: float) -> dict:
"""
updates the salts in solutions after 1 day of growth and checks the solubility of the new solution
Args:
ions_solution: dictionary with keys as ions and values as the amount of ions in the solution [g/L]
plant: dictionary with keys as ions and values as the amount of ions needed for 1 day for the plant [g]
Returns:
dictionary: updated ions_solution after 1 day of growth and checking the solubility
"""
volume_plant = {ion: plant[ion]/volume for ion in plant.keys()}
for ion in volume_plant:
if ions_solution[ion] <= volume_plant[ion]:
ions_solution[ion] = 0
else:
ions_solution[ion] -= volume_plant[ion]
if check_solubility(ions_solution, input_type="ion"):
return ions_solution
else:
unsoluble_ions = check_solubility(ions_solution, input_type ="ion", output_type = "update")
for ion in unsoluble_ions:
ions_solution[ion] = 0
return ions_solution
# Evolution of the concentration as the plant grows (internal function)
[docs]
def data4graph(solution: dict, volume: float, plant: dict, growth_time: float) -> list:
"""
Creates a dictionary with the data needed to plot the graph
Args:
solution (dict): Initial concentration [g/L] of the ions in the solution.
volume (float): Volume of the solution [L].
plant (dict): Dictionary containing required amount of ions for the plant growth.
growth_time (float): Expected growth time of the plant [days].
Returns:
list: A list containing two elements:
- list: A list of days. [0,1,2,3,4,5,6,...]
- list: A list of dictionaries with the concentration of ions in the solution for each day.
"""
#Initialisation:
daily_need_plant = {ion: plant[ion]/growth_time for ion in plant.keys()}
data = [[],[]] #data = [[0,1,2,3,4,5,6,...],[{"Na",0.1,"K",0.2,...}, {"Na": 0.05, "K": 0.1,...},...]]
i=0
data[0].append(i)
data[1].append(solution.copy())
#Loop trough further values
for i in range(1,growth_time+1):
if check_supply_elements(solution, daily_need_plant):
solution = update_sol(solution, daily_need_plant, volume)
data[0].append(i)
data[1].append(solution.copy())
else:
data[0].append(i)
data[1].append(solution.copy())
return data
#List of colors for the graph
python_colors = ['b', 'g', 'c', 'm', 'y', 'k',
'tab:blue', 'tab:orange', 'tab:green', 'tab:red', 'tab:purple', 'tab:brown', 'tab:pink', 'tab:gray', 'tab:olive', 'tab:cyan',
'mediumblue', 'darkorange', 'limegreen', 'indianred', 'violet', 'sienna', 'deepskyblue', 'peru', 'gold']
#Plot the evolution of the solution
[docs]
def plot_graph(solution: dict, input_type:str, plant:dict, growth_time:float, volume:float = 1, ions_of_interest: list = "all"):
"""
Creates graph of salts in hydroponic solution
Args:
salts (dict): Initial concentration [g/L] of the ions in the solution.
input_type (str): Type of input data, either "salt" or "ion".
plant (dict): the plant requirements to fully grow [g].
growth_time (float): Expected growth time of the plant [days].
volume (float, optional): Volume of the solution [L]. Defaults to 1.
ions_of_interest (list, optional): list of ions to plot. Defaults to "all".
Elements must be part of the keys of solution.
Returns:
None, saves the graph as a .png file in the download folder.
"""
#Initialise the figure and aesthetics
plt.figure(figsize=(12, 6), dpi = 500)
plt.title('Evolution of the Ions in hydroponic solution', fontsize=16, weight='bold')
plt.xlabel(f'Time [days]', fontsize=14)
plt.ylabel('Concentration of the ion [g/L]', fontsize=14)
plt.grid(False)
plt.gca().set_facecolor('#f9f9f9')
plt.gca().spines['top'].set_visible(False)
plt.gca().spines['right'].set_visible(False)
# Setting y-axis to log scale
#plt.yscale('log')
#xlength = analyse_nutriments(solution, plant, growth_time, volume, input_type_solution = 'ion')[1]
#Handle Input type
if input_type == "salt":
initial_ion_solution = salt2ions(solution, volume)
else:
initial_ion_solution = solution.copy()
#add the growth limit
analysis = analyse_nutriments(initial_ion_solution, plant, growth_time, volume, input_type_solution = 'ion')
if analysis[1] != growth_time:
plt.axvline(x=analysis[1], color='r', linestyle='--', label=f'Growth limit: insufficient {analysis[2]}')
#Plot the data for the ions of interest
i = 0 #handle the colors
data = data4graph(initial_ion_solution, volume, plant, growth_time)
for ion in initial_ion_solution.keys():
if ion in ions_of_interest or ions_of_interest == "all":
x = data[0]
y = []
for j in range(len(x)):
y.append(data[1][j][ion])
plt.plot(x, y, label = ion, color = python_colors[i],alpha=0.8, linewidth=1.5, linestyle='-', marker='o', markersize=3)
i+=1
plt.legend(loc='best')
#Save Graph to the current directory
current_dir = os.path.dirname(os.path.realpath(__file__))
file_name = "graph_"
for ion in ions_of_interest:
file_name += str(ion) + "_"
file_path = os.path.join(current_dir, file_name)
plt.savefig(file_path + ".png", format='png') # Save as a PNG file for matplotlib