diff --git a/.travis.yml b/.travis.yml index 724372fa..0b4fc47a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,7 +34,7 @@ deploy: skip-cleanup: true github-token: "$GITHUB_TOKEN" keep-history: true - local-dir: docs/build + local-dir: docs/build/html target-branch: gh-pages on: tags: true diff --git a/aguaclara/core/constants.py b/aguaclara/core/constants.py index 906b156a..8638669e 100644 --- a/aguaclara/core/constants.py +++ b/aguaclara/core/constants.py @@ -3,25 +3,36 @@ from aguaclara.core.units import unit_registry as u +# NOTE: "#: " required for Sphinx autodocumentation + +#: Gravitational constant GRAVITY = 9.80665 * u.m / u.s ** 2 +#: Density of water WATER_DENSITY = 1000 * u.kg / u.m ** 3 +#: Kinematic viscosity of water WATER_NU = 1 * 10 ** -6 * u.m ** 2 / u.s +#: Atmospheric pressure ATM_P = 1 * u.atm +#: Average kinematic viscosity of air AIR_NU = 12 * u.mm ** 2 / u.s -AIR_DENSITY = 1.204 * u.kg / u.mm ** 3 -# The influence of viscosity on mixing in jet reactors +#: The influence of viscosity on mixing in jet reactors JET_ROUND_RATIO = 0.08 -# Estimate for plane jets in the flocculator and sed tank jet reverser. +#: An estimate for plane jet ratios in the flocculator and sed tank jet reverser JET_PLANE_RATIO = 0.0124 +#: LFP_FLOW_MAX = 16.1 * u.L / u.s -FITTING_S_MIN = 5 * u.cm # Between fittings and tank wall in a tank. +#: Between fittings and tank wall in a tank. +FITTING_S_MIN = 5 * u.cm +#: CHANNEL_W_MIN = 15 * u.cm +#: VC_ORIFICE_RATIO = 0.63 +#: K_KOZENY = 5 diff --git a/aguaclara/core/data/unit_definitions.txt b/aguaclara/core/data/unit_definitions.txt index f46c440a..6b2bfd90 100644 --- a/aguaclara/core/data/unit_definitions.txt +++ b/aguaclara/core/data/unit_definitions.txt @@ -1,7 +1,7 @@ """ Created on Fri June 23 2017 -author: Sage Weber-Shirk +Author: Sage Weber-Shirk Last modified: Mon Jun 26 2017 By: Sage Weber-Shirk @@ -12,3 +12,4 @@ NTU = 1.47 * (mg / L) dollar = [money] = USD lempira = dollar * 0.0427 = HNL equivalent = mole = eq +rev = revolution diff --git a/aguaclara/core/materials.py b/aguaclara/core/materials.py index ae27ce9a..39aa0c30 100644 --- a/aguaclara/core/materials.py +++ b/aguaclara/core/materials.py @@ -5,11 +5,17 @@ """ from aguaclara.core.units import unit_registry as u +#: PVC_PIPE_ROUGH = 0.12 * u.mm +#: CONCRETE_PIPE_ROUGH = 2 * u.mm +#: CONCRETE_DENSITY = 2400 * (u.kg / u.m ** 3) +#: CONCRETE_THICKNESS_MIN = 5 * u.cm +#: REBAR_D = 0.5 * u.inch +#: ACRYLIC_T = 1 * u.cm diff --git a/aguaclara/core/physchem.py b/aguaclara/core/physchem.py index b402c231..47d174da 100644 --- a/aguaclara/core/physchem.py +++ b/aguaclara/core/physchem.py @@ -10,8 +10,7 @@ import numpy as np from scipy import interpolate, integrate -gravity = 9.80665 * u.m/u.s**2 -"""Define the gravitational constant, in m/s².""" +gravity = con.GRAVITY ###################### Simple geometry ###################### """A few equations for useful geometry. @@ -31,8 +30,9 @@ def diam_circle(AreaCircle): return np.sqrt(4 * AreaCircle / np.pi) ######################### Hydraulics ######################### -RE_TRANSITION_PIPE = 2100 +RE_TRANSITION_PIPE = 2100 +""" """ K_KOZENY = con.K_KOZENY WATER_DENSITY_TABLE = [(273.15, 278.15, 283.15, 293.15, 303.15, 313.15, @@ -78,8 +78,6 @@ def viscosity_kinematic(temp): If given units, the function will automatically convert to Kelvin. If not given units, the function will assume Kelvin. - - TODO: This should return meter ** 2 / second. """ ut.check_range([temp, ">0", "Temperature in Kelvin"]) return (viscosity_dynamic(temp).magnitude diff --git a/aguaclara/core/pipeline.py b/aguaclara/core/pipeline.py index 76047e90..46758233 100644 --- a/aguaclara/core/pipeline.py +++ b/aguaclara/core/pipeline.py @@ -10,21 +10,27 @@ import numpy @u.wraps(u.m**3/u.s, [u.m, u.m, None, u.m], False) -def flow_pipeline(diameters: numpy.ndarray, lengths: numpy.ndarray, k_minors: numpy.ndarray, target_headloss: float, +def flow_pipeline(diameters, lengths, k_minors, target_headloss, nu=con.WATER_NU, pipe_rough=mats.PVC_PIPE_ROUGH): """ This function takes a single pipeline with multiple sections, each potentially with different diameters, lengths and minor loss coefficients and determines the flow rate for a given headloss. - Args: - diameters: list of diameters, where the i_th diameter corresponds to the i_th pipe section - lengths: list of diameters, where the i_th diameter corresponds to the i_th pipe section - k_minors: list of diameters, where the i_th diameter corresponds to the i_th pipe section - target_headloss: a single headloss describing the total headloss through the system - nu: The fluid dynamic viscosity of the fluid. Defaults to water at room temperature (1 * 10**-6 * m**2/s) - pipe_rough: The pipe roughness. Defaults to PVC roughness. - Returns: - flow: the total flow through the system + :param diameters: list of diameters, where the i_th diameter corresponds to the i_th pipe section + :type diameters: numpy.ndarray + :param lengths: list of diameters, where the i_th diameter corresponds to the i_th pipe section + :type lengths: numpy.ndarray + :param k_minors: list of diameters, where the i_th diameter corresponds to the i_th pipe section + :type k_minors: numpy.ndarray + :param target_headloss: a single headloss describing the total headloss through the system + :type target_headloss: float + :param nu: The fluid dynamic viscosity of the fluid. Defaults to water at room temperature (1 * 10**-6 * m**2/s) + :type nu: float + :param pipe_rough: The pipe roughness. Defaults to PVC roughness. + :type pipe_rough: float + + :return: the total flow through the system + :rtype: float """ # Ensure all the arguments except total headloss are the same length diff --git a/aguaclara/core/units.py b/aguaclara/core/units.py index 2251a2c3..f2b5ccdf 100644 --- a/aguaclara/core/units.py +++ b/aguaclara/core/units.py @@ -1,15 +1,30 @@ -"""Module containing global `pint` unit registry. - -The `pint` module supports arithmetic involving *physical quantities* -each of which has a magnitude and units, for example 1 cm or 3 kg. -The units of a quantity come from a `pint` *unit registry*, and it -appears that `pint` supports arithmetic operations only on quantities -whose units come from the same unit registry (an attempt to perform -an operation on quantities whose units come from different unit -registries raises an exception). This module contains a single global -unit registry `unit_registry` that can be used by any number of other -modules. +"""Module containing global ``pint`` unit registry. +The ``pint`` package supports arithmetic involving **physical quantities** +each of which has a magnitude and units, for example 1 cm or 3 kg m/s^2. +The units of a quantity come from ``pint``'s **unit registry**. This module +contains a single global unit registry ``unit_registry`` that can be used by any +number of other modules. The ``aguaclara`` has also defined and added some of +its own units to the ``unit_registry``: + + * NTU = 1.47 * (mg / L) + * dollar = [money] = USD + * lempira = dollar * 0.0427 = HNL + * equivalent = mole = eq + * rev = revolution + +:Examples: + +>>> from aguaclara.core.units import unit_registry as u +>>> rpm = 10 * u.rev/u.min +>>> rpm + +>>> rpm.magnitude +10.0 +>>> rpm.units + +>>> rpm.to(u.rad/u.s) + """ import os @@ -20,6 +35,7 @@ system='mks', autoconvert_offset_to_baseunit=True ) +"""A global unit registry that can be used by any of other module.""" # default formatting includes 4 significant digits. # This can be overridden on a per-print basis with @@ -31,28 +47,27 @@ "data", "unit_definitions.txt")) -def set_sig_figs(n: int): +def set_sig_figs(n): """Set the default number of significant figures used to print pint, pandas and numpy values quantities. Defaults to 4. - Args: - n: number of significant figures to display - - Example: - import aguaclara - from aguaclara.units import unit_registry as u - h=2.5532532522352543*u.m - e = 25532532522352543*u.m - print('h before sigfig adjustment: ',h) - print('e before sigfig adjustment: ',e) - aguaclara.units.set_sig_figs(10) - print('h after sigfig adjustment: ',h) - print('e after sigfig adjustment: ',e) - - h before sigfig adjustment: 2.553 meter - e before sigfig adjustment: 2.553e+16 meter - h after sigfig adjustment: 2.553253252 meter - e after sigfig adjustment: 2.553253252e+16 meter + :param n: number of significant figures to display + :type n: int + + :Examples: + + >>> from aguaclara.core.units import set_sig_figs, unit_registry as u + >>> h = 2.5532532522352543*u.m + >>> e = 25532532522352543*u.m + >>> print('h before sigfig adjustment:',h) + h before sigfig adjustment: 2.553 meter + >>> print('e before sigfig adjustment:',e) + e before sigfig adjustment: 2.553e+16 meter + >>> set_sig_figs(10) + >>> print('h after sigfig adjustment:',h) + h after sigfig adjustment: 2.553253252 meter + >>> print('e after sigfig adjustment:',e) + e after sigfig adjustment: 2.553253252e+16 meter """ unit_registry.default_format = '.' + str(n) + 'g' pd.options.display.float_format = ('{:,.' + str(n) + '}').format diff --git a/aguaclara/core/utility.py b/aguaclara/core/utility.py index 1df77215..b1a59a4b 100644 --- a/aguaclara/core/utility.py +++ b/aguaclara/core/utility.py @@ -10,13 +10,12 @@ def round_sf(number, digits): - """Returns inputted value rounded to number - of significant figures desired. + """Returns inputted value rounded to number of significant figures desired. - Parameters: - number: Value to be rounded - digits: number of significant digits - to be rounded to. + :param number: Value to be rounded + :type number: float + :param digits: number of significant digits to be rounded to. + :type digits: int """ units = None try: diff --git a/aguaclara/play.py b/aguaclara/play.py index e7ccd77a..cbad71f7 100644 --- a/aguaclara/play.py +++ b/aguaclara/play.py @@ -34,8 +34,11 @@ # from aguaclara.design.floc import Flocculator import aguaclara.design.human_access as ha -import aguaclara.research.procoda_parser as pp import aguaclara.research.environmental_processes_analysis as epa +import aguaclara.research.floc_model as fm +import aguaclara.research.procoda_parser as procoda_parser +import aguaclara.research.peristaltic_pump as peristaltic_pump +import aguaclara.research.stock_qc as stock_qc def set_sig_figs(n=4): """Set the number of significant figures used to print Pint, Pandas, and diff --git a/aguaclara/research/data/3_stop_tubing.txt b/aguaclara/research/data/3_stop_tubing.txt new file mode 100644 index 00000000..f123a65b --- /dev/null +++ b/aguaclara/research/data/3_stop_tubing.txt @@ -0,0 +1,27 @@ +Color Diameter (mm) +orange-black 0.13 +orange-red 0.19 +orange-blue 0.25 +orange-green 0.38 +green-yellow 0.44 +orange-yellow 0.51 +white-yellow 0.57 +orange-white 0.64 +black-black 0.76 +orange-orange 0.89 +white-black 0.95 +white-white 1.02 +white-red 1.09 +red-red 1.14 +red-grey 1.22 +grey-grey 1.3 +yellow-yellow 1.42 +yellow-blue 1.52 +blue-blue 1.65 +blue-green 1.75 +green-green 1.85 +purple-purple 2.06 +purple-black 2.29 +purple-orange 2.54 +purple-white 2.79 +black-white 3.17 diff --git a/aguaclara/research/data/LS_tubing.txt b/aguaclara/research/data/LS_tubing.txt new file mode 100644 index 00000000..114e0e78 --- /dev/null +++ b/aguaclara/research/data/LS_tubing.txt @@ -0,0 +1,10 @@ +Number Flow (mL/rev) +13 .06 +14 .21 +15 1.6 +16 .8 +17 2.8 +18 3.8 +24 2.8 +35 3.8 +36 4.8 diff --git a/aguaclara/research/data/tubing_data.txt b/aguaclara/research/data/tubing_data.txt deleted file mode 100644 index 340ed5b0..00000000 --- a/aguaclara/research/data/tubing_data.txt +++ /dev/null @@ -1 +0,0 @@ -Color Diameter (mm) orange-black 0.13 orange-red 0.19 orange-blue 0.25 orange-green 0.38 green-yellow 0.44 orange-yellow 0.51 white-yellow 0.57 orange-white 0.64 black-black 0.76 orange-orange 0.89 white-black 0.95 white-white 1.02 white-red 1.09 red-red 1.14 red-grey 1.22 grey-grey 1.3 yellow-yellow 1.42 yellow-blue 1.52 blue-blue 1.65 blue-green 1.75 green-green 1.85 purple-purple 2.06 purple-black 2.29 purple-orange 2.54 purple-white 2.79 black-white 3.17 \ No newline at end of file diff --git a/aguaclara/research/environmental_processes_analysis.py b/aguaclara/research/environmental_processes_analysis.py index b88e633d..fc1f0614 100644 --- a/aguaclara/research/environmental_processes_analysis.py +++ b/aguaclara/research/environmental_processes_analysis.py @@ -18,48 +18,37 @@ def invpH(pH): - """This function calculates inverse pH + """Calculate inverse pH, i.e. hydronium ion concentration, given pH. - Parameters - ---------- - pH : float - pH to be inverted + :param pH: pH to be inverted + :type pH: float - Returns - ------- - The inverse pH (in moles per liter) of the given pH + :return: The inverse pH or hydronium ion concentration (in moles per liter) + :rtype: float - Examples - -------- - >>> invpH(8.25) - 5.623413251903491e-09 mole/liter - >>> invpH(10) - 1e-10 mole/liter + :Examples: + >>> from aguaclara.research.environmental_processes_analysis import invpH + >>> invpH(10) + """ return 10**(-pH)*u.mol/u.L def alpha0_carbonate(pH): - """This function calculates the fraction of total carbonates of the form - H2CO3 + """Calculate the fraction of total carbonates in carbonic acid form (H2CO3) - Parameters - ---------- - pH : float - pH of the system + :param pH: pH of the system + :type pH: float - Returns - ------- - fraction of CT in the form H2CO3 + :return: Fraction of carbonates in carbonic acid form (H2CO3) + :rtype: float - Examples - -------- - >>> alpha0_carbonate(8.25) - 0.01288388583402879 dimensionless - >>> alpha0_carbonate(10) - 0.00015002337123256595 dimensionless + :Examples: + >>> from aguaclara.research.environmental_processes_analysis import alpha0_carbonate + >>> round(alpha0_carbonate(10), 7) + """ alpha0_carbonate = 1/(1+(K1_carbonate/invpH(pH)) * (1+(K2_carbonate/invpH(pH)))) @@ -67,25 +56,19 @@ def alpha0_carbonate(pH): def alpha1_carbonate(pH): - """This function calculates the fraction of total carbonates of the form - HCO3- + """Calculate the fraction of total carbonates in bicarbonate form (HCO3-) - Parameters - ---------- - pH : float - pH of the system + :param pH: pH of the system + :type pH: float - Returns - ------- - fraction of CT in the form HCO3- + :return: Fraction of carbonates in bicarbonate form (HCO3-) + :rtype: float - Examples - -------- - >>> alpha1_carbonate(8.25) - 0.9773426872930407 dimensionless - >>> alpha1_carbonate(10) - 0.6399689750938067 dimensionless + :Examples: + >>> from aguaclara.research.environmental_processes_analysis import alpha1_carbonate + >>> round(alpha1_carbonate(10), 7) + """ alpha1_carbonate = 1/((invpH(pH)/K1_carbonate) + 1 + (K2_carbonate/invpH(pH))) @@ -93,121 +76,89 @@ def alpha1_carbonate(pH): def alpha2_carbonate(pH): - """This function calculates the fraction of total carbonates of the form - CO3-2 + """Calculate the fraction of total carbonates in carbonate form (CO3-2) - Parameters - ---------- - pH : float - pH of the system + :param pH: pH of the system + :type pH: float - Returns - ------- - fraction of CT in the form CO3-2 + :return: Fraction of carbonates in carbonate form (CO3-2) + :rtype: float - Examples - -------- - >>> alpha2_carbonate(8.25) - 0.009773426872930407 dimensionless - >>> alpha2_carbonate(10) - 0.35988100153496067 dimensionless + :Examples: + >>> from aguaclara.research.environmental_processes_analysis import alpha2_carbonate + >>> round(alpha2_carbonate(10), 7) + """ alpha2_carbonate = 1/(1+(invpH(pH)/K2_carbonate) * (1+(invpH(pH)/K1_carbonate))) return alpha2_carbonate -def ANC_closed(pH, Total_Carbonates): - """Acid neutralizing capacity (ANC) calculated under a closed system where - there are no carbonates exchanged with the atmosphere during the +def ANC_closed(pH, total_carbonates): + """Calculate the acid neutralizing capacity (ANC) under a closed system + in which no carbonates are exchanged with the atmosphere during the experiment. Based on pH and total carbonates in the system. - Parameters - ---------- - pH : float - pH of the system - - Total_Carbonates - total carbonates in the system (mole/L) + :param pH: pH of the system + :type pH: float + :param total_carbonates: Total carbonate concentration in the system (mole/L) + :type total_carbonates: float - Returns - ------- - The acid neutralizing capacity of the closed system (eq/L) + :return: The acid neutralizing capacity of the closed system (eq/L) + :rtype: float - Examples - -------- - >>> ANC_closed(8.25, 1*u.mol/u.L) - 0.9968913136948984 equivalents/liter - >>> ANC_closed(10, 1*u.mol/u.L) - 1.359830978063728 equivalents/liter + :Examples: + >>> from aguaclara.research.environmental_processes_analysis import ANC_closed + >>> from aguaclara.core.units import unit_registry as u + >>> round(ANC_closed(10, 1*u.mol/u.L), 7) + """ - return (Total_Carbonates * (u.eq/u.mol * alpha1_carbonate(pH) + + return (total_carbonates * (u.eq/u.mol * alpha1_carbonate(pH) + 2 * u.eq/u.mol * alpha2_carbonate(pH)) + 1 * u.eq/u.mol * Kw/invpH(pH) - 1 * u.eq/u.mol * invpH(pH)) def ANC_open(pH): - """Acid neutralizing capacity (ANC) calculated under an open system, based - on pH. + """Calculate the acid neutralizing capacity (ANC) calculated under an open + system based on pH. - Parameters - ---------- - pH : float - pH of the system + :param pH: pH of the system + :type pH: float - Returns - ------- - The acid neutralizing capacity of the closed system (eq/L) + :return: The acid neutralizing capacity of the closed system (eq/L) + :rtype: float - Examples - -------- - >>> ANC_open(8.25) - 0.0007755217825265541 equivalents/liter - >>> ANC_open(10) - 0.09073461016054905 equivalents/liter + :Examples: + >>> from aguaclara.research.environmental_processes_analysis import ANC_open + >>> round(ANC_open(10), 7) + """ return ANC_closed(pH, P_CO2*K_Henry_CO2/alpha0_carbonate(pH)) def aeration_data(DO_column, dirpath): - """This function extracts the data from folder containing tab delimited + """Extract the data from folder containing tab delimited files of aeration data. The file must be the original tab delimited file. All text strings below the header must be removed from these files. The file names must be the air flow rates with units of micromoles/s. - An example file name would be "300.xls" where 300 is the flowr ate in + An example file name would be "300.xls" where 300 is the flow rate in micromoles/s. The function opens a file dialog for the user to select the directory containing the data. - Parameters - ---------- - DO_column : int - index of the column that contains the dissolved oxygen concentration - data. - - dirpath : string - path to the directory containing aeration data you want to analyze - - Returns - ------- - filepaths : string list - all file paths in the directory sorted by flow rate - - airflows : numpy array - sorted array of air flow rates with units of micromole/s attached + :param DO_column: Index of the column that contains the dissolved oxygen concentration data. + :type DO_columm: int + :param dirpath: Path to the directory containing aeration data you want to analyze + :type dirpath: string - DO_data : numpy array list - sorted list of numpy arrays. Thus each of the numpy data arrays can - have different lengths to accommodate short and long experiments - - time_data : numpy array list - sorted list of numpy arrays containing the times with units of seconds - - Examples - -------- + :return: collection of + * **filepaths** (*string list*) - All file paths in the directory sorted by flow rate + * **airflows** (*numpy.array*) - Sorted array of air flow rates with units of micromole/s + * **DO_data** (*numpy.array list*) - Sorted list of Numpy arrays. Thus each of the numpy data arrays can have different lengths to accommodate short and long experiments + * **time_data** (*numpy.array list*) - Sorted list of Numpy arrays containing the times with units of seconds """ #return the list of files in the directory filenames = os.listdir(dirpath) @@ -229,25 +180,22 @@ def aeration_data(DO_column, dirpath): def O2_sat(P_air, temp): - """This equation returns saturaed oxygen concentration in mg/L. It is valid - for 278 K < T < 318 K + """Calculate saturaed oxygen concentration in mg/L for 278 K < T < 318 K - Parameters - ---------- - Pressure_air : float - air pressure with appropriate units. - Temperature : - water temperature with appropriate units + :param P_air: Air pressure with appropriate units + :type P_air: float + :param temp: Water temperature with appropriate units + :type temp: float - Returns - ------- - Saturated oxygen concentration in mg/L + :return: Saturated oxygen concentration in mg/L + :rtype: float - Examples - -------- - >>> O2_sat(1*u.atm , 300*u.kelvin) - 8.093157231428425 milligram/liter + :Examples: + >>> from aguaclara.research.environmental_processes_analysis import O2_sat + >>> from aguaclara.core.units import unit_registry as u + >>> round(O2_sat(1*u.atm , 300*u.kelvin), 7) + """ fraction_O2 = 0.21 P_O2 = P_air * fraction_O2 @@ -256,38 +204,18 @@ def O2_sat(P_air, temp): def Gran(data_file_path): - """This function extracts the data from a ProCoDA Gran plot file. - The file must be the original tab delimited file. - - Parameters - ---------- - data_file_path : string - File path. If the file is in the working directory, then the file name - is sufficient. - - Returns - ------- - V_titrant : float - volume of titrant in mL - - ph_data : numpy array - pH of the sample + """Extract the data from a ProCoDA Gran plot file. The file must be the original tab delimited file. - V_sample : float - volume of the original sample that was titrated in mL + :param data_file_path: The path to the file. If the file is in the working directory, then the file name is sufficient. - Normality_titrant : float - normality of the acid used to titrate the sample in mole/L - - V_equivalent : float - volume of acid required to consume all of the ANC in mL - - ANC : float - Acid Neutralizing Capacity of the sample in mole/L - - Examples - -------- + :return: collection of + * **V_titrant** (*float*) - Volume of titrant in mL + * **ph_data** (*numpy.array*) - pH of the sample + * **V_sample** (*float*) - Volume of the original sample that was titrated in mL + * **Normality_titrant** (*float*) - Normality of the acid used to titrate the sample in mole/L + * **V_equivalent** (*float*) - Volume of acid required to consume all of the ANC in mL + * **ANC** (*float*) - Acid Neutralizing Capacity of the sample in mole/L """ df = pd.read_csv(data_file_path, delimiter='\t', header=5) V_t = np.array(pd.to_numeric(df.iloc[0:, 0]))*u.mL @@ -307,93 +235,74 @@ def Gran(data_file_path): # Reactors # The following code is for reactor responses to tracer inputs. def CMFR(t, C_initial, C_influent): - """This function calculates the output concentration of a completely mixed - flow reactor given an influent and initial concentration. + """Calculate the effluent concentration of a conversative (non-reacting) + material with continuous input to a completely mixed flow reactor. + + Note: time t=0 is the time at which the material starts to flow into the + reactor. + + :param C_initial: The concentration in the CMFR at time t=0. + :type C_initial: float + :param C_influent: The concentration entering the CMFR. + :type C_influent: float + :param t: The time(s) at which to calculate the effluent concentration. Time can be made dimensionless by dividing by the residence time of the CMFR. + :type t: float or numpy.array + + :return: Effluent concentration + :rtype: float + + :Examples: + + >>> from aguaclara.research.environmental_processes_analysis import CMFR + >>> from aguaclara.core.units import unit_registry as u + >>> round(CMFR(0.1, 0*u.mg/u.L, 10*u.mg/u.L), 7) + + >>> round(CMFR(0.9, 5*u.mg/u.L, 10*u.mg/u.L), 7) + + """ + return C_influent * (1-np.exp(-t)) + C_initial*np.exp(-t) - Parameters - ---------- - C_initial : float - The concentration in the CMFR at time zero. - C_influent : float - The concentration entering the CMFR. +def E_CMFR_N(t, N): + """Calculate a dimensionless measure of the output tracer concentration + from a spike input to a series of completely mixed flow reactors. - t : float (array) - time made dimensionless by dividing by the residence time of the CMFR. - It can be a single value or a numpy array. + :param t: The time(s) at which to calculate the effluent concentration. Time can be made dimensionless by dividing by the residence time of the CMFR. + :type t: float or numpy.array + :param N: The number of completely mixed flow reactors (CMFRS) in series. Must be greater than 1. + :type N: int - Returns - ------- - float - Effluent concentration + :return: Dimensionless measure of the output tracer concentration (concentration * volume of 1 CMFR) / (mass of tracer) + :rtype: float - Examples - -------- - >>> CMFR(0.1, 0*u.mg/u.L, 10*u.mg/u.L) - 0.9516258196404048 milligram/liter - >>> CMFR(0.9, 5*u.mg/u.L, 10*u.mg/u.L) - 7.967151701297004 milligram/liter + :Examples: + >>> from aguaclara.research.environmental_processes_analysis import E_CMFR_N + >>> round(E_CMFR_N(0.5, 3), 7) + 0.7530643 + >>> round(E_CMFR_N(0.1, 1), 7) + 0.9048374 """ - return C_influent * (1-np.exp(-t)) + C_initial*np.exp(-t) + return (N**N)/special.gamma(N) * (t**(N-1))*np.exp(-N*t) -def E_CMFR_N(t, N): - """This function calculates a dimensionless measure of the output tracer - concentration from a spike input to a series of completely mixed flow - reactors. - - Parameters - ---------- - t : float (array) - time made dimensionless by dividing by the reactor residence time. - t can be a single value or a numpy array. - - N : float - number of completely mixed flow reactors (CMFRS) in series. - This would logically be constrained to real numbers greater than 1. - - Returns - ------- - float - dimensionless measure of the output tracer concentration - (Concentration * volume of 1 CMFR) / (mass of tracer) - - Examples - -------- - >>> E_CMFR_N(0.5, 3) - 0.7530642905009506 - >>> E_CMFR_N(0.1, 1) - 0.9048374180359595 +def E_Advective_Dispersion(t, Pe): + """Calculate a dimensionless measure of the output tracer concentration from + a spike input to reactor with advection and dispersion. - """ - return (N**N)/special.gamma(N) * (t**(N-1))*np.exp(-N*t) + :param t: The time(s) at which to calculate the effluent concentration. Time can be made dimensionless by dividing by the residence time of the CMFR. + :type t: float or numpy.array + :param Pe: The ratio of advection to dispersion ((mean fluid velocity)/(Dispersion*flow path length)) + :type Pe: float + :return: dimensionless measure of the output tracer concentration (concentration * volume of reactor) / (mass of tracer) + :rtype: float -def E_Advective_Dispersion(t, Pe): - """This function calculates a dimensionless measure of the output tracer - concentration from a spike input to reactor with advection and dispersion. - Parameters - ---------- - t : float (array) - time made dimensionless by dividing by the reactor residence time. - t can be a single value or a numpy array. - - Pe : float - The ratio of advection to dispersion - ((mean fluid velocity)/(Dispersion*flow path length)) - - Returns - ------- - float - dimensionless measure of the output tracer concentration - (Concentration * volume of reactor) / (mass of tracer) - - Examples - -------- - >>> E_Advective_Dispersion(0.5, 5) - 0.47748641153355664 + :Examples: + >>> from aguaclara.research.environmental_processes_analysis import E_Advective_Dispersion + >>> round(E_Advective_Dispersion(0.5, 5), 7) + 0.4774864 """ # replace any times at zero with a number VERY close to zero to avoid # divide by zero errors @@ -407,67 +316,46 @@ def Tracer_CMFR_N(t_seconds, t_bar, C_bar, N): The model function, f(x, ...). It takes the independent variable as the first argument and the parameters to fit as separate remaining arguments. - Parameters - ---------- - t_seconds : float list - Array of times + :param t_seconds: List of times + :type t_seconds: float list + :param t_bar: Average time spent in the reactor + :type t_bar: float + :param C_bar: Average concentration (mass of tracer)/(volume of the reactor) + :type C_bar: float + :param N: Number of completely mixed flow reactors (CMFRs) in series, must be greater than 1 + :type N: int - t_bar : float - Average time spent in the reactor + :return: The model concentration as a function of time + :rtype: float list - C_bar : float - Average concentration. - (Mass of tracer)/(volume of the reactor) + :Examples: - N : float - number of completely mixed flow reactors (CMFRS) in series. - This would logically be constrained to real numbers greater than 1. - - Returns - ------- - float list - The model concentration as a function of time - - Examples - -------- + >>> from aguaclara.research.environmental_processes_analysis import Tracer_CMFR_N + >>> from aguaclara.core.units import unit_registry as u >>> Tracer_CMFR_N([1, 2, 3, 4, 5]*u.s, 5*u.s, 10*u.mg/u.L, 3) - \\[\\begin{pmatrix}2.963582834907743 & 6.505794977303565 & 8.033525967569107 & 7.838031164205239 & 6.721254229661633\\end{pmatrix} milligram/liter\\] - + """ return C_bar*E_CMFR_N(t_seconds/t_bar, N) def Solver_CMFR_N(t_data, C_data, theta_guess, C_bar_guess): """Use non-linear least squares to fit the function - Tracer_CMFR_N(t_seconds, t_bar, C_bar, N), to reactor data. - - Parameters - ---------- - t_data : float list - Array of times with units - - C_data : float list - Array of tracer concentration data with units - - theta_guess : float - Estimate of time spent in one CMFR with units. - - C_bar_guess : float - Estimate of average concentration with units - (Mass of tracer)/(volume of one CMFR) - - Returns - ------- - tuple - theta : float - residence time in seconds - - C_bar : float - average concentration with same units as C_bar_guess - - N : float - number of CMFRS in series that best fit the data - + Tracer_CMFR_N(t_seconds, t_bar, C_bar, N) to reactor data. + + :param t_data: Array of times with units + :type t_data: float list + :param C_data: Array of tracer concentration data with units + :type C_data: float list + :param theta_guess: Estimate of time spent in one CMFR with units. + :type theta_guess: float + :param C_bar_guess: Estimate of average concentration with units ((mass of tracer)/(volume of one CMFR)) + :type C_bar_guess: float + + :return: tuple of + + * **theta** (*float*)- Residence time in seconds + * **C_bar** (*float*) - Average concentration with same units as C_bar_guess + * **N** (*float*)- Number of CMFRS in series that best fit the data """ C_unitless = C_data.magnitude C_units = str(C_bar_guess.units) @@ -488,30 +376,24 @@ def Tracer_AD_Pe(t_seconds, t_bar, C_bar, Pe): model function, f(x, ...). It takes the independent variable as the first argument and the parameters to fit as separate remaining arguments. - Parameters - ---------- - t_seconds : float list - Array of times - - t_bar : float - Average time spent in the reactor - - C_bar : float - Average concentration. - (Mass of tracer)/(volume of the reactor) + :param t_seconds: List of times + :type t_seconds: float list + :param t_bar: Average time spent in the reactor + :type t_bar: float + :param C_bar: Average concentration ((mass of tracer)/(volume of the reactor)) + :type C_bar: float + :param Pe: The Peclet number for the reactor. + :type Pe: float - Pe : float - The Peclet number for the reactor. + :return: The model concentration as a function of time + :rtype: float list - Returns - ------- - float - The model concentration as a function of time + :Examples: - Examples - -------- + >>> from aguaclara.research.environmental_processes_analysis import Tracer_AD_Pe + >>> from aguaclara.core.units import unit_registry as u >>> Tracer_AD_Pe([1, 2, 3, 4, 5]*u.s, 5*u.s, 10*u.mg/u.L, 5) - \\[\\begin{pmatrix}0.2583373169261504 & 3.237939891647294 & 5.834983303390744 & 6.625088308600714 & 6.307831305050401\\end{pmatrix} milligram/liter\\] + """ return C_bar*E_Advective_Dispersion(t_seconds/t_bar, Pe) @@ -521,33 +403,20 @@ def Solver_AD_Pe(t_data, C_data, theta_guess, C_bar_guess): """Use non-linear least squares to fit the function Tracer_AD_Pe(t_seconds, t_bar, C_bar, Pe) to reactor data. - Parameters - ---------- - t_data : float list - Array of times with units - - C_data : float list - Array of tracer concentration data with units - - theta_guess : float - Estimate of time spent in one CMFR with units. - - C_bar_guess : float - Estimate of average concentration with units - (Mass of tracer)/(volume of one CMFR) - - Returns - ------- - tuple - theta : float - residence time in seconds - - C_bar : float - average concentration with same units as C_bar_guess + :param t_data: Array of times with units + :type t_data: float list + :param C_data: Array of tracer concentration data with units + :type C_data: float list + :param theta_guess: Estimate of time spent in one CMFR with units. + :type theta_guess: float + :param C_bar_guess: Estimate of average concentration with units ((mass of tracer)/(volume of one CMFR)) + :type C_bar_guess: float - Pe : float - peclet number that best fits the data + :return: tuple of + * **theta** (*float*)- Residence time in seconds + * **C_bar** (*float*) - Average concentration with same units as C_bar_guess + * **Pe** (*float*) - Peclet number that best fits the data """ #remove time=0 data to eliminate divide by zero error t_data = t_data[1:-1] diff --git a/aguaclara/research/floc_model.py b/aguaclara/research/floc_model.py index 75492d21..6f6d5f9e 100644 --- a/aguaclara/research/floc_model.py +++ b/aguaclara/research/floc_model.py @@ -15,7 +15,22 @@ class Material: + """A particulate material with a name, diameter, density, and + molecular weight. + """ + def __init__(self, name, diameter, density, molecWeight): + """Initialize a material object. + + :param name: Name of the material + :type name: string + :param diameter: Diameter of the material in particulate form + :type diameter: float + :param density: Density of the material (mass/volume) + :type density: float + :param molecWeight: Molecular weight of the material (mass/mole) + :type moleWeight: float + """ self.name = name self.Diameter = diameter self.Density = density @@ -23,6 +38,10 @@ def __init__(self, name, diameter, density, molecWeight): class Chemical(Material): + """A chemical with a name, diameter, density, molecular weight, number of + aluminum atoms per molecule, and a precipitate. + """ + def __init__(self, name, diameter, density, molecWeight, Precipitate, AluminumMPM=None): Material.__init__(self, name, diameter, density, molecWeight) @@ -46,17 +65,15 @@ def define_Precip(self, diameter, density, molecweight, alumMPM): ################## Material Definitions ################## # name, diameter in m, density in kg/m³, molecular weight in kg/mole -Clay = Material('Clay', 7 * 10**-6, 2650, None) - -PACl = Chemical('PACl', (90 * u.nm).to(u.m).magnitude, 1138, 1.039, - 'PACl', AluminumMPM=13) - -Alum = Chemical('Alum', (70 * u.nm).to(u.m).magnitude, 2420, 0.59921, - 'AlOH3', AluminumMPM=2) - -Alum.define_Precip((70 * u.nm).to(u.m).magnitude, 2420, 0.078, 1) - -HumicAcid = Chemical('Humic Acid', 72 * 10**-9, 1780, None, 'Humic Acid') +Clay = Material('Clay', 7 * 10**-6 * u.m, 2650 * u.kg/u.m**3, None) +PACl = Chemical('PACl', 9 * 10 **-8 * u.m, 1138 * u.kg/u.m**3, + 1.039 * u.kg/u.mol, 'PACl', AluminumMPM=13) +Alum = Chemical('Alum', 7 * 10 **-8 * u.m, 2420 * u.kg/u.m**3, + 0.59921 * u.kg/u.mol, 'AlOH3', AluminumMPM=2) +Alum.define_Precip(7 * 10 **-8 * u.m, 2420 * u.kg/u.m**3, + 0.078 * u.kg/u.mol, 1) +HumicAcid = Chemical('Humic Acid', 72 * 10**-9 * u.m, 1780 * u.kg/u.m**3, None, + 'Humic Acid') ################### Necessary Constants ################### diff --git a/aguaclara/research/peristaltic_pump.py b/aguaclara/research/peristaltic_pump.py new file mode 100644 index 00000000..03f0feeb --- /dev/null +++ b/aguaclara/research/peristaltic_pump.py @@ -0,0 +1,123 @@ +from aguaclara.core.units import unit_registry as u +import numpy as np +import pandas as pd +import os + +# pump rotor radius based on minimizing error between predicted and measured +# values +R_pump = 1.62 * u.cm +# empirically derived correction factor due to the fact that larger diameter +# tubing has more loss due to space smashed by rollers +k_nonlinear = 13 + + +def vol_per_rev_3_stop(color="", inner_diameter=0): + """Return the volume per revolution of an Ismatec 6 roller pump + given the inner diameter (ID) of 3-stop tubing. The calculation is + interpolated from the table found at + http://www.ismatec.com/int_e/pumps/t_mini_s_ms_ca/tubing_msca2.htm. + + Note: + 1. Either input a string as the tubing color code or a number as the + tubing inner diameter. If both are given, the function will default to using + the color. + 2. The calculation is interpolated for inner diameters between 0.13 and 3.17 + mm. Accuracy is not guaranteed for tubes with smaller or larger diameters. + + :param color: Color code of the Ismatec 3-stop tubing + :type color: string + :param inner_diameter: Inner diameter of the Ismatec 3-stop tubing. Results will be most accurate for inner diameters between 0.13 and 3.17 mm. + :type inner_diameter: float + + :return: Volume per revolution output by a 6-roller pump through the 3-stop tubing (mL/rev) + :rtype: float + + :Examples: + + >>> from aguaclara.research.peristaltic_pump import vol_per_rev_3_stop + >>> from aguaclara.core.units import unit_registry as u + >>> round(vol_per_rev_3_stop(color="yellow-blue"), 6) + + >>> round(vol_per_rev_3_stop(inner_diameter=.20*u.mm), 6) + + """ + if color != "": + inner_diameter = ID_colored_tube(color) + term1 = (R_pump * 2 * np.pi - k_nonlinear * inner_diameter) / u.rev + term2 = np.pi * (inner_diameter ** 2) / 4 + return (term1 * term2).to(u.mL/u.rev) + + +def ID_colored_tube(color): + """Look up the inner diameter of Ismatec 3-stop tubing given its color code. + + :param color: Color of the 3-stop tubing + :type color: string + + :returns: Inner diameter of the 3-stop tubing (mm) + :rtype: float + + :Examples: + + >>> from aguaclara.research.peristaltic_pump import ID_colored_tube + >>> from aguaclara.core.units import unit_registry as u + >>> ID_colored_tube("yellow-blue") + + >>> ID_colored_tube("orange-yellow") + + >>> ID_colored_tube("purple-white") + + """ + tubing_data_path = os.path.join(os.path.dirname(__file__), "data", + "3_stop_tubing.txt") + df = pd.read_csv(tubing_data_path, delimiter='\t') + idx = df["Color"] == color + return df[idx]['Diameter (mm)'].values[0] * u.mm + + +def vol_per_rev_LS(id_number): + """Look up the volume per revolution output by a Masterflex L/S pump + through L/S tubing of the given ID number. + + :param id_number: Identification number of the L/S tubing. Valid numbers are 13-18, 24, 35, and 36. + :type id_number: int + + :return: Volume per revolution output by a Masterflex L/S pump through the L/S tubing + :rtype: float + + :Examples: + + >>> from aguaclara.research.peristaltic_pump import vol_per_rev_LS + >>> from aguaclara.core.units import unit_registry as u + >>> vol_per_rev_LS(13) + + >>> vol_per_rev_LS(18) + + """ + tubing_data_path = os.path.join(os.path.dirname(__file__), "data", + "LS_tubing.txt") + df = pd.read_csv(tubing_data_path, delimiter='\t') + idx = df["Number"] == id_number + return df[idx]['Flow (mL/rev)'].values[0] * u.mL/u.turn + + +def flow_rate(vol_per_rev, rpm): + """Return the flow rate from a pump given the volume of fluid pumped per + revolution and the desired pump speed. + + :param vol_per_rev: Volume of fluid output per revolution (dependent on pump and tubing) + :type vol_per_rev: float + :param rpm: Desired pump speed in revolutions per minute + :type rpm: float + + :return: Flow rate of the pump (mL/s) + :rtype: float + + :Examples: + + >>> from aguaclara.research.peristaltic_pump import flow_rate + >>> from aguaclara.core.units import unit_registry as u + >>> flow_rate(3*u.mL/u.rev, 5*u.rev/u.min) + + """ + return (vol_per_rev * rpm).to(u.mL/u.s) diff --git a/aguaclara/research/procoda_parser.py b/aguaclara/research/procoda_parser.py index d2c41abd..17509464 100644 --- a/aguaclara/research/procoda_parser.py +++ b/aguaclara/research/procoda_parser.py @@ -6,99 +6,58 @@ from pathlib import Path -def get_data_by_time(path, columns, start_date, start_time="00:00", end_date=None, end_time="23:59"): - """Extracts columns of data from a ProCoDA datalog based on starting and ending date(s) and times - - Note: currently only works for 1 or 2 days of data, i.e. end_date must be unspecified or one day after start_date - - Parameters - ---------- - path : string - The path to the folder containing your ProCoDA data files - columns : int (list) - A single index of a column or a list of indices of columns of data to extract - Note: Column 0 is time. The first data column is column 1. - start_date : string - Starting date of data to extract, formatted 'M-D-YYYY' - start_time: string, optional - Starting time of data to extract, formatted 'HH:MM' (24-hour time) - end_date : string, optional - Ending date of data to extract, formatted 'M-D-YYYY' - end_time: string, optional - Ending time of data to extract, formatted 'HH:MM' (24-hour time) - - Return - ------ - list (2D list) - list : - contains the single column of data to extract - 2D list: - a list of lists containing the columns to extract, in order of the indices given in the columns variable - - Examples - -------- - get_data_by_time(path='/Users/.../ProCoDA Data/', columns=4, start_date='6-14-2018', start_time='12:20', - end_date='6-15-2018', end_time='10:50') - get_data_by_time(path='/Users/.../ProCoDA Data/', columns=[0,4], start_date='6-14-2018', start_time='12:20', - end_time='23:59') - get_data_by_time(path='/Users/.../ProCoDA Data/', columns=[0,3,4], start_date='6-14-2018', end_date='6-18-2018') - """ +def get_data_by_time(path, columns, dates, start_time='00:00', end_time='23:59'): + """Extract columns of data from a ProCoDA datalog based on date(s) and time(s) - # Locate and read data file(s) - if path[-1] != '/': - path += '/' - paths = [path + "datalog " + start_date + '.xls'] - data = [remove_notes(pd.read_csv(paths[0], delimiter='\t'))] + Note: Column 0 is time. The first data column is column 1. - if end_date is not None: - paths.append(path + "datalog " + end_date + ".xls") - data.append(remove_notes(pd.read_csv(paths[1], delimiter='\t'))) + :param path: The path to the folder containing the ProCoDA data file(s) + :type path: string + :param columns: A single index of a column OR a list of indices of columns of data to extract. + :type columns: int or int list + :param dates: A single date or list of dates for which data was recorded, formatted "M-D-YYYY" + :type dates: string or string list + :param start_time: Starting time of data to extract, formatted 'HH:MM' (24-hour time) + :type start_time: string, optional + :param end_time: Ending time of data to extract, formatted 'HH:MM' (24-hour time) + :type end_time: string, optional - # Calculate start index - time_column = pd.to_numeric(data[0].iloc[:, 0]) - interval = time_column[1]-time_column[0] - start_idx = int(round((day_fraction(start_time) - time_column[0])/interval + .5)) #round up + :return: a list containing the single column of data to extract, OR a list of lists containing the columns to extract, in order of the indices given in the columns variable + :rtype: list or list list - # Calculate end index - time_column = pd.to_numeric(data[-1].iloc[:, 0]) - end_idx = int(round((day_fraction(end_time) - time_column[0])/interval + .5)) + 1 #round up + :Examples: - # Get columns of interest - if len(paths) == 1: - if isinstance(columns, int): - result = list(pd.to_numeric(data[0].iloc[start_idx:end_idx, columns])) - else: - result = [] - for c in columns: - result.append(list(pd.to_numeric(data[0].iloc[start_idx:end_idx, c]))) - else: - data[1].iloc[0, 0] = 0 - if isinstance(columns, int): - result = list(pd.to_numeric(data[0].iloc[start_idx:, columns])) + \ - list(pd.to_numeric(data[1].iloc[:end_idx, columns]) + (1 if columns == 0 else 0)) - else: - result = [] - for c in columns: - result.append(list(pd.to_numeric(data[0].iloc[start_idx:, c])) + - list(pd.to_numeric(data[1].iloc[:end_idx, c])+(1 if c == 0 else 0))) + .. code-block:: python + + data = get_data_by_time(path='/Users/.../ProCoDA Data/', columns=4, dates=['6-14-2018', '6-15-2018'], start_time='12:20', end_time='10:50') + data = get_data_by_time(path='/Users/.../ProCoDA Data/', columns=[0,4], dates='6-14-2018', start_time='12:20', end_time='23:59') + data = get_data_by_time(path='/Users/.../ProCoDA Data/', columns=[0,3,4], dates='6-14-2018') + """ + data = data_from_dates(path, dates) + first_time_column = pd.to_numeric(data[0].iloc[:, 0]) + start = max(day_fraction(start_time), first_time_column[0]) + start_idx = time_column_index(start, first_time_column) + end_idx = time_column_index(day_fraction(end_time), + pd.to_numeric(data[-1].iloc[:, 0])) + 1 + + if isinstance(columns, int): + return column_start_to_end(data, columns, start_idx, end_idx) + else: + result = [] + for c in columns: + result.append(column_start_to_end(data, c, start_idx, end_idx)) return result def remove_notes(data): - """Omits any rows containing text from a pandas.DataFrame object, except for headers - - Text is defined as characters of the alphabet. The resulting DataFrame should have only headers and numerical data. + """Omit notes from a DataFrame object, where notes are identified as rows with non-numerical entries in the first column. - Parameters - ---------- - data : pandas.DataFrame - DataFrame object to remove text from + :param data: DataFrame object to remove notes from + :type data: Pandas.DataFrame - Returns - ------- - pandas.DataFrame - DataFrame object with no text, except for headers + :return: DataFrame object with no notes + :rtype: Pandas.DataFrame """ has_text = data.iloc[:, 0].astype(str).str.contains('(?!e-)[a-zA-Z]') text_rows = list(has_text.index[has_text]) @@ -106,68 +65,124 @@ def remove_notes(data): def day_fraction(time): - """Converts a 24-hour time to a fraction of a day. + """Convert a 24-hour time to a fraction of a day. For example, midnight corresponds to 0.0, and noon to 0.5. - Parameters - ---------- - time : string - Time in the form of 'HH:MM' (24-hour time) - - Returns - ------- - float - A day fraction - - Examples - -------- - >>> from aguaclara.research.procoda_parser import day_fraction - >>> day_fraction("00:21") - 0.014583333333333334 - >>> day_fraction("18:30") - 0.7708333333333334 + :param time: Time in the form of 'HH:MM' (24-hour time) + :type time: string + + :return: A day fraction + :rtype: float + + :Examples: + + .. code-block:: python + + day_fraction("18:30") """ hour = int(time.split(":")[0]) minute = int(time.split(":")[1]) return hour/24 + minute/1440 +def time_column_index(time, time_column): + """Return the index of lowest time in the column of times that is greater + than or equal to the given time. + + :param time: the time to index from the column of time; a day fraction + :type time: float + :param time_column: a list of times (in day fractions), must be increasing and equally spaced + :type time_column: float list + + :return: approximate index of the time from the column of times + :rtype: int + """ + interval = time_column[1]-time_column[0] + return int(round((time - time_column[0])/interval + .5)) + + +def data_from_dates(path, dates): + """Return list DataFrames representing the ProCoDA datalogs stored in + the given path and recorded on the given dates. + + :param path: The path to the folder containing the ProCoDA data file(s) + :type path: string + :param dates: A single date or list of dates for which data was recorded, formatted "M-D-YYYY" + :type dates: string or string list + + :return: a list DataFrame objects representing the ProCoDA datalogs corresponding with the given dates + :rtype: pandas.DataFrame list + """ + if path[-1] != os.path.sep: + path += os.path.sep + + if not isinstance(dates, list): + dates = [dates] + + data = [] + for d in dates: + filepath = path + 'datalog ' + d + '.xls' + data.append(remove_notes(pd.read_csv(filepath, delimiter='\t'))) + + return data + + +def column_start_to_end(data, column, start_idx, end_idx): + """Return a list of numeric data entries in the given column from the starting + index to the ending index. This can list can be compiled over one or more + DataFrames. + + :param data: a list of DataFrames to extract data in one column from + :type data: Pandas.DataFrame list + :param column: a column index + :type column: int + :param start_idx: the index of the starting row + :type start_idx: int + :param start_idx: the index of the ending row + :type start_idx: int + + :return: a list of data from the given column + :rtype: float list + """ + if len(data) == 1: + result = list(pd.to_numeric(data[0].iloc[start_idx:end_idx, column])) + else: + result = list(pd.to_numeric(data[0].iloc[start_idx:, column])) + for i in range(1, len(data)-1): + data[i].iloc[0, 0] = 0 + result += list(pd.to_numeric(data[i].iloc[:, column]) + + (i if column == 0 else 0)) + data[-1].iloc[0, 0] = 0 + result += list(pd.to_numeric(data[-1].iloc[:end_idx, column]) + + (len(data)-1 if column == 0 else 0)) + + return result + + def get_data_by_state(path, dates, state, column): - """Reads a ProCoDA file and extracts the time and data column for each iteration of - the given state. - - Parameters - ---------- - dates : string (list) - A list of dates or single date for which data was recorded, in - the form "M-D-YYYY" - state : int - The state ID number for which data should be plotted - column : int or string - int: - Index of the column that you want to extract. Column 0 is time. - The first data column is column 1. - string: - Name of the column header that you want to extract - path : string, optional - Optional argument of the path to the folder containing your ProCoDA - files. Defaults to the current directory if no argument is passed in - extension : string, optional - The file extension of the tab delimited file. Defaults to ".xls" if - no argument is passed in - - Returns - ------- - 3-D list - A list of lists of the time and data columns extracted for each iteration of the state. - For example, if "data" is the output, data[i][:,0] gives the time column and data[i][:,1] - gives the data column for the ith iteration of the given state and column. data[i][0] - would give the first [time, data] pair. - - Examples - -------- - get_data_by_state(["6-19-2013", "6-20-2013"], 1, 28) + """Reads a ProCoDA file and extracts the time and data column for each + iteration ofthe given state. + + Note: column 0 is time, the first data column is column 1. + + :param path: The path to the folder containing the ProCoDA data file(s), defaults to the current directory + :type path: string + :param dates: A single date or list of dates for which data was recorded, formatted "M-D-YYYY" + :type dates: string or string list + :param state: The state ID number for which data should be plotted + :type state: int + :param column: The integer index of the column that you want to extract OR the header of the column that you want to extract + :type column: int or string + + :return: A list of lists of the time and data columns extracted for each iteration of the state. For example, if "data" is the output, data[i][:,0] gives the time column and data[i][:,1] gives the data column for the ith iteration of the given state and column. data[i][0] would give the first [time, data] pair. + :type: list of lists of lists + + :Examples: + + .. code-block:: python + + data = get_data_by_state(path='/Users/.../ProCoDA Data/', dates=["6-19-2013", "6-20-2013"], state=1, column=28) """ data_agg = [] day = 0 @@ -241,87 +256,63 @@ def get_data_by_state(path, dates, state, column): return data_agg -def column_of_time(data_file_path, start, end=-1): +def column_of_time(path, start, end=-1): """This function extracts the column of times from a ProCoDA data file. - Parameters - ---------- - data_file_path : string - File path. If the file is in the working directory, then the file name - is sufficient. + :param path: The file path of the ProCoDA data file. If the file is in the working directory, then the file name is sufficient. + :type path: string + :param start: Index of first row of data to extract from the data file + :type start: int + :param end: Index of last row of data to extract from the data. Defaults to last row + :type end: int - start : int or float - Index of first row of data to extract from the data file + :return: Experimental times starting at 0 day with units of days. + :rtype: numpy.array - end : int or float, optional - Index of last row of data to extract from the data - Defaults to -1, which extracts all the data in the file + :Examples: - Returns - ------- - numpy array - Experimental times starting at 0 day with units of days. - - Examples - -------- - ftime(Reactor_data.txt, 0) + .. code-block:: python + time = column_of_time("Reactor_data.txt", 0) """ - if not isinstance(start, int): - start = int(start) - if not isinstance(end, int): - end = int(end) - - df = pd.read_csv(data_file_path, delimiter='\t') + df = pd.read_csv(path, delimiter='\t') start_time = pd.to_numeric(df.iloc[start, 0])*u.day day_times = pd.to_numeric(df.iloc[start:end, 0]) time_data = np.subtract((np.array(day_times)*u.day), start_time) return time_data -def column_of_data(data_file_path, start, column, end="-1", units=""): +def column_of_data(path, start, column, end="-1", units=""): """This function extracts a column of data from a ProCoDA data file. - Parameters - ---------- - data_file_path : string - File path. If the file is in the working directory, then the file name - is sufficient. + Note: Column 0 is time. The first data column is column 1. - start : int - Index of first row of data to extract from the data file + :param path: The file path of the ProCoDA data file. If the file is in the working directory, then the file name is sufficient. + :type path: string + :param start: Index of first row of data to extract from the data file + :type start: int + :param end: Index of last row of data to extract from the data. Defaults to last row + :type end: int, optional + :param column: Index of the column that you want to extract OR name of the column header that you want to extract + :type column: int or string + :param units: The units you want to apply to the data, e.g. 'mg/L'. Defaults to "" (dimensionless) + :type units: string, optional - end : int, optional - Index of last row of data to extract from the data - Defaults to -1, which extracts all the data in the file + :return: Experimental data with the units applied. + :rtype: numpy.array - column : int or string - int: - Index of the column that you want to extract. Column 0 is time. - The first data column is column 1. - string: - Name of the column header that you want to extract + :Examples: - units : string, optional - The units you want to apply to the data, e.g. 'mg/L'. - Defaults to "" which indicates no units - - Returns - ------- - numpy array - Experimental data with the units applied. - - Examples - -------- - column_of_data(Reactor_data.txt, 0, 1, -1, "mg/L") + .. code-block:: python + data = column_of_data("Reactor_data.txt", 0, 1, -1, "mg/L") """ if not isinstance(start, int): start = int(start) if not isinstance(end, int): end = int(end) - df = pd.read_csv(data_file_path, delimiter='\t') + df = pd.read_csv(path, delimiter='\t') if units == "": if isinstance(column, int): data = np.array(pd.to_numeric(df.iloc[start:end, column])) @@ -335,27 +326,16 @@ def column_of_data(data_file_path, start, column, end="-1", units=""): return data -def notes(data_file_path): +def notes(path): """This function extracts any experimental notes from a ProCoDA data file. - Parameters - ---------- - data_file_path : string - File path. If the file is in the working directory, then the file name - is sufficient. - - Returns - ------- - dataframe - The rows of the data file that contain text notes inserted during the - experiment. Use this to identify the section of the data file that you - want to extract. - - Examples - -------- + :param path: The file path of the ProCoDA data file. If the file is in the working directory, then the file name is sufficient. + :type path: string + :return: The rows of the data file that contain text notes inserted during the experiment. Use this to identify the section of the data file that you want to extract. + :rtype: pandas.Dataframe """ - df = pd.read_csv(data_file_path, delimiter='\t') + df = pd.read_csv(path, delimiter='\t') text_row = df.iloc[0:-1, 0].str.contains('[a-z]', '[A-Z]') text_row_index = text_row.index[text_row].tolist() notes = df.loc[text_row_index] @@ -366,46 +346,29 @@ def read_state(dates, state, column, units="", path="", extension=".xls"): """Reads a ProCoDA file and outputs the data column and time vector for each iteration of the given state. - Parameters - ---------- - dates : string (list) - A list of dates or single date for which data was recorded, in - the form "M-D-Y" - - state : int - The state ID number for which data should be extracted - - column : int or string - int: - Index of the column that you want to extract. Column 0 is time. - The first data column is column 1. - string: - Name of the column header that you want to extract + Note: Column 0 is time. The first data column is column 1. - units : string, optional - The units you want to apply to the data, e.g. 'mg/L'. - Defaults to "" which indicates no units + :param dates: A single date or list of dates for which data was recorded, formatted "M-D-YYYY" + :type dates: string or string list + :param state: The state ID number for which data should be extracted + :type state: int + :param column: Index of the column that you want to extract OR header of the column that you want to extract + :type column: int or string + :param units: The units you want to apply to the data, e.g. 'mg/L'. Defaults to "" (dimensionless) + :type units: string, optional + :param path: The file path of the ProCoDA data file. If the file is in the working directory, then the file name is sufficient. + :type path: string + :param extension: The file extension of the tab delimited file. Defaults to ".xls" if no argument is passed in + :type extension: string, optional - path : string, optional - Optional argument of the path to the folder containing your ProCoDA - files. Defaults to the current directory if no argument is passed in + :return: time (numpy.array) - Times corresponding to the data (with units) + :return: data (numpy.array) - Data in the given column during the given state with units - extension : string, optional - The file extension of the tab delimited file. Defaults to ".xls" if - no argument is passed in + :Examples: - Returns - ------- - time : numpy array - Times corresponding to the data (with units) - - data : numpy array - Data in the given column during the given state with units - - Examples - -------- - time, data = read_state(["6-19-2013", "6-20-2013"], 1, 28, "mL/s") + .. code-block:: python + time, data = read_state(["6-19-2013", "6-20-2013"], 1, 28, "mL/s") """ data_agg = [] day = 0 @@ -484,42 +447,29 @@ def average_state(dates, state, column, units="", path="", extension=".xls"): """Outputs the average value of the data for each instance of a state in the given ProCoDA files - Parameters - ---------- - dates : string (list) - A list of dates or single date for which data was recorded, in - the form "M-D-Y" - - state : int - The state ID number for which data should be extracted + Note: Column 0 is time. The first data column is column 1. - column : int or string - int: - Index of the column that you want to extract. Column 0 is time. - The first data column is column 1. - string: - Name of the column header that you want to extract + :param dates: A single date or list of dates for which data was recorded, formatted "M-D-YYYY" + :type dates: string or string list + :param state: The state ID number for which data should be extracted + :type state: int + :param column: Index of the column that you want to extract OR header of the column that you want to extract + :type column: int or string + :param units: The units you want to apply to the data, e.g. 'mg/L'. Defaults to "" (dimensionless) + :type units: string, optional + :param path: The file path of the ProCoDA data file. If the file is in the working directory, then the file name is sufficient. + :type path: string + :param extension: The file extension of the tab delimited file. Defaults to ".xls" if no argument is passed in + :type extension: string, optional - units : string, optional - The units you want to apply to the data, e.g. 'mg/L'. - Defaults to "" which indicates no units + :return: A list of averages for each instance of the given state + :rtype: float list - path : string, optional - Optional argument of the path to the folder containing your ProCoDA - files. Defaults to the current directory if no argument is passed in + :Examples: - extension : string, optional - The file extension of the tab delimited file. Defaults to ".xls" if - no argument is passed in + .. code-block:: python - Returns - ------- - float list - A list of averages for each instance of the given state - - Examples - -------- - data_avgs = average_state(["6-19-2013", "6-20-2013"], 1, 28, "mL/s") + data_avgs = average_state(["6-19-2013", "6-20-2013"], 1, 28, "mL/s") """ data_agg = [] @@ -600,49 +550,33 @@ def perform_function_on_state(func, dates, state, column, units="", path="", ext """Performs the function given on each state of the data for the given state in the given column and outputs the result for each instance of the state - Parameters - ---------- - func : function - A function which will be applied to data from each instance of the state - - dates : string (list) - A list of dates or single date for which data was recorded, in - the form "M-D-Y" - - state : int - The state ID number for which data should be extracted - - column : int or string - int: - Index of the column that you want to extract. Column 0 is time. - The first data column is column 1. - string: - Name of the column header that you want to extract - - units : string, optional - The units you want to apply to the data, e.g. 'mg/L'. - Defaults to "" which indicates no units - - path : string, optional - Optional argument of the path to the folder containing your ProCoDA - files. Defaults to the current directory if no argument is passed in - - extension : string, optional - The file extension of the tab delimited file. Defaults to ".xls" if - no argument is passed in - - Returns - ------- - list - The outputs of the given function for each instance of the given state - - Requires - -------- - func takes in a list of data with units and outputs the correct units - - Examples - -------- - def avg_with_units(lst): + Note: Column 0 is time. The first data column is column 1. + + :param func: A function that will be applied to data from each instance of the state + :type func: function + :param dates: A single date or list of dates for which data was recorded, formatted "M-D-YYYY" + :type dates: string or string list + :param state: The state ID number for which data should be extracted + :type state: int + :param column: Index of the column that you want to extract OR header of the column that you want to extract + :type column: int or string + :param units: The units you want to apply to the data, e.g. 'mg/L'. Defaults to "" (dimensionless) + :type units: string, optional + :param path: The file path of the ProCoDA data file. If the file is in the working directory, then the file name is sufficient. + :type path: string + :param extension: The file extension of the tab delimited file. Defaults to ".xls" if no argument is passed in + :type extension: string, optional + + :requires: func takes in a list of data with units and outputs the correct units + + :return: The outputs of the given function for each instance of the given state + :type: list + + :Examples: + + .. code-block:: python + + def avg_with_units(lst): num = np.size(lst) acc = 0 for i in lst: @@ -650,8 +584,7 @@ def avg_with_units(lst): return acc / num - data_avgs = perform_function_on_state(avg_with_units, ["6-19-2013", "6-20-2013"], 1, 28, "mL/s") - + data_avgs = perform_function_on_state(avg_with_units, ["6-19-2013", "6-20-2013"], 1, 28, "mL/s") """ data_agg = [] day = 0 @@ -736,56 +669,40 @@ def read_state_with_metafile(func, state, column, path, metaids=[], certain state in each of the experiments (denoted by file paths in then metafile) - Parameters - ---------- - func : function - A function which will be applied to data from each instance of the state - - state : int - The state ID number for which data should be extracted + Note: Column 0 is time. The first data column is column 1. - column : int or string - int: - Index of the column that you want to extract. Column 0 is time. - The first data column is column 1. - string: - Name of the column header that you want to extract + :param func: A function that will be applied to data from each instance of the state + :type func: function + :param state: The state ID number for which data should be extracted + :type state: int + :param column: Index of the column that you want to extract OR header of the column that you want to extract + :type column: int or string + :param path: The file path of the ProCoDA data file (must be tab-delimited) + :type path: string + :param metaids: a list of the experiment IDs you'd like to analyze from the metafile + :type metaids: string list, optional + :param extension: The file extension of the tab delimited file. Defaults to ".xls" if no argument is passed in + :type extension: string, optional + :param units: The units you want to apply to the data, e.g. 'mg/L'. Defaults to "" (dimensionless) + :type units: string, optional - path : string - Path to your ProCoDA metafile (must be tab-delimited) + :return: ids (string list) - The list of experiment ids given in the metafile + :return: outputs (list) - The outputs of the given function for each experiment - metaids : string list, optional - a list of the experiment IDs you'd like to analyze from the metafile + :Examples: - extension : string, optional - The file extension of the tab delimited file. Defaults to ".xls" if - no argument is passed in + .. code-block:: python - units : string, optional - The units you want to apply to the data, e.g. 'mg/L'. - Defaults to "" which indicates no units + def avg_with_units(lst): + num = np.size(lst) + acc = 0 + for i in lst: + acc = i + acc - Returns - ------- - ids : string list - The list of experiment ids given in the metafile - - outputs : list - The outputs of the given function for each experiment - - Examples - -------- - def avg_with_units(lst): - num = np.size(lst) - acc = 0 - for i in lst: - acc = i + acc - - return acc / num - - path = "../tests/data/Test Meta File.txt" - ids, answer = read_state_with_metafile(avg_with_units, 1, 28, path, [], ".xls", "mg/L") + return acc / num + path = "../tests/data/Test Meta File.txt" + ids, answer = read_state_with_metafile(avg_with_units, 1, 28, path, [], ".xls", "mg/L") """ outputs = [] @@ -850,62 +767,31 @@ def avg_with_units(lst): def write_calculations_to_csv(funcs, states, columns, path, headers, out_name, metaids=[], extension=".xls"): """Writes each output of the given functions on the given states and data - columns to a new column in a - - Parameters - ---------- - funcs : function (list) - A function or list of functions which will be applied in order to the - data. If only one function is given it is applied to all the - states/columns - - states : string (list) - The state ID numbers for which data should be extracted. List should be - in order of calculation or if only one state is given then it will be - used for all the calculations - - columns : int or string (list) - If only one column is given it is used for all the calculations - int: - Index of the column that you want to extract. Column 0 is time. - The first data column is column 1. - string: - Name of the column header that you want to extract - - path : string - Path to your ProCoDA metafile (must be tab-delimited) - - headers : string list - List of the desired header for each calculation, in order - - out_name : string - Desired name for the output file. Can include a relative path - - metaids : string list, optional - a list of the experiment IDs you'd like to analyze from the metafile - - extension : string, optional - The file extension of the tab delimited file. Defaults to ".xls" if - no argument is passed in - - Returns - ------- - out_name.csv - A CSV file with the each column being a new calcuation and each row - being a new experiment on which the calcuations were performed - - output : DataFrame - Pandas dataframe which is the same data that was written to CSV - - Requires - -------- - funcs, states, columns, and headers are all of the same length if they are - lists. Some being lists and some single values are okay. - - Examples - -------- - - + columns to a new column in the specified output file. + + Note: Column 0 is time. The first data column is column 1. + + :param funcs: A function or list of functions which will be applied in order to the data. If only one function is given it is applied to all the states/columns + :type funcs: function or function list + :param states: The state ID numbers for which data should be extracted. List should be in order of calculation or if only one state is given then it will be used for all the calculations + :type states: string or string list + :param columns: The index of a column, the header of a column, a list of indexes, OR a list of headers of the column(s) that you want to apply calculations to + :type columns: int, string, int list, or string list + :param path: Path to your ProCoDA metafile (must be tab-delimited) + :type path: string + :param headers: List of the desired header for each calculation, in order + :type headers: string list + :param out_name: Desired name for the output file. Can include a relative path + :type out_name: string + :param metaids: A list of the experiment IDs you'd like to analyze from the metafile + :type metaids: string list, optional + :param extension: The file extension of the tab delimited file. Defaults to ".xls" if no argument is passed in + :type extension: string, optional + + :requires: funcs, states, columns, and headers are all of the same length if they are lists. Some being lists and some single values are okay. + + :return: out_name.csv (CVS file) - A CSV file with the each column being a new calcuation and each row being a new experiment on which the calcuations were performed + :return: output (Pandas.DataFrame)- Pandas DataFrame holding the same data that was written to the output file """ if not isinstance(funcs, list): funcs = [funcs] * len(headers) diff --git a/aguaclara/research/stock_qc.py b/aguaclara/research/stock_qc.py new file mode 100644 index 00000000..3a875e58 --- /dev/null +++ b/aguaclara/research/stock_qc.py @@ -0,0 +1,273 @@ +from aguaclara.core.units import unit_registry as u + + +class Stock(object): + """A stock of material in solution, with functions for calculations + involving flow rate and concentration. A parent class to be used in + Variable_C_Stock and Variable_Q_Stock. + """ + + def rpm(self, vol_per_rev, Q): + return (Q / vol_per_rev) + + def T_stock(self, V_stock, Q_stock): + return (V_stock / Q_stock) + + def M_stock(self, V_stock, C_stock): + return C_stock * V_stock + + def V_super_stock(self, V_stock, C_stock, C_super_stock): + return V_stock * (C_stock / C_super_stock).to(u.dimensionless) + + def dilution_factor(self, C_stock, C_super_stock): + return (C_stock / C_super_stock).to(u.dimensionless) + + +class Variable_C_Stock(Stock): + """A flow reactor with input from a stock of material of unknown + concentration. + + :Examples: + + >>> from aguaclara.research.stock_qc import Variable_C_Stock + >>> from aguaclara.core.units import unit_registry as u + >>> reactor = Variable_C_Stock(Q_sys = 1*u.mL/u.s, C_sys = 1.4*u.mg/u.L, Q_stock = .01*u.mL/u.s) + >>> reactor.C_stock() + + """ + + def __init__(self, Q_sys, C_sys, Q_stock): + """Initialize a reactor of unknown material stock concentration. + + :param Q_sys: Flow rate of the system + :type Q_sys: float + :param C_sys: Concentration of the material in the system + :type C_sys: float + :param Q_stock: Flow rate from the stock of material + :type Q_stock: float + """ + self._Q_sys = Q_sys + self._C_sys = C_sys + self._Q_stock = Q_stock + + def Q_sys(self): + """Return the flow rate of the system. + + :return: Flow rate of the system + :rtype: float + """ + return self._Q_sys + + def C_sys(self): + """Return the concentration of the material in the system. + + :return: Concentration of the material in the system + :rtype: float + """ + return self._C_sys + + def Q_stock(self): + """Return the flow rate from the stock of material. + + :return: Flow rate from the stock of material + :rtype: float + """ + return self._Q_stock + + def C_stock(self): + """Return the required concentration of material in the stock given a + reactor's desired system flow rate, system concentration, and stock + flow rate. + + :return: Concentration of material in the stock + :rtype: float + """ + return self._C_sys * (self._Q_sys / self._Q_stock).to(u.dimensionless) + + def rpm(self, vol_per_rev): + """Return the pump speed required for the reactor's stock of material + given the volume of fluid output per revolution by the stock's pump. + + :param vol_per_rev: Volume of fluid pumped per revolution (dependent on pump and tubing) + :type vol_per_rev: float + + :return: Pump speed for the material stock, in revolutions per minute + :rtype: float + """ + return Stock.rpm(self, vol_per_rev, self._Q_stock).to(u.rev/u.min) + + def T_stock(self, V_stock): + """Return the amount of time at which the stock of materal will be + depleted. + + :param V_stock: Volume of the stock of material + :type V_stock: float + + :return: Time at which the stock will be depleted + :rtype: float + """ + return Stock.T_stock(self, V_stock, self._Q_stock).to(u.hr) + + def M_stock(self, V_stock): + """Return the mass of undiluted material required for the stock + concentration. + + :param V_stock: Volume of the stock of material + :type V_stock: float + + :return: Mass of undiluted stock material + :rtype: float + """ + return Stock.M_stock(self, V_stock, self.C_stock()) + + def V_super_stock(self, V_stock, C_super_stock): + """Return the volume of super (more concentrated) stock that must be + diluted for the desired stock volume and required stock concentration. + + :param V_stock: Volume of the stock of material + :type V_stock: float + :param C_super_stock: Concentration of the super stock + :type C_super_stock: float + + :return: Volume of super stock to dilute + :rtype: float + """ + return Stock.V_super_stock(self, V_stock, self.C_stock(), C_super_stock) + + def dilution_factor(self, C_super_stock): + """Return the dilution factor of the concentration of material in the + stock relative to the super stock. + + :param C_super_stock: Concentration of the super stock + :type C_super_stock: float + + :return: dilution factor of stock concentration over super stock concentration (< 1) + :rtype: float + """ + return Stock.dilution_factor(self, self.C_stock(), C_super_stock) + + +class Variable_Q_Stock(Stock): + """A flow reactor with input from a stock of material at an unknown flow + rate. + + :Examples: + + >>> from aguaclara.research.stock_qc import Variable_Q_Stock + >>> from aguaclara.core.units import unit_registry as u + >>> reactor = Variable_Q_Stock(Q_sys = 1*u.mL/u.s, C_sys = 1.4*u.mg/u.L, C_stock = 7.6*u.mg/u.L) + >>> reactor.Q_stock() + + >>> reactor.rpm(vol_per_rev = .5*u.mL/u.rev).to(u.rev/u.min) + + """ + + def __init__(self, Q_sys, C_sys, C_stock): + """Initialize a reactor of unknown material stock flow rate. + + :param Q_sys: Flow rate of the system + :type Q_sys: float + :param C_sys: Concentration of the material in the system + :type C_sys: float + :param C_stock: Concentration of the material in the stock + :type C_stock: float + """ + self._Q_sys = Q_sys + self._C_sys = C_sys + self._C_stock = C_stock + + def Q_sys(self): + """Return the flow rate of the system. + + :return: Flow rate of the system + :rtype: float + """ + return self._Q_sys + + def C_sys(self): + """Return the concentration of the material in the system. + + :return: Concentration of the material in the system + :rtype: float + """ + return self._C_sys + + def C_stock(self): + """Return the concentration of the material in the stock. + + :return: Concentration of the material in the stock + :rtype: float + """ + return self._C_stock + + def Q_stock(self): + """Return the required flow rate from the stock of material given + a reactor's desired system flow rate, system concentration, and stock + concentration. + + :return: Flow rate from the stock of material + :rtype: float + """ + return self._Q_sys * (self._C_sys / self._C_stock).to(u.dimensionless) + + def rpm(self, vol_per_rev): + """Return the pump speed required for the reactor's stock of material + given the volume of fluid output per revolution by the stock's pump. + + :param vol_per_rev: Volume of fluid pumped per revolution (dependent on pump and tubing) + :type vol_per_rev: float + + :return: Pump speed for the material stock, in revolutions per minute + :rtype: float + """ + return Stock.rpm(self, vol_per_rev, self.Q_stock()).to(u.rev/u.min) + + def T_stock(self, V_stock): + """Return the amount of time at which the stock of materal will be + depleted. + + :param V_stock: Volume of the stock of material + :type V_stock: float + + :return: Time at which the stock will be depleted + :rtype: float + """ + return Stock.T_stock(self, V_stock, self.Q_stock()).to(u.hr) + + def M_stock(self, V_stock): + """Return the mass of undiluted material required for the stock + concentration. + + :param V_stock: Volume of the stock of material + :type V_stock: float + + :return: Mass of undiluted stock material + :rtype: float + """ + return Stock.M_stock(self, V_stock, self._C_stock) + + def V_super_stock(self, V_stock, C_super_stock): + """Return the volume of super (more concentrated) stock that must be + diluted for the desired stock volume and stock concentration. + + :param V_stock: Volume of the stock of material + :type V_stock: float + :param C_super_stock: Concentration of the super stock + :type C_super_stock: float + + :return: Volume of super stock to dilute + :rtype: float + """ + return Stock.V_super_stock(self, V_stock, self._C_stock, C_super_stock) + + def dilution_factor(self, C_super_stock): + """Return the dilution factor of the concentration of material in the + stock relative to the super stock. + + :param C_super_stock: Concentration of the super stock + :type C_super_stock: float + + :return: dilution factor of stock concentration over super stock concentration (< 1) + :rtype: float + """ + return Stock.dilution_factor(self, self._C_stock, C_super_stock) diff --git a/aguaclara/research/tube_sizing.py b/aguaclara/research/tube_sizing.py deleted file mode 100644 index 462b08eb..00000000 --- a/aguaclara/research/tube_sizing.py +++ /dev/null @@ -1,305 +0,0 @@ -from aguaclara.core.units import unit_registry as u -import numpy as np -import pandas as pd - -# pump rotor radius based on minimizing error between predicted and measured -# values -R_pump = 1.62 * u.cm - -# empirically derived correction factor due to the fact that larger diameter -# tubing has more loss ue to space smashed by rollers -k_nonlinear = 13 - -# maximum and minimum rpms for a 100 rpm pump -min_rpm = 3 * u.rev/u.min -max_rpm = 95 * u.rev/u.min - - -def Q6_roller(ID_tube): - """This function calculates the volume per revolution of a 6 roller pump - given the innner diameter (ID) of 3-stop tubing. It was empirically derived - using the table found at - http://www.ismatec.com/int_e/pumps/t_mini_s_ms_ca/tubing_msca2.htm - - Parameters - ---------- - ID_tube : float - inner diameter of the tube - - Returns - ------- - float - flow from the 6 roller pump (mL/rev) - - Examples - -------- - >>> Q6_roller(2.79*u.mm) - 0.4005495805189351 milliliter/rev - >>> Q6_roller(1.52*u.mm) - 0.14884596727278446 milliliter/rev - >>> Q6_roller(0.51*u.mm) - 0.01943899117521222 milliliter/rev - - """ - term1 = (R_pump * 2 * np.pi - k_nonlinear * ID_tube) / u.rev - term2 = np.pi * (ID_tube ** 2) / 4 - return (term1 * term2).to(u.mL/u.rev) - - -def ID_colored_tube(color): - """This function looks up the inner diameter of a tube from the tubing data - table given the color. - - Parameters - ---------- - color : string - color of the tubing to be used - - Returns - ------- - float - diameter of the tubing (mm) - - Examples - -------- - >>> ID_colored_tube("yellow-blue") - 1.52 millimeter - >>> ID_colored_tube("orange-yellow") - 0.51 millimeter - >>> ID_colored_tube("purple-white") - 2.79 millimeter - - """ - df = pd.read_csv("/data/tubing_data.txt", delimiter='\t') - idx = df["Color"] == color - return df[idx]['Diameter (mm)'].values[0] * u.mm - - -def C_stock_max(Q_plant, C, tubing_color): - """This function calculates the maximum stock concentration of a generic - material given desired concentration in the plant. - - Parameters - ---------- - Q_plant : float - flow rate of the plant - - C : float - desired concentration of the material within the plant - - tubing_color : string - color of the tubing to be used - - Returns - ------- - float - maximum stock concentration (g/L) - - Examples - -------- - >>> C_stock_max(7*u.mL/u.s, 100*u.NTU, "yellow-blue") - 159.89684125188708 gram/liter - >>> C_stock_max(7*u.mL/u.s, 2*u.mg/u.L, "orange-yellow") - 14.404039668326215 gram/liter - - """ - ID_tube = ID_colored_tube(tubing_color) - return (C * Q_plant / (Q6_roller(ID_tube) * min_rpm)).to(u.g/u.L) - - -def Q_stock_max(Q_plant, C, tubing_color): - """This function calculates the flow rate of the stock of the desired - concentration. - - Parameters - ---------- - Q_plant : float - flow rate of the plant - - C : float - desired concentration within the plant - - tubing_color : string - color of the tubing to be used - - Returns - ------- - float - flow rate of the stock (mL/s) - - Examples - -------- - >>> Q_stock_max(7*u.mL/u.s, 100*u.NTU, "yellow-blue") - 0.007442298363639224 milliliter/second - >>> Q_stock_max(7*u.mL/u.s, 2*u.mg/u.L, "orange-yellow") - 0.0009719495587606109 milliliter/second - - """ - return (C * Q_plant / C_stock_max(Q_plant, C, tubing_color)).to(u.mL/u.s) - - -def T_stock(Q_plant, C, tubing_color, V_stock): - """This function calculates the time after the experiment at which the - stock container will run out. - - Parameters - ---------- - Q_plant : float - flow rate of the plant - - C : float - desired concentration within the plant - - tubing_color : string - color of the tubing to be used - - V_stock : float - volume of the stock container - - Returns - ------- - float - time before the stock is depleted (hr) - - Examples - -------- - >>> T_stock(7*u.mL/u.s, 100*u.NTU, "yellow-blue", 1*u.L) - 37.324192635827984 hour - >>> T_stock(7*u.mL/u.s, 2*u.mg/u.L, "orange-yellow", 1*u.L) - 285.79443786361537 hour - - """ - return (V_stock / Q_stock_max(Q_plant, C, tubing_color)).to(u.hr) - - -def M_stock(Q_plant, C, tubing_color, V_stock): - """The mass of the material required to reach the desired stock - concentration. - - Parameters - ---------- - Q_plant : float - flow rate of the plant - - C : float - desired concentration within the plant - - tubing_color : string - color of the tubing to be used - - V_stock : float - volume of the stock container - - Returns - ------- - float - mass of material to be added to the stock container - - Examples - -------- - >>> M_stock(7*u.mL/u.s, 100*u.NTU, "yellow-blue", 1*u.L) - 159.89684125188708 gram - - """ - return (C_stock_max(Q_plant, C, tubing_color) * V_stock).to(u.g) - - -def V_super_stock(Q_plant, C, tubing_color, V_stock, C_super_stock): - """The volume of super stock added to the stock container to reach the - desired concentration within the plant. - - Parameters - ---------- - Q_plant : float - flow rate of the plant - - C : float - desired concentration within the plant - - tubing_color : string - color of the tubing to be used - - V_stock : float - volume of the stock container - - C_super_stock : float - concentration of the super stock to be diluted down to the - stock solution - - Returns - ------- - float - volume of super stock to be added to the stock container - - Examples - -------- - >>> V_super_stock(7*u.mL/u.s, 2*u.mg/u.L, "orange-yellow", 1*u.L, 69.4*u.g/u.L) - 207.55100386637196 milliliter - - """ - C_stock = C_stock_max(Q_plant, C, tubing_color) - return (V_stock * C_stock / C_super_stock).to(u.mL) - - -def Q_water(Q_plant, C_clay, C_pacl_min, tubing_clay, tubing_pacl): - """This function calculates the required flow rate for water from the tap - for the experiment. - - Parameters - ---------- - Q_plant : float - flow rate of the plant - - C_clay : float - concentration of clay to be added, i.e. the desired influent turbidity - - C_pacl_min : float - minimum coagulant dose of the mixed water in the flocculator - - tubing_clay - color of the tubing to be used for the clay stock - - tubing_clay - color of the tubing to be used for the PACL stock - - Returns - ------- - float - required flow rate for water for the experiment if it were coming from - a tap - - Examples - -------- - >>> Q_water(7*u.mL/u.s, 100*u.NTU, 0.2*u.mg/u.L, "yellow-blue", "orange-yellow") - 419.49514512465606 milliliter/minute - - """ - return (Q_plant - Q_stock_max(Q_plant, C_clay, tubing_clay) - - Q_stock_max(Q_plant, C_pacl_min, tubing_pacl)).to(u.mL/u.min) - - -def pump_rpm(Q, tubing_color): - """This function calculates the RPMs required for a given flow rate and - tube color. - - Parameters - ---------- - Q : float - desired flow rate - - tubing_color - color of the tubing to be used - - Returns - ------- - float - revolutions per minute to set the pump to for the desired flow rate - - Examples - -------- - >>> pump_rpm(0.01*u.mL/u.s, "yellow-blue") - 4.031012804669423 rev/minute - - """ - flow_per_rev = Q6_roller(ID_colored_tube(tubing_color)) - return (Q / flow_per_rev).to(u.rev/u.min) diff --git a/docs/source/core/constants.rst b/docs/source/core/constants.rst new file mode 100644 index 00000000..83381616 --- /dev/null +++ b/docs/source/core/constants.rst @@ -0,0 +1,5 @@ +Constants +========= + +.. automodule:: aguaclara.core.constants + :members: diff --git a/docs/source/core/core.rst b/docs/source/core/core.rst new file mode 100644 index 00000000..7530e93d --- /dev/null +++ b/docs/source/core/core.rst @@ -0,0 +1,14 @@ +Core +==== + +.. toctree:: + :maxdepth: 2 + + constants + drills + materials + physchem + pipeline + pipes + units + utility diff --git a/docs/source/core/drills.rst b/docs/source/core/drills.rst new file mode 100644 index 00000000..78165f05 --- /dev/null +++ b/docs/source/core/drills.rst @@ -0,0 +1,5 @@ +Drills +====== + +.. automodule:: aguaclara.core.drills + :members: diff --git a/docs/source/core/materials.rst b/docs/source/core/materials.rst new file mode 100644 index 00000000..6adc72d5 --- /dev/null +++ b/docs/source/core/materials.rst @@ -0,0 +1,5 @@ +Materials +========= + +.. automodule:: aguaclara.core.materials + :members: diff --git a/docs/source/core/physchem.rst b/docs/source/core/physchem.rst new file mode 100644 index 00000000..ebe7aff9 --- /dev/null +++ b/docs/source/core/physchem.rst @@ -0,0 +1,5 @@ +PhysChem +======== + +.. automodule:: aguaclara.core.physchem + :members: diff --git a/docs/source/core/pipeline.rst b/docs/source/core/pipeline.rst new file mode 100644 index 00000000..8760dc98 --- /dev/null +++ b/docs/source/core/pipeline.rst @@ -0,0 +1,5 @@ +Pipeline +======== + +.. automodule:: aguaclara.core.pipeline + :members: diff --git a/docs/source/core/pipes.rst b/docs/source/core/pipes.rst new file mode 100644 index 00000000..c688239b --- /dev/null +++ b/docs/source/core/pipes.rst @@ -0,0 +1,5 @@ +Pipes +===== + +.. automodule:: aguaclara.core.pipes + :members: diff --git a/docs/source/core/units.rst b/docs/source/core/units.rst new file mode 100644 index 00000000..58c6ea34 --- /dev/null +++ b/docs/source/core/units.rst @@ -0,0 +1,5 @@ +Units +===== + +.. automodule:: aguaclara.core.units + :members: diff --git a/docs/source/core/utility.rst b/docs/source/core/utility.rst new file mode 100644 index 00000000..fffc4ded --- /dev/null +++ b/docs/source/core/utility.rst @@ -0,0 +1,5 @@ +Utility +======= + +.. automodule:: aguaclara.core.utility + :members: diff --git a/docs/source/design/design.rst b/docs/source/design/design.rst new file mode 100644 index 00000000..e1a9ef29 --- /dev/null +++ b/docs/source/design/design.rst @@ -0,0 +1,7 @@ +Design +====== + +.. toctree:: + :maxdepth: 2 + + floc diff --git a/docs/source/design.rst b/docs/source/design/floc.rst similarity index 64% rename from docs/source/design.rst rename to docs/source/design/floc.rst index 6e1fa0b8..9de096f1 100644 --- a/docs/source/design.rst +++ b/docs/source/design/floc.rst @@ -1,5 +1,5 @@ -Design -====== +Flocculator +=========== Here's how to run the code: @@ -9,8 +9,5 @@ Here's how to run the code: floc = Flocculator() floc.channel_l -.. toctree:: - :maxdepth: 2 - - floc - +.. automodule:: aguaclara.design.floc + :members: diff --git a/docs/source/floc.rst b/docs/source/floc.rst deleted file mode 100644 index 4a323a07..00000000 --- a/docs/source/floc.rst +++ /dev/null @@ -1,5 +0,0 @@ -Flocculator -=========== - -.. automodule:: aguaclara.design.floc - :members: \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 9316333a..c626078e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -4,10 +4,11 @@ .. image:: images/logo.png -``aguaclara`` is a Python package built by `AguaClara Cornell `_. It contains Python classes for designing AguaClara water treatment plants, and functions for conducting research on water treatment and wastewater treatment processes. +``aguaclara`` is a Python package built by `AguaClara Cornell `_. It contains Python classes for designing AguaClara water treatment plants and functions for water treatment research. .. toctree:: - :maxdepth: 2 + :maxdepth: 1 - design - research + core/core + design/design + research/research diff --git a/docs/source/research.rst b/docs/source/research.rst deleted file mode 100644 index c08b6dc1..00000000 --- a/docs/source/research.rst +++ /dev/null @@ -1,5 +0,0 @@ -Research -======== - -.. automodule:: aguaclara.research.procoda_parser - :members: diff --git a/docs/source/research/environmental_processes_analysis.rst b/docs/source/research/environmental_processes_analysis.rst new file mode 100644 index 00000000..289f6da6 --- /dev/null +++ b/docs/source/research/environmental_processes_analysis.rst @@ -0,0 +1,5 @@ +Environmental Processes Analysis +================================ + +.. automodule:: aguaclara.research.environmental_processes_analysis + :members: diff --git a/docs/source/research/peristaltic_pump.rst b/docs/source/research/peristaltic_pump.rst new file mode 100644 index 00000000..8a174229 --- /dev/null +++ b/docs/source/research/peristaltic_pump.rst @@ -0,0 +1,5 @@ +Peristaltic Pump +================ + +.. automodule:: aguaclara.research.peristaltic_pump + :members: diff --git a/docs/source/research/procoda_parser.rst b/docs/source/research/procoda_parser.rst new file mode 100644 index 00000000..e025422c --- /dev/null +++ b/docs/source/research/procoda_parser.rst @@ -0,0 +1,6 @@ +ProCoDA Parser +============== + +.. automodule:: aguaclara.research.procoda_parser + :members: + :exclude-members: day_fraction, time_column_index, data_from_dates, column_start_to_end diff --git a/docs/source/research/research.rst b/docs/source/research/research.rst new file mode 100644 index 00000000..7d3ab553 --- /dev/null +++ b/docs/source/research/research.rst @@ -0,0 +1,10 @@ +Research +======== + +.. toctree:: + :maxdepth: 2 + + environmental_processes_analysis + peristaltic_pump + procoda_parser + stock_qc diff --git a/docs/source/research/stock_qc.rst b/docs/source/research/stock_qc.rst new file mode 100644 index 00000000..2d70c019 --- /dev/null +++ b/docs/source/research/stock_qc.rst @@ -0,0 +1,13 @@ +Stock QC +================ + +.. autoclass:: aguaclara.research.stock_qc.Variable_C_Stock + :members: + + .. automethod:: __init__ + + +.. autoclass:: aguaclara.research.stock_qc.Variable_Q_Stock + :members: + + .. automethod:: __init__ diff --git a/setup.py b/setup.py index 63a706b8..b81e447d 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='aguaclara', - version='0.0.23', + version='0.0.24', description='Open source functions for AguaClara water treatment research and plant design.', url='https://github.com/AguaClara/aguaclara', author='AguaClara at Cornell', diff --git a/tests/research/test_ProCoDA_Parser.py b/tests/research/test_ProCoDA_Parser.py index ce8d6329..d49e1255 100644 --- a/tests/research/test_ProCoDA_Parser.py +++ b/tests/research/test_ProCoDA_Parser.py @@ -23,23 +23,23 @@ def test_get_data_by_time(self): data_day2[0][0] = 0 # to remove scientific notation "e-" # SINGLE COLUMN, ONE DAY - output = get_data_by_time(path=path, columns=0, start_date="6-14-2018", start_time="12:20", end_time="13:00") + output = get_data_by_time(path=path, columns=0, dates="6-14-2018", start_time="12:20", end_time="13:00") self.assertSequenceEqual(np.round(output, 5).tolist(), data_day1[0][1041:1282]) # SINGLE COLUMN, TWO DAYS - output = get_data_by_time(path=path, columns=0, start_date="6-14-2018", end_date='6-15-2018', + output = get_data_by_time(path=path, columns=0, dates=["6-14-2018", "6-15-2018"], start_time="12:20", end_time="10:50") time_column = data_day1[0][1041:] + np.round(np.array(data_day2[0][:3901])+1, 5).tolist() self.assertSequenceEqual(np.round(output, 5).tolist(), time_column) # MULTI COLUMN, ONE DAY - output = get_data_by_time(path=path, columns=[0, 4], start_date="6-14-2018", start_time="12:20", + output = get_data_by_time(path=path, columns=[0, 4], dates=["6-14-2018"], start_time="12:20", end_time="13:00") self.assertSequenceEqual(np.round(output[0], 5).tolist(), data_day1[0][1041:1282]) self.assertSequenceEqual(np.round(output[1], 5).tolist(), data_day1[1][1041:1282]) # MULTI COLUMN, TWO DAYS - output = get_data_by_time(path=path, columns=[0, 4], start_date="6-14-2018", end_date='6-15-2018', + output = get_data_by_time(path=path, columns=[0, 4], dates=["6-14-2018", "6-15-2018"], start_time="12:20", end_time="10:50") time_column = data_day1[0][1041:] + np.round(np.array(data_day2[0][:3901])+1, 5).tolist() self.assertSequenceEqual(np.round(output[0], 5).tolist(), time_column) diff --git a/tests/research/test_floc_model.py b/tests/research/test_floc_model.py new file mode 100644 index 00000000..c24f015f --- /dev/null +++ b/tests/research/test_floc_model.py @@ -0,0 +1,45 @@ +""" +Tests for the research package's floc_model functions +""" + +import unittest +from aguaclara.core.units import unit_registry as u + +developing = False +if developing: + import sys + sys.path.append("../../aguaclara/research") + import floc_model as fm +else: + import aguaclara.research.floc_model as fm + + +class TestFlocModel(unittest.TestCase): + + def test_Material(self): + self.assertEqual(fm.Clay.name, 'Clay') + self.assertEqual(fm.Clay.Diameter, 7 * 10**-6 * u.m) + self.assertEqual(fm.Clay.Density, 2650 * u.kg/u.m**3) + self.assertEqual(fm.Clay.MolecWeight, None) + + def test_Chemical(self): + PaCl = fm.Chemical('PACl', 9 * 10 **-8 * u.m, 1138 * u.kg/u.m**3, 1.039 * u.kg/u.mol, 'PACl', AluminumMPM=13) + self.assertEqual(fm.PACl.name, 'PACl') + self.assertEqual(fm.PACl.Diameter, 9 * 10 **-8 * u.m) + self.assertEqual(fm.PACl.Density, 1138 * u.kg/u.m**3) + self.assertEqual(fm.PACl.MolecWeight, 1.039 * u.kg/u.mol) + self.assertEqual(fm.PACl.Precip, 'PACl') + self.assertEqual(fm.PACl.AluminumMPM, 13) + + self.assertEqual(fm.Alum.name, 'Alum') + self.assertEqual(fm.Alum.Diameter, 7 * 10 **-8 * u.m) + self.assertEqual(fm.Alum.Density, 2420 * u.kg/u.m**3) + self.assertEqual(fm.Alum.MolecWeight, 0.59921 * u.kg/u.mol) + self.assertEqual(fm.Alum.Precip, 'AlOH3') + self.assertEqual(fm.Alum.AluminumMPM, 2) + + self.assertEqual(fm.HumicAcid.name, 'Humic Acid') + self.assertEqual(fm.HumicAcid.Diameter, 72 * 10**-9 * u.m) + self.assertEqual(fm.HumicAcid.Density, 1780 * u.kg/u.m**3) + self.assertEqual(fm.HumicAcid.MolecWeight, None) + self.assertEqual(fm.HumicAcid.Precip, 'Humic Acid') diff --git a/tests/research/test_peristaltic_pump.py b/tests/research/test_peristaltic_pump.py new file mode 100644 index 00000000..c10bde09 --- /dev/null +++ b/tests/research/test_peristaltic_pump.py @@ -0,0 +1,43 @@ +""" +Tests for the research package's tube_sizing module. +""" + +import unittest +from aguaclara.core.units import unit_registry as u + +developing = False +if developing: + import sys + sys.path.append("../../aguaclara/research") + import peristaltic_pump as pp +else: + import aguaclara.research.peristaltic_pump as pp + + +class TestTubeSizing(unittest.TestCase): + + def assertAlmostEqualQuantity(self, first, second, places=7): + self.assertAlmostEqual(first.magnitude, second.magnitude, places) + self.assertAlmostEqual(first.units, second.units, places) + + def test_vol_per_rev_3_stop(self): + self.assertAlmostEqualQuantity(0.0013286183895203283*u.mL/u.rev, pp.vol_per_rev_3_stop(color="orange-black")) + self.assertAlmostEqualQuantity(0.14884596727278449*u.mL/u.rev, pp.vol_per_rev_3_stop(color="yellow-blue")) + self.assertAlmostEqualQuantity(0.0031160704169596186*u.mL/u.rev, pp.vol_per_rev_3_stop(inner_diameter=.20*u.mm)) + self.assertAlmostEqualQuantity(0.4005495805189351*u.mL/u.rev, pp.vol_per_rev_3_stop(inner_diameter=2.79*u.mm)) + + def test_ID_colored_tube(self): + self.assertEqual(1.52*u.mm, pp.ID_colored_tube("yellow-blue")) + self.assertEqual(0.51*u.mm, pp.ID_colored_tube("orange-yellow")) + self.assertEqual(2.79*u.mm, pp.ID_colored_tube("purple-white")) + + def test_vol_per_rev_LS(self): + self.assertEqual(0.06*u.mL/u.rev, pp.vol_per_rev_LS(13)) + self.assertEqual(1.6*u.mL/u.rev, pp.vol_per_rev_LS(15)) + self.assertEqual(3.8*u.mL/u.rev, pp.vol_per_rev_LS(18)) + self.assertEqual(4.8*u.mL/u.rev, pp.vol_per_rev_LS(36)) + + def test_flow_rate(self): + self.assertAlmostEqualQuantity(0.25*u.mL/u.s, pp.flow_rate(3*u.mL/u.rev, 5*u.rev/u.min)) + self.assertAlmostEqualQuantity(0.016666666666666666*u.mL/u.s, pp.flow_rate(.04*u.mL/u.rev, 25*u.rev/u.min)) + self.assertAlmostEqualQuantity(0.01*u.mL/u.s, pp.flow_rate(.001*u.mL/u.rev, 600*u.rev/u.min)) diff --git a/tests/research/test_stock_qc.py b/tests/research/test_stock_qc.py new file mode 100644 index 00000000..02fe50e4 --- /dev/null +++ b/tests/research/test_stock_qc.py @@ -0,0 +1,60 @@ +""" +Tests for the research package's tube_sizing module. +""" +import unittest +from aguaclara.core.units import unit_registry as u + +developing = False +if developing: + import sys + sys.path.append("../../aguaclara/research") + import stock_qc as stock_qc +else: + import aguaclara.research.stock_qc as stock_qc + +C_reactor = stock_qc.Variable_C_Stock(1*u.mL/u.s, 2*u.mg/u.L, 0.4*u.mL/u.s) +Q_reactor = stock_qc.Variable_Q_Stock(4.9*u.mL/u.s, 3.6*u.mg/u.L, 50*u.mg/u.L) + + +class TestStockQC(unittest.TestCase): + + def assertAlmostEqualQuantity(self, first, second, places=7): + self.assertAlmostEqual(first.magnitude, second.magnitude, places) + self.assertAlmostEqual(first.units, second.units, places) + + def test_init(self): + self.assertEqual(1*u.mL/u.s, C_reactor.Q_sys()) + self.assertEqual(2*u.mg/u.L, C_reactor.C_sys()) + self.assertEqual(0.4*u.mL/u.s, C_reactor.Q_stock()) + + self.assertEqual(4.9*u.mL/u.s, Q_reactor.Q_sys()) + self.assertEqual(3.6*u.mg/u.L, Q_reactor.C_sys()) + self.assertEqual(50*u.mg/u.L, Q_reactor.C_stock()) + + def test_C_Stock(self): + self.assertAlmostEqualQuantity(5.0*u.mg/u.L, C_reactor.C_stock()) + + def test_Q_Stock(self): + self.assertAlmostEqualQuantity(0.3528*u.mL/u.s, Q_reactor.Q_stock()) + + def test_rpm(self): + self.assertAlmostEqualQuantity(480*u.rev/u.min, C_reactor.rpm(0.05*u.mL/u.rev)) + self.assertAlmostEqualQuantity(88.2*u.rev/u.min, Q_reactor.rpm(0.24*u.mL/u.rev)) + + def test_T_stock(self): + self.assertAlmostEqualQuantity(3.4722222222222222*u.hr, C_reactor.T_stock(5*u.L)) + self.assertAlmostEqualQuantity(24.722852103804485*u.hr, Q_reactor.T_stock(31.4*u.L)) + + def test_M_stock(self): + self.assertAlmostEqualQuantity(25.0*u.mg, C_reactor.M_stock(5*u.L)) + self.assertAlmostEqualQuantity(1570.0*u.mg, Q_reactor.M_stock(31.4*u.L)) + + def test_V_super_stock(self): + self.assertAlmostEqualQuantity(0.00035714285714285714*u.L, + C_reactor.V_super_stock(5*u.L, 70*u.g/u.L)) + self.assertAlmostEqualQuantity(0.028035714285714285*u.L, + Q_reactor.V_super_stock(31.4*u.L, 56*u.g/u.L)) + + def test_dilution_factor(self): + self.assertEqual(7.142857142857142e-05*u.dimensionless, C_reactor.dilution_factor(70*u.g/u.L)) + self.assertEqual(0.0008928571428571429*u.dimensionless, Q_reactor.dilution_factor(56*u.g/u.L))