Source code for hydroponics.Basic_functions

"""
This file contains the most basic functions used in the project.
In particular, the file contains functions to:
- Import user data from an Excel file
- Calculate the molar mass of a salt
- Get the solubility product constant (Ksp) and solubility product (Q) of a salt
- Convert salt concentrations to ion concentrations
- Make a solution given the ion concentration wanted
"""

#Imports
import re
import os
import csv
import pandas as pd
from sympy import symbols, Eq, solve, solveset, S, Interval 


# ----- Import User Data from Excel file -------------

[docs] def load_from_data(file_name): # Get the directory of the current file current_dir = os.path.dirname(os.path.realpath(__file__)) # Construct the absolute path to the CSV file path = os.path.join(current_dir, '..\\..\\data', file_name) return path
[docs] def import_solution_data(solution_name: str) -> dict: """ Import user data from the Excel file "UserData.xlsx" and returns it as a dictionary. Args: solution_name (str): Name of the column in the Excel file. Returns: dict: Dictionary containing the concentration of the ions [g/L] in the solution. """ # Read the Excel file file_name = load_from_data("UserData.xlsx") df = pd.read_excel(file_name, sheet_name="My solution", skiprows=1) # Create a dictionary using zip and dictionary comprehension try: ion_solution_dict = dict(zip(df['Ions'], df[solution_name])) except KeyError: raise ValueError(f"Solution {solution_name} not found in the Excel file. Please check the name of the column.") return ion_solution_dict
[docs] def predefined_solutions(plant:str, concentration:str="g" ) -> dict: """ Import user data from the Excel file "UserData.xlsx" and returns it as a dictionary. Args: plant (str): Name of the plant. Can either be 'Eggplant', 'Tomato' or 'Cucumber'. concentration (str): Unit of concentration. Can either be 'g' or 'mol'. Returns: dict: Dictionary containing the concentration of the ions [g or mol/L] in the solution. """ if concentration not in ['g','mol']: raise ValueError("Concentration must be 'g' or 'mol'.") available_plants = ['Eggplant', 'Tomato', 'Cucumber'] if plant not in available_plants: raise ValueError(f"Plant {plant} not found. Available plants are {available_plants}.") else: # Read the Excel file file_name = load_from_data("UserData.xlsx") df = pd.read_excel(file_name, sheet_name="Optimal Solution", skiprows=0) # Create a dictionary using zip and dictionary comprehension ion_solution_dict = dict(zip(df['Formula'], df[str('c ['+concentration+'/L] '+plant)])) return ion_solution_dict
[docs] def import_plant_data(plant_name:str) -> dict: """ Import user data from sheet "My Plant" from the Excel file "UserData.xlsx" and returns it as a dictionary. Args: plant (str): name of the column where the data is located Returns: dict: dictionary containing the needs of the plant for each ion [g] for a full growth cycle """ # Read the Excel file file_name = load_from_data("UserData.xlsx") df = pd.read_excel(file_name, sheet_name="My plant", skiprows=1) # Create a dictionary using zip and dictionary comprehension plant_dict = dict(zip(df['Ions'], df[plant_name])) return plant_dict
# ----- Molar mass calculator ------------------------
[docs] def get_atom_mass(atom_name: str) -> float: """ Gets the atomic mass of the atom from a CVS file Args: atom_name (str): Name of the atom, i.e. 'H', 'O', 'C', etc. Returns: float: atomic mass of the atom in g/mol """ # Define the name of the CSV file data_file = load_from_data("molar_mass.csv") #Check path to CSV file with open(data_file, mode='r') as file: reader = csv.DictReader(file) for row in reader: if row['Atom'] == atom_name: return float(row['Molar mass [g/mol]'])
[docs] def get_molar_mass(salt: str) -> float: """ Calculate the molar mass of a salt in g/mol. Args: salt (str): The chemical formula of the salt. Handles chemical formulas of the following types: - "Ca(NO3)2" - "Ca(2+)(NO3-)2" - "Ca2(NO3)2" - "Ca(NO3)2(2+)" Please do not use spaces in the formula and avoid putting brackets inside brackets. Returns: float: The molar mass of the salt in g/mol. """ #Check for special characters def contains_special_characters(input_string:str): # Define a regular expression pattern to match special characters pattern = re.compile(r'[!@#$%^&*_=\[\]{};\'\\:"|,.<>\/?]') # Use search method to find if any special characters exist in the input string if pattern.search(input_string): return True else: return False def molar(molecule:str): #Check for special characters if contains_special_characters(molecule): raise ValueError("Invalid input. The formula of the molcule can only contain letters,numbers, parentheses and '+' or '-' signs.") current_count = "" current_element = "" i=0 molar_mass = 0 #Test for charges and numbers while molecule[-1] == "+" or molecule[-1] == "-": molecule = molecule[:-1] if len(molecule) == 0: break if molecule.isnumeric(): return 0 while i < len(molecule): if i<len(molecule)-3: if molecule[i].isupper() and molecule[i+1].islower() and molecule[i+2].isnumeric() and molecule[i+3].isnumeric(): current_element = molecule[i]+molecule[i+1] current_count = molecule[i+2]+molecule[i+3] i+=4 molar_mass += get_atom_mass(current_element)*int(current_count) continue if i<len(molecule)-2: if molecule[i].isupper() and molecule[i+1].islower() and molecule[i+2].isnumeric(): current_element = molecule[i]+molecule[i+1] current_count = molecule[i+2] i+=3 molar_mass += get_atom_mass(current_element)*int(current_count) continue if molecule[i].isupper() and molecule[i+1].islower() and molecule[i+2].isalpha(): current_element = molecule[i]+molecule[i+1] current_count = 1 i+=2 molar_mass += get_atom_mass(current_element)*int(current_count) continue if molecule[i].isupper() and molecule[i+1].isnumeric() and molecule[i+2].isnumeric(): current_element = molecule[i] current_count = molecule[i+1]+molecule[i+2] i+=2 molar_mass += get_atom_mass(current_element)*int(current_count) continue if i<len(molecule)-1: if molecule[i].isupper() and molecule[i+1].isnumeric(): current_element = molecule[i] current_count = molecule[i+1] i+=2 molar_mass += get_atom_mass(current_element)*int(current_count) continue elif molecule[i].isupper() and molecule[i+1].isupper(): current_element = molecule[i] current_count = 1 i+=1 molar_mass += get_atom_mass(current_element)*int(current_count) continue if i == len(molecule)-2: if molecule[i].isupper() and molecule[i+1].islower(): current_element = molecule[i]+molecule[i+1] current_count = 1 i+=2 molar_mass += get_atom_mass(current_element)*int(current_count) continue if molecule[i].isupper() and molecule[i+1].isupper(): current_element = molecule[i] current_count = 1 i+=1 molar_mass += get_atom_mass(current_element)*int(current_count) continue if molecule[i].isupper() and molecule[i+1].isnumeric(): current_element = molecule[i] current_count = molecule[i+1] i+=2 molar_mass += get_atom_mass(current_element)*int(current_count) continue if i == len(molecule)-1: current_element = molecule[i] current_count = 1 i+=1 molar_mass += get_atom_mass(current_element)*int(current_count) continue if molar_mass >=100000: break return molar_mass #Take care of brakets M = 0 while "(" in salt: left = salt.index("(") right = salt.index(")") substing = salt[left+1:right] if right != len(salt)-1: if salt[right+1].isnumeric(): M += int(salt[right+1])*molar(substing) salt = salt[:left] + salt[right+2:] else: M += molar(substing) salt = salt[:left] + salt[right+1:] else: M += molar(substing) salt = salt[:left] + salt[right+1:] total_molar_mass = M + molar(salt) return total_molar_mass
#------ Basic Solubility functions ----------------------------- #Dictionaries used to handle salts with multiple ions, extend dictionaries if needed salt2nbIons = { "Ca(NO3)2": [1,2], "MgSO4": [1,1], "KNO3": [1,1], "KH2PO4": [1,1], "H3BO3": [1], "MnCl2": [1,2], "ZnSO4": [1,1], "CuSO4": [1,1], "(NH4)6Mo7O24": [6,1], "C10H12FeN2NaO8": [1,1,1], "Fe(III)EDTANa": [1,1,1], # "(NH4)H2PO4" : [1,1], "NaCl": [1,1], "NaOH": [1,1], "Na2CO3": [2,1], "Na2SO4": [2,1], "NaHCO3": [1,1], "Na2S": [2,1], "KCl": [1,1], "KOH": [1,1], "K2SO4": [2,1], "K2CO3": [2,1], "KHCO3": [1,1], "CaCl2": [1,2], "Ca(OH)2": [1,2], "CaCO3": [1,1], "MgCl2": [1,2], "Mg(OH)2": [1,2], "MgCO3": [1,1], "FeSO4": [1,1], "FeCl3": [1,3], "AlCl3": [1,3], "KBr" : [1,1], "Mg(NO3)2": [1,2], "Zn(NO3)2": [1,2], "Cu(NO3)2": [1,2] } dict_salts_trad = { "Ca(NO3)2": {"Ca(2+)": 1, "NO3(-)": 2}, "MgSO4": {"Mg(2+)": 1, "SO4(2-)": 1}, "KNO3": {"K+": 1, "NO3(-)": 1}, "KH2PO4": {"K+": 1, "H2PO4(-)": 1}, "H3BO3": {"B": 1}, "MnCl2": {"Mn(2+)": 1, "Cl-": 2}, "ZnSO4": {"Zn(2+)": 1, "SO4(2-)": 1}, "CuSO4": {"Cu(2+)": 1, "SO4(2-)": 1}, "(NH4)6Mo7O24": {"NH(4+)" : 6, "Mo": 1}, "C10H12FeN2NaO8": {"Fe(3+)": 1, "Na+": 1, "EDTA": 1}, "Fe(III)EDTANa": {"Fe(3+)": 1, "Na+": 1, "EDTA": 1}, "(NH4)H2PO4" : {"NH4+": 1, "H2PO4(-)": 1}, "NaCl": {"Na+": 1, "Cl-": 1}, "NaOH": {"Na+": 1, "OH-": 1}, "Na2CO3": {"Na+": 2, "CO3": 1}, "Na2SO4": {"Na+": 2, "SO4(2-)": 1}, "NaHCO3": {"Na+": 1, "HCO3(-)": 1}, "Na2S": {"Na+": 2, "S(2-)": 1}, "KCl": {"K+": 1, "Cl-": 1}, "KOH": {"K+": 1, "OH-": 1}, "K2SO4": {"K+": 2, "SO4(2-)": 1}, "K2CO3": {"K+": 2, "CO3(2-)": 1}, "KHCO3": {"K+": 1, "HCO3(-)": 1}, "CaCl2": {"Ca(2+)": 1, "Cl-": 2}, "Ca(OH)2": {"Ca(2+)": 1, "OH-": 2}, "CaCO3": {"Ca(2+)": 1, "CO3(2-)": 1}, "MgCl2": {"Mg(2+)": 1, "Cl-": 2}, "Mg(OH)2": {"Mg(2+)": 1, "OH-": 2}, "MgCO3": {"Mg(2+)": 1, "CO3(2-)": 1}, "FeSO4": {"Fe(2+)": 1, "SO4(2-)": 1}, "FeCl3": {"Fe(3+)": 1, "Cl-": 3}, "AlCl3": {"Al(3+)": 1, "Cl-": 3}, "KBr" : {"K+": 1, "Br-": 1}, "Mg(NO3)2": {"Mg(2+)": 1, "NO3(-)": 2}, "Zn(NO3)2": {"Zn(2+)": 1, "NO3(-)": 2}, "Cu(NO3)2": {"Cu(2+)": 1, "NO3(-)": 2} } # Get the solubility constant Ksp of a salt
[docs] def get_Ksp(salt_name: str) -> float: """ Access the solubility product constant (Ksp) of a salt based on its name. Uses a salt2nbIons dictionary to find the number of ions in a salt. Args: salt_name (str): The formula of the salt. Use standard notation for the formula, e.g. "Ca(NO3)2", "H2O", etc. Returns: float: The solubility product constant (Ksp) of the salt. """ solubility_file = load_from_data("Solubility_data.csv") df_solubility = pd.read_csv(solubility_file) row = df_solubility.loc[df_solubility["Formula"] == salt_name].values.tolist() # [[name, formula, value]] Ksp = 0 if row: solubility = float(row[0][2]) *10 #go from g/100mL to g/L mol_sol = solubility/get_molar_mass(salt_name) #mol/L #Ksp = n^n * m^m * mol_sol^(n+m) for salt of type nXmY ions = salt2nbIons[salt_name] if len(ions) <=1: Ksp = mol_sol else: n = ions[0] m = ions[1] Ksp = (n**n)*(m**m)*(mol_sol**(n+m)) return Ksp
# Get the solubility product Q of a salt
[docs] def get_Q_solubility(salt_name: str, ions_in_solution: dict) -> float: ''' Returns the solubility product of a salt Args: salt_name (str): name of the salt ions_in_solution (dict): dictionary with keys as ions and values as the concentration in mol/L Returns: float: solubility product Q ''' Q = 1 i=0 for ion in dict_salts_trad[salt_name]: if ion in ions_in_solution: Q *= ions_in_solution[ion]**salt2nbIons[salt_name][i] i+=1 return Q
#-----------Basic ion-salt functions---------------------------- # Convert salt concentrations to ion concentrations
[docs] def salt2ions(solution: dict, volume: float = 1, unit: str = "g") -> dict: """ Convert salts to ions based on their concentrations and volume. Concentrations are in g/L by default but can also be given in mol/L by specifying 'unit' as 'mol'. A 'dict_salts_trad' dictionary is used to convert salt names to ions. Make sure the salts are in the 'dict_salts_trad' dictionary. Args: solution (dict): A dictionary containing salt names as keys and concentrations as values. volume (float, optional): Volume of the solution. Defaults to 1. unit (str, optional): Unit of concentration. Defaults to "g". Options are "g" or "mol". Returns: dict: A dictionary containing ions and their concentrations [g/L] in the solution (volume =1), their quantity [g] if volume > 1 Raises: ValueError: If the volume is non-positive. """ if volume <= 0: raise ValueError("Volume must be positive.") if unit == "mol": ions = {} for salt in solution: if salt in dict_salts_trad: for ion in dict_salts_trad[salt].keys(): if ion in ions: ions[ion] += dict_salts_trad[salt][ion] * solution[salt] else: ions[ion] = dict_salts_trad[salt][ion] * solution[salt] else: raise ValueError(f"Salt {salt} not found in the 'dict_salts_trad' dictionary.") ions_in_solution = {n: volume * ions[n] for n in ions.keys()} return ions_in_solution elif unit == "g": salts = {salt: mass/get_molar_mass(salt) for salt, mass in solution.items()} ions = {} for salt in salts: if salt in dict_salts_trad: for ion in dict_salts_trad[salt].keys(): if ion in ions: ions[ion] += dict_salts_trad[salt][ion] * solution[salt] else: ions[ion] = dict_salts_trad[salt][ion] * solution[salt] else: raise ValueError(f"Salt {salt} not found in the 'dict_salts_trad' dictionary.") ions_in_solution = {ion: float(get_molar_mass(ion)) * float(volume) * ions[ion] for ion in ions.keys()} return ions_in_solution else: raise ValueError("'Unit' must be 'g' or 'mol'.")
#Make a solution given the ion concentration wanted
[docs] def make_solution (ions_in_solution :dict, forbidden_ions:list, volume:float, )->dict: ''' Returns how much of each salt [g] to add to a solution of certain volume [L] to obtain a certain concentration of ions [g/L] Args: ions_in_solution (dict): dictionary with keys as ions and values as the concentration in g/L. forbidden_ions (list): list of ions that cannot be in the solution. volume (float): volume of the final solution in L. Returns: dict: dictionary with keys as salt names and values as the amount of salt needed in g. ''' if set(ions_in_solution.keys()).intersection(forbidden_ions) != set() : problems = set(ions_in_solution.keys()).intersection(forbidden_ions) raise ValueError(f"Forbidden ions are required in the solution: {problems}") molar_ions = {ion: ions_in_solution[ion]/get_molar_mass(ion) for ion in ions_in_solution} # Define variables possible_salts = [] for salt in dict_salts_trad.keys(): for ion in dict_salts_trad[salt]: if (ion in molar_ions) and (salt not in possible_salts) and (len(set(dict_salts_trad[salt].keys()).intersection(forbidden_ions)) == 0): possible_salts.append(salt) #Check for soluble salts for salt in possible_salts: if get_Ksp(salt) <= get_Q_solubility(salt, molar_ions): possible_salts.remove(salt) #NB variables == NB equations final_salts = [] for ion in molar_ions.keys(): for salt in possible_salts: if ion in dict_salts_trad[salt] and salt not in final_salts: final_salts.append(salt) break variables = symbols(final_salts) #Set up equations equations = [] for ion in molar_ions.keys(): lhs = 0 rhs = molar_ions[ion] * volume for salt in final_salts: if ion in dict_salts_trad[salt]: lhs += dict_salts_trad[salt][ion] * symbols(salt) equation = Eq(lhs, rhs) equations.append(equation) #Solve equations try : positive_solutions = solveset(equations, variables, domain=Interval(0, 1000000)) except: positive_solutions = set() if positive_solutions == set(): solution = solve(equations, variables) if solution == []: raise ValueError("No solution found. Please check the ions in the solution and the forbidden ions.") else: mass_salts = {salt: solution[symbols(salt)]*get_molar_mass(salt) for salt in final_salts} return mass_salts else: mass_salts = {salt: positive_solutions[symbols(salt)]*get_molar_mass(salt) for salt in final_salts} return mass_salts