diff --git a/README.md b/README.md index 3010c4a4..4549324c 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,7 @@ the production outlets in sync. Here are the steps: * Update your package through pip and ensure the version number has changed: `$pip install aide_design --upgrade` and `$pip list` * If the version number now matches, you've successfully upgraded the pip package. + + +## Organizational Vision + diff --git a/aide_design/cdc_functions.py b/aide_design/cdc_functions.py index 7a61891b..7ec02ee7 100644 --- a/aide_design/cdc_functions.py +++ b/aide_design/cdc_functions.py @@ -1,10 +1,6 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Jun 28 13:57:23 2017 - -@author: cc2467 +"""This file is used to calculate the design paramters for the chemical dose +controller of an AguaClara plant. -This file does not follow PEP naming conventions for variables. - mw24 """ import numpy as np @@ -21,14 +17,14 @@ #============================================================================== def _DiamTubeAvail(en_tube_series = True): - if en_tube_series: + if en_tube_series: return 1*u.mm else: return (1/16)*u.inch # This section may be unnecessary #============================================================================== # def _DiamTubeAvail(en_tube_series = True): -# if en_tube_series: +# if en_tube_series: # return 1*u.mm # else: # return (1/16)*u.inch @@ -37,35 +33,35 @@ def _DiamTubeAvail(en_tube_series = True): @u.wraps(u.m**2/u.s, [u.kg/u.m**3, u.degK], False) def viscosity_kinematic_alum(conc_alum, temp): """Return the dynamic viscosity of water at a given temperature. - + If given units, the function will automatically convert to Kelvin. If not given units, the function will assume Kelvin. - This function assumes that the temperature dependence can be explained - based on the effect on water and that there is no confounding effect from + This function assumes that the temperature dependence can be explained + based on the effect on water and that there is no confounding effect from the coagulant. """ nu = (1 + (4.255 * 10**-6) * conc_alum**2.289) * pc.viscosity_kinematic(temp).magnitude return nu -@u.wraps(u.m**2/u.s, [u.kg/u.m**3, u.degK], False) +@u.wraps(u.m**2/u.s, [u.kg/u.m**3, u.degK], False) def viscosity_kinematic_pacl(conc_pacl, temp): """Return the dynamic viscosity of water at a given temperature. - + If given units, the function will automatically convert to Kelvin. If not given units, the function will assume Kelvin. - This function assumes that the temperature dependence can be explained - based on the effect on water and that there is no confounding effect from + This function assumes that the temperature dependence can be explained + based on the effect on water and that there is no confounding effect from the coagulant. """ nu = (1 + (2.383 * 10**-5) * conc_pacl**1.893) * pc.viscosity_kinematic(temp).magnitude return nu -@u.wraps(u.m**2/u.s, [u.kg/u.m**3, u.degK, None], False) +@u.wraps(u.m**2/u.s, [u.kg/u.m**3, u.degK, None], False) def viscosity_kinematic_chem(conc_chem, temp, en_chem): """Return the dynamic viscosity of water at a given temperature. - + If given units, the function will automatically convert to Kelvin. If not given units, the function will assume Kelvin. """ @@ -77,18 +73,18 @@ def viscosity_kinematic_chem(conc_chem, temp, en_chem): nu = pc.viscosity_kinematic(temp).magnitude return nu - - + + #============================================================================== # Flow rate Constraints for Laminar Tube Flow, Deviation from Linear Head Loss -# Behavior, and Lowest Possible Flow +# Behavior, and Lowest Possible Flow #============================================================================== @u.wraps(u.m**3/u.s, [u.m, u.m, None, None], False) def max_linear_flow(Diam, HeadlossCDC, Ratio_Error, KMinor): """Return the maximum flow that will meet the linear requirement. - Maximum flow that can be put through a tube of a given diameter without + Maximum flow that can be put through a tube of a given diameter without exceeding the allowable deviation from linear head loss behavior """ flow = (pc.area_circle(Diam)).magnitude * np.sqrt((2 * Ratio_Error * HeadlossCDC * pc.gravity)/ KMinor) @@ -98,14 +94,14 @@ def max_linear_flow(Diam, HeadlossCDC, Ratio_Error, KMinor): #============================================================================== -# Length of Tubing Required Given Head Loss, Max Flow, and Diameter +# Length of Tubing Required Given Head Loss, Max Flow, and Diameter #============================================================================== -# Length of tube required to get desired head loss at maximum flow based on -# the Hagen-Poiseuille equation. +# Length of tube required to get desired head loss at maximum flow based on +# the Hagen-Poiseuille equation. @u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.kg/u.m**3, u.degK, None, None], False) def _len_tube(Flow, Diam, HeadLoss, conc_chem, temp, en_chem, KMinor): - """Length of tube required to get desired head loss at maximum flow based on + """Length of tube required to get desired head loss at maximum flow based on the Hagen-Poiseuille equation.""" num1 = pc.gravity.magnitude * HeadLoss * np.pi * (Diam**4) denom1 = 128 * viscosity_kinematic_chem(conc_chem, temp, en_chem) * Flow @@ -117,68 +113,68 @@ def _len_tube(Flow, Diam, HeadLoss, conc_chem, temp, en_chem, KMinor): #============================================================================== -# Helper Functions +# Helper Functions #============================================================================== @u.wraps(None, [u.m**3/u.s, u.kg/u.m**3, u.kg/u.m**3, u.m, u.m, None, None], False) -def _n_tube_array(FlowPlant, ConcDoseMax, ConcStock, - DiamTubeAvail, HeadlossCDC, Ratio_Error, KMinor): - +def _n_tube_array(FlowPlant, ConcDoseMax, ConcStock, + DiamTubeAvail, HeadlossCDC, Ratio_Error, KMinor): + np.ceil((FlowPlant * ConcDoseMax - )/ ConcStock*max_linear_flow(DiamTubeAvail, HeadlossCDC, Ratio_Error, KMinor).magnitude) - return np.ceil((FlowPlant * ConcDoseMax) / - (ConcStock * max_linear_flow(DiamTubeAvail, HeadlossCDC, Ratio_Error, KMinor).magnitude)) + )/ ConcStock*max_linear_flow(DiamTubeAvail, HeadlossCDC, Ratio_Error, KMinor).magnitude) + return np.ceil((FlowPlant * ConcDoseMax) / + (ConcStock * max_linear_flow(DiamTubeAvail, HeadlossCDC, Ratio_Error, KMinor).magnitude)) @u.wraps(u.m**3/u.s, [u.m**3/u.s, u.kg/u.m**3, u.kg/u.m**3], False) def _flow_chem_stock(FlowPlant, ConcDoseMax, ConcStock): - FlowPlant * ConcDoseMax / ConcStock - return FlowPlant * ConcDoseMax / ConcStock - + FlowPlant * ConcDoseMax / ConcStock + return FlowPlant * ConcDoseMax / ConcStock + @u.wraps(u.m**3/u.s, [u.m**3/u.s, u.kg/u.m**3, u.kg/u.m**3, u.m, u.m,None,None], False) -def _flow_cdc_tube(FlowPlant, ConcDoseMax, ConcStock, +def _flow_cdc_tube(FlowPlant, ConcDoseMax, ConcStock, DiamTubeAvail, HeadlossCDC,Ratio_Error, KMinor): - + (_flow_chem_stock(FlowPlant, ConcDoseMax, ConcStock) - ) / (_n_tube_array(FlowPlant, ConcDoseMax, ConcStock, + ) / (_n_tube_array(FlowPlant, ConcDoseMax, ConcStock, DiamTubeAvail, HeadlossCDC,Ratio_Error, KMinor)) return (_flow_chem_stock(FlowPlant, ConcDoseMax, ConcStock).magnitude - ) / (_n_tube_array(FlowPlant, ConcDoseMax, ConcStock, + ) / (_n_tube_array(FlowPlant, ConcDoseMax, ConcStock, DiamTubeAvail, HeadlossCDC,Ratio_Error, KMinor)) - - - -# + + + +# @u.wraps(u.m, [u.m**3/u.s, u.kg/u.m**3, u.kg/u.m**3, u.m, u.m, u.degK, None, None], False) -def _length_cdc_tube_array(FlowPlant, ConcDoseMax, ConcStock, +def _length_cdc_tube_array(FlowPlant, ConcDoseMax, ConcStock, DiamTubeAvail, HeadlossCDC, temp, en_chem, KMinor): """Calculate the length of each diameter tube given the corresponding flow rate and coagulant. Choose the tube that is shorter than the maximum length tube.""" - + Flow = _flow_cdc_tube(FlowPlant, ConcDoseMax, ConcStock, DiamTubeAvail, HeadlossCDC,Ratio_Error, KMinor).magnitude - - + + return _len_tube(Flow, DiamTubeAvail, HeadlossCDC, ConcStock, temp, en_chem, KMinor).magnitude - + # Find the index of that tube @u.wraps(None, [u.m**3/u.s, u.kg/u.m**3, u.kg/u.m**3, u.m, u.m, u.m, u.degK, None, None], False) -def i_cdc(FlowPlant, ConcDoseMax, ConcStock, +def i_cdc(FlowPlant, ConcDoseMax, ConcStock, DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, temp, en_chem, KMinor): - - tube_array = (_length_cdc_tube_array(FlowPlant, ConcDoseMax, ConcStock, - DiamTubeAvail, HeadlossCDC, temp, en_chem, - KMinor)).magnitude - - + + tube_array = (_length_cdc_tube_array(FlowPlant, ConcDoseMax, ConcStock, + DiamTubeAvail, HeadlossCDC, temp, en_chem, + KMinor)).magnitude + + if tube_array[0] < float(LenCDCTubeMax): y = ut.floor_nearest(LenCDCTubeMax,tube_array) myindex =np.where(tube_array==y)[0][0] - + else: myindex = 0 - + return myindex @@ -188,48 +184,48 @@ def i_cdc(FlowPlant, ConcDoseMax, ConcStock, #============================================================================== @u.wraps(u.m, [u.m**3/u.s, u.kg/u.m**3, u.kg/u.m**3, u.m, u.m, u.m, u.degK, None, None], False) -def len_cdc_tube(FlowPlant, ConcDoseMax, ConcStock, - DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, temp, +def len_cdc_tube(FlowPlant, ConcDoseMax, ConcStock, + DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, temp, en_chem, KMinor): - """The length of tubing may be longer than the max specified if the stock - concentration is too high to give a viable solution with the specified + """The length of tubing may be longer than the max specified if the stock + concentration is too high to give a viable solution with the specified length of tubing.""" - index = i_cdc(FlowPlant, ConcDoseMax, ConcStock, + index = i_cdc(FlowPlant, ConcDoseMax, ConcStock, DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, temp, en_chem, KMinor) - len_cdc_tube = (_length_cdc_tube_array(FlowPlant, ConcDoseMax, ConcStock, - DiamTubeAvail, HeadlossCDC, temp, en_chem, + len_cdc_tube = (_length_cdc_tube_array(FlowPlant, ConcDoseMax, ConcStock, + DiamTubeAvail, HeadlossCDC, temp, en_chem, KMinor))[index].magnitude - + return len_cdc_tube @u.wraps(u.m, [u.m**3/u.s, u.kg/u.m**3, u.kg/u.m**3, u.m, u.m, u.m, u.degK, None, None], False) -def diam_cdc_tube(FlowPlant, ConcDoseMax, ConcStock, - DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, +def diam_cdc_tube(FlowPlant, ConcDoseMax, ConcStock, + DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, temp, en_chem, KMinor): - - index = i_cdc(FlowPlant, ConcDoseMax, ConcStock, - DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, + + index = i_cdc(FlowPlant, ConcDoseMax, ConcStock, + DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, temp, en_chem, KMinor) - + diam_cdc_tube = DiamTubeAvail[index] - + return diam_cdc_tube - -@u.wraps(None, [u.m**3/u.s, u.kg/u.m**3, u.kg/u.m**3, u.m, u.m, u.m, u.degK, None, None], False) -def n_cdc_tube(FlowPlant, ConcDoseMax, ConcStock, - DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, + +@u.wraps(None, [u.m**3/u.s, u.kg/u.m**3, u.kg/u.m**3, u.m, u.m, u.m, u.degK, None, None], False) +def n_cdc_tube(FlowPlant, ConcDoseMax, ConcStock, + DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, temp, en_chem, KMinor): - - index = i_cdc(FlowPlant, ConcDoseMax, ConcStock, - DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, + + index = i_cdc(FlowPlant, ConcDoseMax, ConcStock, + DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, temp, en_chem, KMinor) - - n_cdc_tube = _n_tube_array(FlowPlant, ConcDoseMax, ConcStock, + + n_cdc_tube = _n_tube_array(FlowPlant, ConcDoseMax, ConcStock, DiamTubeAvail, HeadlossCDC, Ratio_Error, KMinor)[index] - + return n_cdc_tube @@ -241,14 +237,14 @@ def n_cdc_tube(FlowPlant, ConcDoseMax, ConcStock, temp = u.Quantity(20,u.degC) HeadlossCDC = 20*(u.cm) ConcStock = 51.4*(u.gram/u.L) -ConcDoseMax = 2*(u.mg/u.L) +ConcDoseMax = 2*(u.mg/u.L) LenCDCTubeMax = 4 * u.m en_chem = 2 KMinor = 2 Ratio_Error=0.1 -x=len_cdc_tube(FlowPlant, ConcDoseMax, ConcStock, - DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, temp, +x=len_cdc_tube(FlowPlant, ConcDoseMax, ConcStock, + DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, temp, en_chem, KMinor) #print(x) #print(diam_cdc_tube(FlowPlant, ConcDoseMax, ConcStock, DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, temp, en_chem, KMinor).to(u.inch)) -#print(n_cdc_tube(FlowPlant, ConcDoseMax, ConcStock, DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, temp, en_chem, KMinor)) \ No newline at end of file +#print(n_cdc_tube(FlowPlant, ConcDoseMax, ConcStock, DiamTubeAvail, HeadlossCDC, LenCDCTubeMax, temp, en_chem, KMinor)) diff --git a/aide_design/constants.py b/aide_design/constants.py new file mode 100644 index 00000000..5c1033de --- /dev/null +++ b/aide_design/constants.py @@ -0,0 +1,879 @@ +#-*- coding: utf-8 -*- +"""This file contains constants which represent physical properties and +scientific principles which will be used in AguaClara plant design. + +""" +from aide_design.units import unit_registry as u +import aide_design.pipedatabase as pipe +import numpy as np + +#####tabulated constants + +GRAVITY = 9.80665 * u.m/u.s**2 + +#Density of water +DENSITY_WATER = 1000 * u.kg/u.m**3 + +#The kinematic viscosity of water +NU_WATER = 1 * 10**-6 * u.m**2/u.s + +#The influence of viscosity on mixing in jet reactors +RATIO_JET_ROUND = 0.5 + +#This is an estimate for plane jets as created in the flocculator and +# in the sed tank jet reverser. +RATIO_JET_PLANE = 0.225 + +RATIO_VC_ORIFICE = 0.63 + +P_ATM = 1*u.atm + +#Needed for the filter siphon design +NU_AIR = 12 *u.mm**2/u.s + +#Needed for the filter siphon design +RHO_AIR = 1.204 * u.kg/u.mm**3 + +####General assumptions and constants + +PLANT_ORIGIN=[0, 0, 0] * u.m + +COUNTRY = 0 + +LANGUAGE = 0 + +#Prompts the transition to a low flow plant +FLOW_PLANT_MAX_LF = 16.1 * u.L/u.s + +WIDTH_HUMAN_MIN = 0.5 * u.m + +#The height of the walkway above the drain channel bottom so that +# someone can walk through the drain channel. +HEIGHT_HUMAN_ACCESS = 1.5 * u.m + +#Used to set the minimum height of entrance, floc, and sed walls +HEIGHT_PLANT_FREE_BOARD = 0.1 * u.m + +#Minimum space between fittings in a tank or fittings and the wall of +# the tank. +SPACE_FITTING = 5 * u.cm + +#Minimum channel width for constructability +WIDTH_CHANNEL_MIN = 15 * u.cm + +#RATIO_RECTANGULAR is defined as the optimum "height over width ratio" +# (1/2) for a rectangular open channel +RATIO_RECTANGULAR = 0.5 + +##Equals 1 to draw boxes showing max water levels, 0 normally +#EN_WATER=0 #ASK Monroe + +##If EN_WATER is set to 1, this controls the filter operation mode for +# which water/sand elevations are drawn. 0 for terminal, 1 for clean bed, +# and 2 for backwash. +#EN_WATER=2 #ASK Monroe + +WIDTH_DOOR = 1 * u.m + +THICKNESS_ACRYLIC = 1 * u.cm + +#Due to a 24 in LFOM because that's the biggest pipe we have in our +# database right now. if we need a bigger single train, we can do that +# by adding that pipe size into the pipe database +FLOW_TRAIN_MAX = 150.1 * u.L/u.s + +def en_multiple_train(flow_plant): + if flow_plant > 60 * u.L/u.s: + return 1 + else: + return 0 + + +def n_train(flow_plant): + if flow_plant > 60*u.L/u.s: + return 1 + elif flow_plant > 60*u.L/u.s and flow_plant <= 120 * u.L/u.s: + return 2 + else: + return 4 + + +def flow_train(flow_plant): + return flow_plant / n_train(flow_plant) + +#####Flow orifice meter + +#Maximum number of rows or orifices in lfom. +RATIO_LFOM_ORIFICE = 10 + +#Safety coefficient that ensures free fall at the bottom of the lfom pipe +RATIO_LFOM_SAFETY = 1.5 + +#Minimum safety coefficient for lfom pipe diameter; only reduced +# between 55 L/s and 70 L/s - the intermediate zone between the using an +# LFOM pipe and an LFOM channel. +#It may be possible to eliminate this if we switch to plate LFOM at 50 Lp +RATIO_LFOM_SAFETY_MIN = 1.15 + +#Minimum head loss through linear orifice meter +HEADLOSS_LFOM_MIN = 20 * u.cm + +#Maximum head loss through linear orifice meter to be used as needed for high flow plants. +HEADLOSS_LFOM_MAX = 40 * u.cm + +#Changed from 12 in by pc479 because this is not a constraint anymore +# because we don't have an elbow constraining us. LFOM still needs to fit +# in the entrance tank. Need to check this constraint (mrf222) +NOM_DIAM_LFOM_PIPE_MAX = 36 * u.inch + +HEIGHT_LFOM_FREEFALL = 10 * u.cm + +#Enumerated type that selects between pipe(1) and plate(0) for the LFOM +def en_lfom_pipe(flow_plant): + if flow_plant >= 80 * u.L/u.s: + return 0 + else: + return 1 + +####entrance tank + +#Used to make a smaller entrance tank if the source water doesn't contain grit. +##0 if we want entrance tank sized to capture grit, 1 for minimum size +##EN_GRIT=0 ASK Monroe + +##0 if there is only one inlet, 1 if there is two inlet +##EN_TWO_INLETS=0 + +#Angle of the sloped walls of the entrance tank hoppers +ANGLE_ENT_TANK_SLOPE = 45 * u.deg + +#Extra space around the float (increase in effective diameter) to ensure +# that float has free travel +SPACE_ENT_TANK_FLOAT = 5 * u.cm + +#Increased to get better mixing (10/10/2015 by Monroe) +ENERGY_DIS_RAPID_MIX = 3 * u.W/u.kg + +#Distance that the rapid mix coupling extends into the first floc channel +# so that the RM orifice place can be fixed in place. +LENGTH_FLOC_COUPLING_EXT = 5 * u.cm + +WIDTH_ENT_TANK_HOPPER_PEAK = 3 * u.cm + +#Distance from the front wall to the pipe stubs in the hopper drains so +# that an operator can easily reach them. +LENGTH_ENT_TANK_WALLTODRAIN_MAX = 40 * u.cm + +#Entrance tank capture velocity +VEL_ENT_TANK_CAPTURE_BOD = 8 * u.mm/u.s + +AN_ENT_TANK_PLATE = 50 * u.deg + +SPACE_ENT_TANK_PLATE = 2.5 * u.cm + +THICKNESS_ENT_TANK_PLATE = 2 * u.mm + +DIST_CENTER_ENT_TANK_PLATE = SPACE_ENT_TANK_PLATE + THICKNESS_ENT_TANK_PLATE + +NOM_DIAM_ENT_TANK_MOD = 0.5 * u.inch + +NOM_DIAM_ENT_TANK_MOD_SPACER = 0.75 * u.inch + +#Thickness of the PVC disk used as the float for the chemical dose +# controller lever arm. +THICKNESS_ENT_TANK_FLOAT = 5 * u.cm + +SPACE_ENT_TANK_LAMINA_PIPETOEDGE = 5 * u.cm + +NOM_DIAM_RAPID_MIX_PLATE_RESTRAINER = 0.5 * u.inch + +#Nom diam of the pipes that are embedded in the entrance tank slope +# to support the plate settler module +NOM_DIAM_ENT_TANK_PLATE_SUPPORT = 3 * u.inch + +####chemical dose controller + +##0 is alum, 1 is PACl +##EN_COAG=1 + +M_COAG_SACK = 25 * u.kg + +#The coagulant stock is relatively stable and can last many days. Here we +# set the minimum time the coagulant stock will last when applying the +# maximum possible dose to size the stock tanks. In general the dose will +# be less than this and the stock will last much longer. +TIME_COAG_STOCK_MIN_EST = 1 * u.day + +#Want chlorine stock to run out on average every day so that the stock +# is made fresh frequently because the chlorine stock degrades with time +# depending on temperature, concentration, and pH. +TIME_CHLOR_STOCK_AVE = 1 * u.day + +ID_COAG_TUBE = 0.125 * u.inch +#1/8" tubes are readily available in hardware stores in Honduras +ID_CHLOR_TUBE = 0.125 * u.inch + +CONC_COAG_STOCK_EST = 150 * u.g/u.L + +CONC_CHLOR_STOCK_EST1 = 15 * u.g/u.L + +P_CHLOR = 0.7 + +#This is the elevation difference between the outlet of the coagulant +# stock tanks and the water level in the constant head tank, which is set +# by the hydraulic head required to provide the desired max chemical flow +# rate through the float valve orifice in the CHT. +#It is treated as constant here to ensure a practical elevation difference +# is left between the stock tanks and the CHT even when a float valve is +# selected which requires very little hydraulic head to deliver the +# required maximum chemical flow rate. +HEIGHT_COAG_TANK_ABOVE_HEAD_TANK = 30 * u.cm + +#This is the distance from the bottom of the stock tanks to the outlets +# to allow space for solids to settle. +DIST_CENTER_STOCK_OUTLET = 10 * u.cm + +#Distance between a tank and the border of the platform +SPACE_CHEM_TANK_BORDER = 5 * u.cm + +#This is the estimated elevation difference between the water level in +# the constant head tank and the top of the entrance tank wall. +#The constant head tank water level is the same as the elevation of the +# outlet of the dosing tube when the lever arm is horizontal (zero flow). +#Therefore this height depends only on the hardware used to make the +# slider/drop tube assembly and to mount the lever arm to the entrance +# tank wall. +#Note that this will vary depending on hardware used, and is only +# defined here to calculate the elevation of the stock tanks, which can +# be approximate. +HEIGHT_DOSER_ASSEMBLY = 6.77 * u.cm + +#Maximum error allowed between a linear flow vs tube head loss +# relationship and the actual performance (which is affected by non-linear +# minor losses), assuming calibration at the maximum flow rate. +RATIO_LINEAR_CDC_ERROR = 0.1 + +#Estimated minor loss coefficient for the small-diameter flexible tubing +# using fittings that have larger ID than the tubing. +K_MINOR_CDC_TUBE = 2 + +#Head loss through the doser at maximum flow rate. +#Maximum head loss through the small-diameter dosing tubing, which +# corresponds to the variation in water levels in the entrance tank and +# the difference between the maximum and minimum elevation of the dosing +# tube outlet attached to the lever arm. +HEADLOSS_CDC = 20 * u.cm + +#Estimated distance between fluid level in constant head tank and float valve orifice +HEIGHT_CDC_FLOAT_VALVE = 5 * u.cm + +#Nominal diameter of the PVC plumbing for the chlorine dosing system. +NOM_DIAM_CHLOR_PIPE = 0.5 * u.inch + +#Nominal diameter of the PVC plumbing for the coagulant dosing system. +NOM_DIAM_COAG_PIPE = 0.5 * u.inch + +#Supplier Information: +# http://www.rotoplas.com/assets/files/industria/catalogo.pdf +#5-gallon bucket +# http://www.mcmaster.com/#storage-buckets/=kd23oh +#35-gallon drum +# http://www.jlmovingsupplies.com/c31/DIXIE-OPEN-CLOSED-HEAD-DRUMS-p36721.html +VOL_CHEM_TANK_AVAIL = [5 * u.gal, 35 * u.gal, 55 * u.gal, + 450 * u.L, 750 * u.L, 1100 * u.L, 2500 * u.L] + +DIAM__CHEM_TANK_AVAIL = [11.875 * u.inch, 20.75 * u.inch, 22.5 * u.inch, + 0.85 * u.m, 1.10 * u.m, 1.10 * u.m, 1.55 * u.m] + +HEIGHT_CHEM_TANK_AVAIL = [17.75 * u.inch, 31.75 * u.inch, 33.5 * u.inch, + 0.99 * u.inch, 1.02 * u.inch, 1.39 * u.inch, + 1.65 * u.inch] + +####Chemical dose controller dimensions (based on inserted drawings) + +#st587 addition +LENGTH_CDC_LEVER_ARM = 0.5 * u.m + +DIAM_CDC_LEVER_CYLINDER1 = 1 * u.inch + +DIAM_CDC_LEVER_CYLINDER4 = 2 * u.inch + +DIAM_CDC_LEVER_CYLINDER_2 = 0.5 * u.inch + +LENGTH_CDC_LEVER_PIVOTTO_CYLINDER2 = 6 * u.cm + +LENGTH_CDC_LEVER_CYLINDER_2TO3 = 9.5 * u.cm + +LENGTH_CDC_LEVER_PIVOT_BOX = 2 * u.inch + +WIDTH_CDC_LEVER_PIVOT_BOX = 1 * u.inch + +HEIGHT_CDC_LEVER_PIVOT_BOX = 1 * u.inch + +THICKNESS_CDC_LEVER_ARM = 0.125 * u.inch + +HEIGHT_CDC_LEVER_ARM = 1 * u.inch + +LENGTH_CDC_LEVER_INNERBAR = 7 * u.inch + +LENGTH_CDC_LEVER_MOUNTING_PLATE = 6 * u.inch + +WIDTH_CDC_LEVER_MOUNTING_PLATE = 0.5 * u.cm + +HEIGHT_CDC_LEVER_MOUNTING_PLATE = 2 * u.inch + +SPACE_LEVER_TO_ENT_TANK_Z_TOP = 1 * u.cm + +THICKNESS_CDC_FLOAT = 5 * u.cm + +DIAM_CDC_FLOAT_CABLE = 0.5 * u.cm + +LENGTH_CDC_LEVER_SLIDER_ORIGIN_TO_SCREW = 1 * u.inch + +THICKNESS_CDC_LEVER_SLIDER = 0.25 * u.inch + +HEIGHT_CDC_LEVER_SLIDER = 1.5 * u.inch + +LENGTH_CDC_LEVER_SLIDER = 3 * u.inch + +HEIGHT_CDC_LEVER_SLIDER_SHORT = 0.125 * u.inch + +LENGTH_CDC_LEVER_CYLINDER = 6 * u.inch + +LENGTH_ENT_TANK_FRONT_WALL_TO_CDC_FLOAT = 0.874 * u.m + +LENGTH_CDC_LEVER = 0.5 * u.m #This may be obsolete now... mrf222 2/10/16 + +WIDTH_LEVER_ARM = 0.0032 * u.m + +HEIGHT_LEVER_ARM = 0.0254 * u.m + +DIAM_CDC_CHT = 6 * u.inch + +#Distance from the top of the entrance tank to the to the middle of the +# lever arm hole for the cable - (minus the) radius of the hole. +HEIGHT_LEVER_HOLE = 0.0132 * u.m - (0.0095/2 * u.m) + +DIAM_CABLE = 0.1 * u.inch + +#Edited DLABOrigintoLAOriginZ to accommodate dimensions from McMaster +# vs Inserted Drawing +DIAM_LAB_ORIGIN_TO_LA_ORIGIN_Z = 0.0245 * u.m + +#Distance from the lever arm origin to the outside center of the top part +# of the drop tube in the y direction. +LENGTH_LA_ORIGIN_TO_DT_Y = 0.7812 * u.m + +#Distance from the lever arm origin to the drop tube in the z direction. +LENGTH_LA_ORIGIN_TO_DT_Z = 0.0429 * u.m + +#Distance from the lever arm origin to the center of the drop tube in the +#x direction. +LENGTH_LA_ORIGIN_TO_DT_CENTER_X = 0.0290 * u.m + +#Measured from CDC research team's apparatus. +THICKNESS_CDC_REDUCER = 9.5 * u.mm + +#Distance from the lever arm origin to the center of the reducer in the +# x direction. +LENGTH_LA_ORIGIN_TO_REDUCER_X = 0.0290 * u.m + +#Distance from the lever arm origin to the outside center of the top part +# of the reducer in the y direction. +LENGTH_LA_ORIGIN_TO_REDUCER_Y = 0.7135 * u.cm + +#Distance from the lever arm origin to the center of the reducer in the +# x direction. +LENGTH_LA_ORIGIN_TO_REDUCER_CENTER_X = 0.0290 * u.m + +#Distance from the lever arm origin to the center of the reducer in the +# y direction. +LENGTH_LA_ORIGIN_TO_REDUCER_CENTER_Y = 0.7919 * u.m + +WIDTH_LEVER_BRACKET = 0.625 * u.inch + +LENGTH_LEVER_BRAKCET = 1.5 * u.inch + +RADIUS_LA_BAR = 0.375 * u.inch + +THICKNESS_LEVER_BRACKET = 0.08 * u.inch + +DIAM_LA_BAR = 0.375 * u.inch + +LENGTH_LA_BAR = 4 * u.inch + +LENGTH_SLIDER = 3 * u.inch + +WIDTH_SLIDER = 3.2 * 10**-3 * u.m + +NOM_DIAM_DROPTUBE = 0.5 * u.inch + +HEIGHT_SLIDER = 0.625 * u.inch + +LENGTH_DROPTUBE = 0.61 * u.m + +#The length of the drop tube needs to be calculated. The drop tube must be +# as long as the supercritical flow. +#Thus the drop tube must extend down to the elevation of the sed tank +# effluent weir. This constant should be removed! + +#Outer diameter of fitting- measured from CDC research team's fittin +OUTER_DIAM_CDC_FITTING = 5/32 * u.inch + +#Inner diameter of fitting- measured from CDC research team's fitting +ID_CDC_FITTING = 0.126 * u.inch + +#Length of fitting - measured from CDC research team's fitting +LENGTH_CDC_FITTING = 0.75 * u.inch + +#st587 addition +##Constant Head Tank Dimensions + +#five gallons bucket dimensions for constant head tanks + +DIAM_CHT = 10 * u.cm + +HEIGHT_CHT = 37/3 * u.cm + +THICKNESS_CHT_WALL = 1/3 * u.cm + +NOM_DIAM_DELIVERY_PIPE = 0.6 * u.inch + +PIPE_SCHEDULE_FLEX_TUBE = 2 + +LENGTH_PVC_BALL_VALVE = 0.1625/4 * u.cm + +THICKNESS_MOUNTING_BOARD = 1.5 * u.inch + +##Manifold Dimensions + +SPACE_CDC_LEVER_TO_MANIFOLD = 40 * u.cm + +LENGTH_CHLOR_AIR_RELEASE_PIPE = 30 * u.cm #Arbitratily selected + + +####Flocculator +##The minor loss coefficient is 2. According to measurements at Agalteca +# and according to +# https://confluence.cornell.edu/display/AGUACLARA/PAHO+Water+Treatment+Publications +# (page 100 in chapter on flocculation) +HEIGHT_FLOC_OPTION = 0 + +##Increased both to provide a safety margin on flocculator head loss and +# to simultaneously scale back on the actual collision potential we are +# trying to achieve. +K_MINOR_FLOC_BAFFLE = 2.5 + +SPACE_FLOC_BAFFLE_SET_BACK_PLASTIC= 2 * u.cm + +###Target flocculator collision potential basis of design +COLL_POT_FLOC_BOD = 75 * u.m**(2/3) + +##Minimum width of flocculator channel required for constructability based +# on the width of the human hip +FLOC_WIDTH_MIN_CONST = 45 * u.cm + +##Minimum and maximum distance between expansions to baffle spacing ratio for +#flocculator geometry that will provide optimal efficiency. +RATIO_HS_MIN = 3 +RATIO_HS_MAX = 6 + +##Ratio of the width of the gap between the baffle and the wall and the +# spacing between the baffles. +RATIO_FLOC_BAFFLE = 1 + +##Max energy dissipation rate in the flocculator, basis of design. +ENERGY_DIS_FLOC_BOD = 10* u.mW/u.kg + +TIME_FLOC_DRAIN = 15 * u.min + +NOM_DIAM_FLOC_MOD = 0.5 * u.inch + +NOM_DIAM_FLOC_SPACER = 0.75 * u.inch + +SPACE_FLOC_MOD_EDGE_TO_LAST_PIPE = 10 * u.cm + +NOM_DIAM_FLOC_RM_RESTRAINER = 0.5 * u.inch + +###Height that the drain stub extends above the top of the flocculator wall +LENGTH_FLOC_DRAIN_STUB_EXT = 20 * u.cm + +SPACE_FLOC_MOD_PIPE_TO_EDGE = 10 * u.cm + +THICKNESS_FLOC_BAFFLE = 2 * u.mm + + + +###Sedimentation tank +##General + +VEL_SED_UP_BOD = 1 * u.mm/u.s + +##Plate settler +VEL_SED_CONC_BOD = 0.12 * u.mm/u.s # capture velocity + +ANGLE_SED_PLATE = 60 * u.deg + +SPACE_SED_PLATE = 2.5*u.cm + +N_SED_MODULE_PLATES_MIN = 8 + +# This is moved to template because THICKNESS_SED_PLATE is in materials.yaml +# DIST_CENTER_SED_PLATE = SPACE_SED_PLATE + THICKNESS_SED_PLATE + +# Bottom of channel +ANGLE_SED_SLOPE = 50 * u.deg + +##This slope needs to be verified for functionality in the field. +# A steeper slope may be required in the floc hopper. +ANGLE_SED_HOPPER_SLOPE = 45 * u.deg + +HEIGHT_WATER_SED_EST = 2 * u.m + +SED_GATE_VALVE_URL = "https://confluence.cornell.edu/download/attachments/173604905/Sed-Scaled-Gate-Valve-Threaded.dwg" + +SUPPORT_BOLT_URL = "https://confluence.cornell.edu/download/attachments/173604905/PlateSettlerSupportBolt.dwg" + +##Inlet channel +HEADLOSS_SED_WEIR_MAX = 5 * u.cm + +##Height of the inlet channel overflow weir above the normal water level +# in the inlet channel so that the far side of the overflow weir does not +# fill with water under normal operating conditions. This means the water +# level in the inlet channel will increase when the inlet overflow weir +# is in use. +HEIGHT_SED_INLET_WEIR_FREE_BOARD = 2 * u.cm + +THICKNESS_SED_WEIR = 5*u.cm + +HL_SED_INLET_MAX = 1 * u.cm + +# ratio of the height to the width of the sedimentation tank inlet channel. +RATIO_HW_SED_INLET = 0.95 + +##Exit launder +##Target headloss through the launder orifices +HEADLOSS_SED_LAUNDER_BOD = 4 * u.cm + +##Acceptable ratio of min to max flow through the launder orifices +RATIO_FLOW_LAUNDER_ORIFICES = 0.80 + +##Center to center spacing of orifices in the launder +DIST_CENTER_SED_LAUNDER_EST = 10 * u.cm + +##The additional length needed in the launder cap pipe that is to be +# inserted into the launder coupling +LENGTH_SED_LAUNDER_CAP_EXCESS = 3 * u.cm + +##Space between the top of the plate settlers and the bottom of the +# launder pipe +HEIGHT_LAMELLA_TO_LAUNDER = 5 * u.cm + +##The additional length needed in the launder cap pipe that is to be +# inserted into the launder coupling + +##Diameter of the pipe used to hold the plate settlers together +NOM_DIAMETER_SED_MOD = 0.5 * u.inch + +##Diameter of the pipe used to create spacers. The spacers slide over the +# 1/2" pipe and are between the plates +NOM_DIAMETER_SED_MOD_SPACER = 0.75 * u.inch + +SDR_SED_MOD_SPACER = 17 + +##This is the vertical thickness of the lip where the lamella support sits. mrf222 +THICKNESS_SED_LAMELLA_LEDGE = 8 * u.cm + +SPACE_SED_LAMELLA_PIPE_TO_EDGE = 5 * u.cm + +##Approximate x-dimension spacing between cross pipes in the plate settler +# support frame. +DIST_CENTER_SED_PLATE_FRAME_CROSS_EST = 0.8 * u.m + +##Estimated plate length used to get an initial estimate of sedimentation +# tank active length. +LENGTH_SED_PLATE_EST = 60 * u.cm + +##Pipe size of the support frame that holds up the plate settler modules +NOM_DIAMETER_SED_PLATE_FRAME = 1.5 * u.inch + +##Floc weir + +#Vertical distance from the top of the floc weir to the bottom of the pipe +# frame that holds up the plate settler modules +HEIGHT_FLOC_WEIR_TO_PLATE_FRAME = 10 * u.cm + +##Minimum length (X dimension) of the floc hopper +LENGTH_SED_HOPPER_MIN = 50 * u.cm + +##Inlet manifold +##Max energy dissipation rate in the sed diffuser outletS +ENERGY_DIS_SED_INT_MAX = 150 * u.mW/u.kg + +##Ratio of min to max flow through the inlet manifold diffusers +RATIO_FLOW_SED_INLET = 0.8 + +ND_SED_MANIFOLD_MAX = 8 * u.inch + +SDR_SED_MANIFOLD = 41 # SDR of pipe for sed tank inlet manifold + +##This is the minimum distance between the inlet manifold and the slope +# of the sed tank. +SPACE_SED_INLET_MAN_SLOPE = 10 * u.cm + +##Length of exposed manifold stub coming out of the floc weir to which the +# free portion of the inlet manifold is attached with a flexible coupling. +LENGTH_SED_MAN_CONNECTION_STUB = 4 * u.cm + +##Space between the end of the manifold pipe and the edge of the first +# diffuser's hole, or the first manifold orifice. + +LENGTH_SED_MANIFOLD_FIRST_DIFFUSER_GAP = 3 * u.cm + +##Vertical distance from the edge of the jet reverser half-pipe to the tip +# of the inlet manifold diffusers +HEIGHT_JET_REVERSER_TO_DIFFUSERS = 3 * u.cm + +##Gap between the end of the inlet manifold pipe and the end wall of the +# tank to be able to install the pipe +LENGTH_SED_MANIFOLD_PIPE_FROM_TANK_END = 2 *u.cm + +LENGTH_SED_WALL_TO_DIFFUSER_GAP_MIN = 3 * u.cm + +##Diameter of the holes drilled in the manifold so that the molded 1" +# diffuser pipes can fit tightly in place (normal OD of a 1" pipe is +# close to 1-5/16") +DIAM_SED_MANIFOLD_PORT = 1.25 * u.inch + +ND_JET_REVERSER = 3 * u.inch # nominal diameter of pipe used for jet reverser in bottom of set tank + +SDR_REVERSER = 26 # SDR of jet reverser pipe + +## Diffuser geometry +SDR_DIFFUSER = 26 # SDR of diffuser pipe + +ND_DIFFUSER_PIPE = 4 * u.cm # nominal diameter of pipe used to make diffusers + +AREA_PVC_DIFFUSER = (np.pi/4) * ((pipe.OD(ND_DIFFUSER_PIPE)**2) + - (pipe.ID_SDR(ND_DIFFUSER_PIPE, SDR_DIFFUSER))**2) + +RATIO_PVC_STRETCH = 1.2 # stretch factor applied to the diffuser PVC pipes as they are heated and molded + +T_DIFFUSER = ((pipe.OD(ND_DIFFUSER_PIPE) - + pipe.ID_SDR(ND_DIFFUSER_PIPE, SDR_DIFFUSER)) + / (2 * RATIO_PVC_STRETCH)) + +W_DIFFUSER_INNER = 0.3175 * u.cm # opening width of diffusers + +# Calculating using a minor loss equation with K = 1 +V_SED_DIFFUSER_MAX = np.sqrt(2 * GRAVITY * HL_SED_INLET_MAX).to(u.mm/u.s) + +L_DIFFUSER = 15 * u.cm # vertical length of diffuser + +B_DIFFUSER = 5 * u.cm # center to center spacing beteen diffusers + +HEADLOSS_SED_DIFFUSER = 0.001 * u.m # Headloss through the diffusers to ensure uniform flow between sed tanks + +##Outlet to filter +#If the plant has two trains, the current design shows the exit channel +# continuing from one set of sed tanks into the filter inlet channel. +#The execution of this extended channel involves a few calculations. +HEADLOSS_SED_TO_FILTER_PIPE_MAX = 10 * u.cm +#============================================================================== +# if EN_DOUBLE_TRAIN == 1: +# K_SED_EXIT = 1 +# else: +# K_SED_EXIT = 0 +# +# +# if EN_DOUBLE_TRAIN == 1: +# HEIGHT_EXIT_FREE = 5 * u.cm +# else: +# HEIGHT_EXIT_FREE = 0 * u.cm +#============================================================================== + +##added 12/5/16 by mrf222 ensures weir does not overtop backwards if +# filter weir is too high +HEIGHT_SED_WEIR_FREE_BOARD = 5 * u.cm + + + +##Stacked rapid sand filter +####Construction and Design Inputs + +#Design guidelines say 11 mm/s. The success of lab-scale backwashing at +# 10 mm/s suggests that this is a reasonable and conservative value +VEL_FILTER_Bw_ = 11 * u.mm/u.s + +N_FILTER_LAYER = 6 + +VEL_FILTER_LAYER = 1.833 * u.mm/u.s ##VEL_FIBER_DIST_CENTER_/N_FIBER_LAYER + +##Minimum thickness of each filter layer (can be increased to accomodate +# larger pipe diameters in the bottom layer) +HEIGHT_FILTER_LAYER_MIN = 20 * u.cm + +##center to center distance for slotted pipes +DIST_CENTER_FILTER_MANIFOLD_BRANCH = 10 * u.cm + +##How far the branch extends into the trunk line +LENGTH_FILTER_MAN_BRANCH_EXT = 2 * u.cm + +##The time to drain the filter box of the water above the fluidized bed +TIME_FIBER_BACKWASH_INITIATION_BOD = 3 * u.min + +##Mickey suggested this value based on lab experience. This was moved to +# Expert Inputs 12/4/16 by mrf222 as a result of feedback from Monroe and +# Skyler. In the Moroceli plant, the Fi Entrance box was overflowing +# before filtration backwash. The HL of a dirty filter has therefore been +# increased from 40 to 60 cm. +HEADLOSS_FILTER_DIRTY = 60 * u.cm + +##This is the extra head we are going to provide on top of steady state +# backwash head loss to ensure that we can fluidize the bed to initiate +# backwash. +HEADLOSS_FIBER_BACKWASH_STEADY_FLOW = 20 * u.cm + +##Maximum acceptable head loss through the siphon at steady state; used to +# calculate a diameter +HEADLOSS_FILTER_SIPHON_MAX = 35 * u.cm + +##Diameter of sand drain pipe +NOM_DIAMETER_FILTER_SAND_OUTLET = 2 * u.inch + +##Height of the barrier between the exit box and distribution box. +HEIGHT_FILTER_DIST_BARRIER = 10 * u.cm + +##Length that the siphon pipe extends up into the plant drain channel. +#Being able to shorten the stub from which the siphon discharges into the +# main plant drain channel allows for some flexibility in the hydraulic design. +LENGTH_FILTER_SIPHON_CHANNEL_STUB_MIN = 20 * u.cm + +HEADLOSS_FILTER_ENTRANCE_PIPE_MAX = 10 * u.cm + +NOM_DIAMETER_FILTER_TRUNK_MAX = 6 * u.inch + +NOM_DIAMETER_FILTER_BACK_WASH_SIPHON_MAX = 8 * u.inch + +##Purge valves on the trunk lines are angled downwards so that sediment is +# cleared more effectively. This angle allows the tees to fit on top of one +# another at the filter wall. +ANGLE_FILTER_TRUNK_VALVES = 25 * u.deg + +##Purge valves on the trunk lines are angled downwards so that sediment is +# cleared more effectively. This angle allows the tees to fit on top of one +# another at the filter wall. +THICKNESS_FILTER_WEIR = 5 * u.cm + +SPACE_FILTER_BRANCH_TO_WALL = 5 * u.cm + +FILTER_GATE_VALUE_URL = "https://confluence.cornell.edu/download/attachments/173604905/Fi-Scaled-Gate-Valve-Threaded.dwg" +FILTER_BALL_VALVE_URL = "https://confluence.cornell.edu/download/attachments/173604905/FiMetalBallValve.dwg" + +HEIGHT_FILTER_WALL_TO_PLANT_FLOOR_MIN = 10 * u.cm + +HEADLOSS_FILTER_INLET_WEIR_MAX = 5 * u.cm + +##Dimensions get too small for construction below a certain flow rate +FLOW_FILTER_MIN = 8 * u.L/u.s + +LENGTH_FILTER_MAN_FEMCO_COUPLING = 6 * u.cm + +##Nominal diameter of the spacer tees in the four corners of the filter +# manifold assembly. +NOM_DIAMETER_FILTER_MAN_WING_SPACER = 2 * u.inch + +##Length of the vertical pipe segment following the valve on the filter +# sand drain. This stub can be capped to allow the sand in the valve to +# settle, so that the valve can be closed without damage from fluidized sand. +LENGTH_FILTER_SAND_OUTLET_PIPE = 20 * u.cm + + + + +#######Elevation Safety Margins + +##Minimum depth in the entrance box during backwash such that there is +# standing water over the inlet. +HEIGHT_FILTER_BACKWASH_NO_SUCK_AIR = 20 * u.cm + +##Minimum water depth over the orifices in the siphon manifold so that air +# is not entrained. +HEIGHT_FILTER_SIPHON_NO_SUCK_AIR = 10 * u.cm + +HEIGHT_FILTER_FLUIDIZED_BED_TO_SIPHON = 20 * u.cm + +HEIGHT_FILTER_FORWARD_NO_SUCK_AIR = 10 * u.cm + +HEIGHT_FILTER_WEIR_FREEFALL = 3 * u.cm + +HEIGHT_FILTER_AIR_REMOVAL_BLOCK_SUBMERGED = 5 * u.cm + +HEIGHT_FILTER_BYPASS_SAFETY = 10 * u.cm + +HEIGHT_DRAIN_OUTLET_SAFETY = 10 * u.cm + +HEIGHT_FILTER_OVERFLOW_WEIR_FREEFALL = 10 * u.cm + + + +#######Plant drain channel + +###Space beyond the entrance tank in the plant drain channel where the +# drop pipes from the CDC lever arm can come down and be connected with +# the chlorine and coagulant dosing points. +LENGTH_CHEM_LEVER_ARM_SPACE = 75 * u.cm + + +###Operator access + +##combine walkway assumptions! +WIDTH_MP_WALKWAY_MIN = 1 * u.m + +##Width of the walkway above the main plant drain channel +WIDTH_DC_WALKWAY = 1.2 * u.m + +##Width of the floor space between the flocculator and the rapid mix pipe +# floor cutout next to the entrance tank. +WIDTH_ET_WALKWAY = 1 * u.m + +##for high flow, double train situations +W_TRAIN_WALKWAY = 1.5 * u.m + +W_BASEMENT_STAIRS = 0.9 * u.m + +W_ENTRANCE_STAIRS = 1.2 * u.m + + +##Minor loss coefficients +##Individual K Values + +##90 deg elbow +K_MINOR_EL90 = 0.9 + +K_MINOR_EL45 = 0.45 +##The loss coefficient for the channel transition in a 90 degree turn +K_MINOR_90 = 0.4 + +K_MINOR_ANGLE_VALVE = 4.3 + +K_MINOR_GLOBE_VALVE = 10 + +K_MINOR_GATE_VALVE = 0.39 + +K_MINOR_CHECK_VALVE_CONV = 4 + +K_MINOR_CHECK_VALVE_BALL = 4.5 + +##headloss coefficient of jet +K_MINOR_EXP = 1 + +K_MINOR_TEE_FLOW_RUN = 0.6 + +K_MINOR_TEE_FLOW_BR = 1.8 + +K_MINOR_PIPE_ENTRANCE = 0.5 + +K_MINOR_PIPE_EXIT = 1 + +K_MINOR_RM_GATE_VIN = 25 diff --git a/aide_design/expert_inputs.py b/aide_design/expert_inputs.py index 3332d9c0..bc3bb2c5 100644 --- a/aide_design/expert_inputs.py +++ b/aide_design/expert_inputs.py @@ -1,14 +1,18 @@ -#-*- coding: utf-8 -*- -""" -Created on Mon Jun 26 15:54:16 2017 - -@author: Karan Newatia +"""This file contains constants which represent physical properties and +scientific principles which will be used in AguaClara plant design. -Last modified: Fri Jul 7 2017 -by: Sage Weber-Shirk """ from aide_design.units import unit_registry as u +import warnings +warnings.simplefilter('default') +warnings.warn( + "The module expert_inputs.py is deprecated. Please use constants" + + ".py which is imported as con instead. Some global variables" + + "have been moved to optional_inputs.py which is imported as opt", + DeprecationWarning + ) + #####tabulated constants #Density of water @@ -20,7 +24,7 @@ #The influence of viscosity on mixing in jet reactors RATIO_JET_ROUND = 0.5 -#This is an estimate for plane jets as created in the flocculator and +#This is an estimate for plane jets as created in the flocculator and # in the sed tank jet reverser. RATIO_JET_PLANE = 0.225 @@ -47,7 +51,7 @@ WIDTH_HUMAN_MIN = 0.5 * u.m -#The height of the walkway above the drain channel bottom so that +#The height of the walkway above the drain channel bottom so that # someone can walk through the drain channel. HEIGHT_HUMAN_ACCESS = 1.5 * u.m @@ -61,14 +65,14 @@ #Minimum channel width for constructability WIDTH_CHANNEL_MIN = 15 * u.cm -#RATIO_RECTANGULAR is defined as the optimum "height over width ratio" +#RATIO_RECTANGULAR is defined as the optimum "height over width ratio" # (1/2) for a rectangular open channel -RATIO_RECTANGULAR = 0.5 +RATIO_RECTANGULAR = 0.5 ##Equals 1 to draw boxes showing max water levels, 0 normally #EN_WATER=0 #ASK Monroe -##If EN_WATER is set to 1, this controls the filter operation mode for +##If EN_WATER is set to 1, this controls the filter operation mode for # which water/sand elevations are drawn. 0 for terminal, 1 for clean bed, # and 2 for backwash. #EN_WATER=2 #ASK Monroe @@ -77,7 +81,7 @@ THICKNESS_ACRYLIC = 1 * u.cm -#Due to a 24 in LFOM because that's the biggest pipe we have in our +#Due to a 24 in LFOM because that's the biggest pipe we have in our # database right now. if we need a bigger single train, we can do that # by adding that pipe size into the pipe database FLOW_TRAIN_MAX = 150.1 * u.L/u.s @@ -148,7 +152,7 @@ def en_lfom_pipe(flow_plant): ANGLE_ENT_TANK_SLOPE = 45 * u.deg #Extra space around the float (increase in effective diameter) to ensure -# that float has free travel +# that float has free travel SPACE_ENT_TANK_FLOAT = 5 * u.cm #Increased to get better mixing (10/10/2015 by Monroe) @@ -199,8 +203,8 @@ def en_lfom_pipe(flow_plant): M_COAG_SACK = 25 * u.kg #The coagulant stock is relatively stable and can last many days. Here we -# set the minimum time the coagulant stock will last when applying the -# maximum possible dose to size the stock tanks. In general the dose will +# set the minimum time the coagulant stock will last when applying the +# maximum possible dose to size the stock tanks. In general the dose will # be less than this and the stock will last much longer. TIME_COAG_STOCK_MIN_EST = 1 * u.day @@ -225,11 +229,11 @@ def en_lfom_pipe(flow_plant): # rate through the float valve orifice in the CHT. #It is treated as constant here to ensure a practical elevation difference # is left between the stock tanks and the CHT even when a float valve is -# selected which requires very little hydraulic head to deliver the +# selected which requires very little hydraulic head to deliver the # required maximum chemical flow rate. HEIGHT_COAG_TANK_ABOVE_HEAD_TANK = 30 * u.cm -#This is the distance from the bottom of the stock tanks to the outlets +#This is the distance from the bottom of the stock tanks to the outlets # to allow space for solids to settle. DIST_CENTER_STOCK_OUTLET = 10 * u.cm @@ -240,15 +244,15 @@ def en_lfom_pipe(flow_plant): # the constant head tank and the top of the entrance tank wall. #The constant head tank water level is the same as the elevation of the # outlet of the dosing tube when the lever arm is horizontal (zero flow). -#Therefore this height depends only on the hardware used to make the -# slider/drop tube assembly and to mount the lever arm to the entrance +#Therefore this height depends only on the hardware used to make the +# slider/drop tube assembly and to mount the lever arm to the entrance # tank wall. -#Note that this will vary depending on hardware used, and is only -# defined here to calculate the elevation of the stock tanks, which can +#Note that this will vary depending on hardware used, and is only +# defined here to calculate the elevation of the stock tanks, which can # be approximate. HEIGHT_DOSER_ASSEMBLY = 6.77 * u.cm -#Maximum error allowed between a linear flow vs tube head loss +#Maximum error allowed between a linear flow vs tube head loss # relationship and the actual performance (which is affected by non-linear # minor losses), assuming calibration at the maximum flow rate. RATIO_LINEAR_CDC_ERROR = 0.1 @@ -282,11 +286,11 @@ def en_lfom_pipe(flow_plant): VOL_CHEM_TANK_AVAIL = [5 * u.gal, 35 * u.gal, 55 * u.gal, 450 * u.L, 750 * u.L, 1100 * u.L, 2500 * u.L] -DIAM__CHEM_TANK_AVAIL = [11.875 * u.inch, 20.75 * u.inch, 22.5 * u.inch, +DIAM__CHEM_TANK_AVAIL = [11.875 * u.inch, 20.75 * u.inch, 22.5 * u.inch, 0.85 * u.m, 1.10 * u.m, 1.10 * u.m, 1.55 * u.m] -HEIGHT_CHEM_TANK_AVAIL = [17.75 * u.inch, 31.75 * u.inch, 33.5 * u.inch, - 0.99 * u.inch, 1.02 * u.inch, 1.39 * u.inch, +HEIGHT_CHEM_TANK_AVAIL = [17.75 * u.inch, 31.75 * u.inch, 33.5 * u.inch, + 0.99 * u.inch, 1.02 * u.inch, 1.39 * u.inch, 1.65 * u.inch] ####Chemical dose controller dimensions (based on inserted drawings) @@ -356,7 +360,7 @@ def en_lfom_pipe(flow_plant): DIAM_CABLE = 0.1 * u.inch -#Edited DLABOrigintoLAOriginZ to accommodate dimensions from McMaster +#Edited DLABOrigintoLAOriginZ to accommodate dimensions from McMaster # vs Inserted Drawing DIAM_LAB_ORIGIN_TO_LA_ORIGIN_Z = 0.0245 * u.m @@ -367,26 +371,26 @@ def en_lfom_pipe(flow_plant): #Distance from the lever arm origin to the drop tube in the z direction. LENGTH_LA_ORIGIN_TO_DT_Z = 0.0429 * u.m -#Distance from the lever arm origin to the center of the drop tube in the +#Distance from the lever arm origin to the center of the drop tube in the #x direction. LENGTH_LA_ORIGIN_TO_DT_CENTER_X = 0.0290 * u.m #Measured from CDC research team's apparatus. THICKNESS_CDC_REDUCER = 9.5 * u.mm -#Distance from the lever arm origin to the center of the reducer in the +#Distance from the lever arm origin to the center of the reducer in the # x direction. LENGTH_LA_ORIGIN_TO_REDUCER_X = 0.0290 * u.m -#Distance from the lever arm origin to the outside center of the top part +#Distance from the lever arm origin to the outside center of the top part # of the reducer in the y direction. LENGTH_LA_ORIGIN_TO_REDUCER_Y = 0.7135 * u.cm -#Distance from the lever arm origin to the center of the reducer in the +#Distance from the lever arm origin to the center of the reducer in the # x direction. LENGTH_LA_ORIGIN_TO_REDUCER_CENTER_X = 0.0290 * u.m -#Distance from the lever arm origin to the center of the reducer in the +#Distance from the lever arm origin to the center of the reducer in the # y direction. LENGTH_LA_ORIGIN_TO_REDUCER_CENTER_Y = 0.7919 * u.m @@ -414,7 +418,7 @@ def en_lfom_pipe(flow_plant): #The length of the drop tube needs to be calculated. The drop tube must be # as long as the supercritical flow. -#Thus the drop tube must extend down to the elevation of the sed tank +#Thus the drop tube must extend down to the elevation of the sed tank # effluent weir. This constant should be removed! #Outer diameter of fitting- measured from CDC research team's fittin @@ -454,9 +458,9 @@ def en_lfom_pipe(flow_plant): ####Flocculator ##The minor loss coefficient is 2. According to measurements at Agalteca -# and according to +# and according to # https://confluence.cornell.edu/display/AGUACLARA/PAHO+Water+Treatment+Publications -# (page 100 in chapter on flocculation) +# (page 100 in chapter on flocculation) HEIGHT_FLOC_OPTION = 0 ##Increased both to provide a safety margin on flocculator head loss and @@ -469,9 +473,14 @@ def en_lfom_pipe(flow_plant): ###Target flocculator collision potential basis of design COLL_POT_FLOC_BOD = 75 * u.m**(2/3) -##Minimum J/S ratio for flocculator geometry that will provide optimal -# efficiency. -RATIO_J_S_OPT_MIN = 3 +##Minimum width of flocculator channel required for constructability based +# on the width of the human hip +FLOC_WIDTH_MIN_CONST = 45 * u.cm + +##Minimum and maximum distance between expansions to baffle spacing ratio for +#flocculator geometry that will provide optimal efficiency. +RATIO_HS_MIN = 3 +RATIO_HS_MAX = 6 ##Ratio of the width of the gap between the baffle and the wall and the # spacing between the baffles. @@ -509,7 +518,7 @@ def en_lfom_pipe(flow_plant): ANGLE_SED_SLOPE = 50 * u.deg -##This slope needs to be verified for functionality in the field. +##This slope needs to be verified for functionality in the field. # A steeper slope may be required in the floc hopper. ANGLE_SED_HOPPER_SLOPE = 45 * u.deg @@ -523,14 +532,14 @@ def en_lfom_pipe(flow_plant): # segments can be used for the inlet and outlet manifoldS LENGTH_SED_UP_FLOW_MAX = 5.8 * u.m - + ##Inlet channel HEADLOSS_SED_WEIR_MAX = 5 * u.cm ##Height of the inlet channel overflow weir above the normal water level # in the inlet channel so that the far side of the overflow weir does not # fill with water under normal operating conditions. This means the water -# level in the inlet channel will increase when the inlet overflow weir +# level in the inlet channel will increase when the inlet overflow weir # is in use. HEIGHT_SED_INLET_WEIR_FREE_BOARD = 2 * u.cm @@ -544,7 +553,7 @@ def en_lfom_pipe(flow_plant): ##Center to center spacing of orifices in the launder DIST_CENTER_SED_LAUNDER_EST = 10 * u.cm -##The additional length needed in the launder cap pipe that is to be +##The additional length needed in the launder cap pipe that is to be # inserted into the launder coupling LENGTH_SED_LAUNDER_CAP_EXCESS = 3 * u.cm @@ -596,7 +605,7 @@ def en_lfom_pipe(flow_plant): NOM_DIAMETER_SED_MANIFOLD_MAX = 8 * u.inch -##This is the minimum distance between the inlet manifold and the slope +##This is the minimum distance between the inlet manifold and the slope # of the sed tank. SPACE_SED_INLET_MAN_SLOPE = 10 * u.cm @@ -604,7 +613,7 @@ def en_lfom_pipe(flow_plant): # free portion of the inlet manifold is attached with a flexible coupling. LENGTH_SED_MAN_CONNECTION_STUB = 4 * u.cm -##Space between the end of the manifold pipe and the edge of the first +##Space between the end of the manifold pipe and the edge of the first # diffuser's hole, or the first manifold orifice. LENGTH_SED_MANIFOLD_FIRST_DIFFUSER_GAP = 3 * u.cm @@ -622,14 +631,14 @@ def en_lfom_pipe(flow_plant): LENGTH_SED_WALL_TO_DIFFUSER_GAP_MIN = 3 * u.cm -##Diameter of the holes drilled in the manifold so that the molded 1" -# diffuser pipes can fit tightly in place (normal OD of a 1" pipe is +##Diameter of the holes drilled in the manifold so that the molded 1" +# diffuser pipes can fit tightly in place (normal OD of a 1" pipe is # close to 1-5/16") DIAM_SED_MANIFOLD_PORT = 1.25 * u.inch ##Outlet to filter -#If the plant has two trains, the current design shows the exit channel -# continuing from one set of sed tanks into the filter inlet channel. +#If the plant has two trains, the current design shows the exit channel +# continuing from one set of sed tanks into the filter inlet channel. #The execution of this extended channel involves a few calculations. HEADLOSS_SED_TO_FILTER_PIPE_MAX = 10 * u.cm #============================================================================== @@ -637,15 +646,15 @@ def en_lfom_pipe(flow_plant): # K_SED_EXIT = 1 # else: # K_SED_EXIT = 0 -# -# -# if EN_DOUBLE_TRAIN == 1: -# HEIGTH_EXIT_FREE = 5 * u.cm +# +# +# if EN_DOUBLE_TRAIN == 1: +# HEIGHT_EXIT_FREE = 5 * u.cm # else: -# HEIGTH_EXIT_FREE = 0 * u.cm +# HEIGHT_EXIT_FREE = 0 * u.cm #============================================================================== -##added 12/5/16 by mrf222 ensures weir does not overtop backwards if +##added 12/5/16 by mrf222 ensures weir does not overtop backwards if # filter weir is too high HEIGHT_SED_WEIR_FREE_BOARD = 5 * u.cm @@ -654,7 +663,7 @@ def en_lfom_pipe(flow_plant): ##Stacked rapid sand filter ####Construction and Design Inputs -#Design guidelines say 11 mm/s. The success of lab-scale backwashing at +#Design guidelines say 11 mm/s. The success of lab-scale backwashing at # 10 mm/s suggests that this is a reasonable and conservative value VEL_FILTER_Bw_ = 11 * u.mm/u.s @@ -697,7 +706,7 @@ def en_lfom_pipe(flow_plant): ##Height of the barrier between the exit box and distribution box. HEIGHT_FILTER_DIST_BARRIER = 10 * u.cm -##Length that the siphon pipe extends up into the plant drain channel. +##Length that the siphon pipe extends up into the plant drain channel. #Being able to shorten the stub from which the siphon discharges into the # main plant drain channel allows for some flexibility in the hydraulic design. LENGTH_FILTER_SIPHON_CHANNEL_STUB_MIN = 20 * u.cm @@ -723,7 +732,7 @@ def en_lfom_pipe(flow_plant): FILTER_GATE_VALUE_URL = "https://confluence.cornell.edu/download/attachments/173604905/Fi-Scaled-Gate-Valve-Threaded.dwg" FILTER_BALL_VALVE_URL = "https://confluence.cornell.edu/download/attachments/173604905/FiMetalBallValve.dwg" -HEIGTH_FILTER_WALL_TO_PLANT_FLOOR_MIN = 10 * u.cm +HEIGHT_FILTER_WALL_TO_PLANT_FLOOR_MIN = 10 * u.cm HEADLOSS_FILTER_INLET_WEIR_MAX = 5 * u.cm @@ -732,7 +741,7 @@ def en_lfom_pipe(flow_plant): LENGTH_FILTER_MAN_FEMCO_COUPLING = 6 * u.cm -##Nominal diameter of the spacer tees in the four corners of the filter +##Nominal diameter of the spacer tees in the four corners of the filter # manifold assembly. NOM_DIAMETER_FILTER_MAN_WING_SPACER = 2 * u.inch @@ -748,25 +757,25 @@ def en_lfom_pipe(flow_plant): ##Minimum depth in the entrance box during backwash such that there is # standing water over the inlet. -HEIGTH_FILTER_BACKWASH_NO_SUCK_AIR = 20 * u.cm +HEIGHT_FILTER_BACKWASH_NO_SUCK_AIR = 20 * u.cm ##Minimum water depth over the orifices in the siphon manifold so that air # is not entrained. -HEIGTH_FILTER_SIPHON_NO_SUCK_AIR = 10 * u.cm +HEIGHT_FILTER_SIPHON_NO_SUCK_AIR = 10 * u.cm -HEIGTH_FILTER_FLUIDIZED_BED_TO_SIPHON = 20 * u.cm +HEIGHT_FILTER_FLUIDIZED_BED_TO_SIPHON = 20 * u.cm -HEIGTH_FILTER_FORWARD_NO_SUCK_AIR = 10 * u.cm +HEIGHT_FILTER_FORWARD_NO_SUCK_AIR = 10 * u.cm -HEIGTH_FILTER_WEIR_FREEFALL = 3 * u.cm +HEIGHT_FILTER_WEIR_FREEFALL = 3 * u.cm -HEIGTH_FILTER_AIR_REMOVAL_BLOCK_SUBMERGED = 5 * u.cm +HEIGHT_FILTER_AIR_REMOVAL_BLOCK_SUBMERGED = 5 * u.cm -HEIGTH_FILTER_BYPASS_SAFETY = 10 * u.cm +HEIGHT_FILTER_BYPASS_SAFETY = 10 * u.cm -HEIGTH_DRAIN_OUTLET_SAFETY = 10 * u.cm +HEIGHT_DRAIN_OUTLET_SAFETY = 10 * u.cm -HEIGTH_FILTER_OVERFLOW_WEIR_FREEFALL = 10 * u.cm +HEIGHT_FILTER_OVERFLOW_WEIR_FREEFALL = 10 * u.cm @@ -830,4 +839,3 @@ def en_lfom_pipe(flow_plant): K_MINOR_PIPE_EXIT = 1 K_MINOR_RM_GATE_VIN = 25 - diff --git a/aide_design/floc_model.py b/aide_design/floc_model.py index b9218e85..30e454c2 100644 --- a/aide_design/floc_model.py +++ b/aide_design/floc_model.py @@ -1,11 +1,7 @@ # -*- coding: utf-8 -*- -""" -Created on Mon Jun 26 16:50:46 2017 - -@author: Sage Weber-Shirk +"""This file contains functions which can be used to model the behavior of +flocs based on the chemical interactions of clay, coagulant, and humic acid. -Last revised: Fri Aug 11 2017 -By: Sage Weber-Shirk """ ######################### Imports ######################### @@ -25,11 +21,10 @@ def __init__(self, name, diameter, density, molecWeight): self.Diameter = diameter self.Density = density self.MolecWeight = molecWeight - class Chemical(Material): - def __init__(self, name, diameter, density, molecWeight, Precipitate, + def __init__(self, name, diameter, density, molecWeight, Precipitate, AluminumMPM=None): Material.__init__(self, name, diameter, density, molecWeight) self.AluminumMPM = AluminumMPM @@ -54,25 +49,26 @@ def define_Precip(self, diameter, density, molecweight, alumMPM): # 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) +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') ################### Necessary Constants ################### -# Fractal diameter, based on data from Adachi. -DIAM_FRACTAL = 2.3 +# Fractal dimension, based on data from published in Environmental Engineering +# Science, "Fractal Models for Floc Density, Sedimentation Velocity, and Floc +# Volume Fraction for High Peclet Number Reactors" by Monroe Weber-Shirk and +# Leonard Lion (2015). +DIM_FRACTAL = 2.3 # Ratio of clay platelet height to diameter. RATIO_HEIGHT_DIAM = 0.1 -# Ration between inner viscous length scale and Kolmogorov length scale. +# Ratio between inner viscous length scale and Kolmogorov length scale. RATIO_KOLMOGOROV = 50 # Shape factor for drag on flocs used in terminal velocity equation. PHI_FLOC = 45/24 @@ -143,24 +139,27 @@ def sep_dist_aluminum(ConcAluminum): @u.wraps(1/u.m**3, [u.kg/u.m**3, u.m], False) def num_clay(ConcClay, material): + """Return the number of clay particles in suspension.""" return ConcClay / ((material.Density * np.pi * material.Diameter**3) / 6) @u.wraps(u.m, [u.kg/u.m**3, u.m], False) def sep_dist_clay(ConcClay, material): """Return the separation distance between clay particles.""" - return ((material.Density / ConcClay) * ((np.pi * material.Diameter**3) / 6))**(1/3) + return ((material.Density/ConcClay)*((np.pi + * material.Diameter ** 3)/6))**(1/3) @u.wraps(1/u.m**3, [u.kg/u.m**3, None], False) def num_nanoclusters(ConcAluminum, coag): + """Return the number of Aluminum nanoclusters.""" return (ConcAluminum / (dens_alum_nanocluster(coag).magnitude - * np.pi * coag.Diameter**3 - )) + * np.pi * coag.Diameter**3)) @u.wraps(None, [u.kg/u.m**3, u.kg/u.m**3, None, None], False) def frac_vol_floc_initial(ConcAluminum, ConcClay, coag, material): + """Return the fraction of flocs initially present.""" return ((conc_precipitate(ConcAluminum, coag).magnitude/coag.PrecipDensity) + (ConcClay / material.Density)) @@ -176,42 +175,42 @@ def invp(pC, Cprime): #################### Fractal functions #################### @u.wraps(u.m, [u.dimensionless, u.m, u.dimensionless], False) -def diam_fractal(DiamFractal, DiamInitial, NumCol): +def diam_fractal(DIM_FRACTAL, DiamInitial, NumCol): """Return the diameter of a floc given NumCol doubling collisions.""" - return DiamInitial * 2**(NumCol / DiamFractal) + return DiamInitial * 2**(NumCol / DIM_FRACTAL) @u.wraps(None, [u.dimensionless, None, u.m], False) -def num_coll_reqd(DiamFractal, material, DiamTarget): +def num_coll_reqd(DIM_FRACTAL, material, DiamTarget): """Return the number of doubling collisions required. Calculates the number of doubling collisions required to produce a floc of diameter DiamTarget. """ - return DiamFractal * np.log2(DiamTarget/material.Diameter) + return DIM_FRACTAL * np.log2(DiamTarget/material.Diameter) @u.wraps(u.m, [u.kg/u.m**3, u.kg/u.m**3, None, None, u.dimensionless, u.m], False) def sep_dist_floc(ConcAluminum, ConcClay, coag, material, - DiamFractal, DiamTarget): + DIM_FRACTAL, DiamTarget): """Return separation distance as a function of floc size.""" return (material.Diameter * (np.pi/(6 * frac_vol_floc_initial(ConcAluminum, ConcClay, coag, material) ))**(1/3) - * (DiamTarget / material.Diameter)**(DiamFractal / 3) + * (DiamTarget / material.Diameter)**(DIM_FRACTAL / 3) ) @u.wraps(u.m, [u.kg/u.m**3, u.kg/u.m**3, None, u.dimensionless, None, u.m], False) -def frac_vol_floc(ConcAluminum, ConcClay, coag, DiamFractal, +def frac_vol_floc(ConcAluminum, ConcClay, coag, DIM_FRACTAL, material, DiamTarget): """Return the floc volume fraction.""" return (frac_vol_floc_initial(ConcAluminum, ConcClay, coag, material) - * (DiamTarget / material.Diameter)**(3-DiamFractal) + * (DiamTarget / material.Diameter)**(3-DIM_FRACTAL) ) @@ -266,26 +265,38 @@ def gamma_coag(ConcClay, ConcAluminum, coag, material, nanoglobs. The poisson distribution results in the coverage only gradually approaching full coverage as coagulant dose increases. """ - return (1 - np.exp( - ((- frac_vol_floc_initial(ConcAluminum, 0, coag, material) - * material.Diameter - ) + return (1 - np.exp(( + (-frac_vol_floc_initial(ConcAluminum, 0, coag, material) + * material.Diameter) / (frac_vol_floc_initial(0, ConcClay, coag, material) - * coag.Diameter - ) - ) + * coag.Diameter)) * (1 / np.pi) * (ratio_area_clay_total(ConcClay, material, DiamTube, RatioHeightDiameter) - / ratio_clay_sphere(RatioHeightDiameter) - ) - ) - ) + / ratio_clay_sphere(RatioHeightDiameter)) + )) @u.wraps(None, [u.kg/u.m**3, u.kg/u.m**3, None, None], False) @ut.list_handler def gamma_humic_acid_to_coag(ConcAl, ConcNatOrgMat, NatOrgMat, coag): + """Return the fraction of the coagulant that is coated with humic acid. + + Parameters + ---------- + var1 : float + Concentration of alumninum in solution + var2 : float + Concentration of natural organic matter in solution + var3 : ? + var4 : ? + + Returns + ------- + float + fraction of the coagulant that is coated with humic acid + + """ return min(((ConcNatOrgMat / conc_precipitate(ConcAl, coag).magnitude) * (coag.Density / NatOrgMat.Density) * (coag.Diameter / (4 * NatOrgMat.Diameter)) @@ -295,8 +306,33 @@ def gamma_humic_acid_to_coag(ConcAl, ConcNatOrgMat, NatOrgMat, coag): @u.wraps(None, [u.m, u.kg/u.m**3, u.kg/u.m**3, u.kg/u.m**3, None, None, None, u.dimensionless], False) -def _pacl_term(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, NatOrgMat, +def pacl_term(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, NatOrgMat, coag, material, RatioHeightDiameter): + """Return the fraction of the surface area that is covered with coagulant + that is not covered with humic acid. + + Parameters + ---------- + var1 : float + Diameter of the dosing tube + var2 : float + Concentration of clay in solution + var3 : float + Concentration of alumninum in solution + var4 : float + Concentration of natural organic matter in solution + var5 : ? + var6 : ? + var7 : float + Ratio between inner viscous length scale and Kolmogorov length scale + + Returns + ------- + float + fraction of the surface area that is covered with coagulant that is not + covered with humic acid + + """ return (gamma_coag(ConcClay, ConcAl, coag, material, DiamTube, RatioHeightDiameter) * (1 - gamma_humic_acid_to_coag(ConcAl, ConcNatOrgMat, @@ -304,11 +340,12 @@ def _pacl_term(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, NatOrgMat, ) -@u.wraps(None, [u.m, u.kg/u.m**3, u.kg/u.m**3, u.kg/u.m**3, +@u.wraps(None, [u.m, u.kg/u.m**3, u.kg/u.m**3, u.kg/u.m**3, None, None, None, u.dimensionless], False) -def alpha_pacl_clay(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, +def alpha_pacl_clay(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, NatOrgMat, coag, material, RatioHeightDiameter): - PAClTerm = _pacl_term(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, + """""" + PAClTerm = pacl_term(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, NatOrgMat, coag, material, RatioHeightDiameter) return 2 * (PAClTerm * (1 - gamma_coag(ConcClay, ConcAl, coag, material, DiamTube, RatioHeightDiameter))) @@ -316,18 +353,20 @@ def alpha_pacl_clay(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, @u.wraps(None, [u.m, u.kg/u.m**3, u.kg/u.m**3, u.kg/u.m**3, None, None, None, u.dimensionless], False) -def alpha_pacl_pacl(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, +def alpha_pacl_pacl(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, NatOrgMat, coag, material, RatioHeightDiameter): - PAClTerm = _pacl_term(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, + """""" + PAClTerm = pacl_term(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, NatOrgMat, coag, material, RatioHeightDiameter) return PAClTerm ** 2 @u.wraps(None, [u.m, u.kg/u.m**3, u.kg/u.m**3, u.kg/u.m**3, None, None, None, u.dimensionless], False) -def alpha_pacl_nat_org_mat(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, +def alpha_pacl_nat_org_mat(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, NatOrgMat, coag, material, RatioHeightDiameter): - PAClTerm = _pacl_term(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, + """""" + PAClTerm = pacl_term(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, NatOrgMat, coag, material, RatioHeightDiameter) return (2 * PAClTerm * gamma_coag(ConcClay, ConcAl, coag, material, DiamTube, @@ -335,16 +374,17 @@ def alpha_pacl_nat_org_mat(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, * gamma_humic_acid_to_coag(ConcAl, ConcNatOrgMat, NatOrgMat, coag)) -@u.wraps(None, [u.m, u.kg/u.m**3, u.kg/u.m**3, u.kg/u.m**3, +@u.wraps(None, [u.m, u.kg/u.m**3, u.kg/u.m**3, u.kg/u.m**3, None, None, None, u.dimensionless], False) -def alpha(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, +def alpha(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, NatOrgMat, coag, material, RatioHeightDiameter): - return (alpha_pacl_nat_org_mat(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, - NatOrgMat, coag, material, + """""" + return (alpha_pacl_nat_org_mat(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, + NatOrgMat, coag, material, RatioHeightDiameter) - + alpha_pacl_pacl(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, + + alpha_pacl_pacl(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, NatOrgMat, coag, material, RatioHeightDiameter) - + alpha_pacl_clay(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, + + alpha_pacl_clay(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, NatOrgMat, coag, material, RatioHeightDiameter) ) @@ -355,16 +395,15 @@ def alpha(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, def pc_viscous(EnergyDis, Temp, Time, DiamTube, ConcClay, ConcAl, ConcNatOrgMat, NatOrgMat, coag, material, FittingParam, RatioHeightDiameter): + """""" return ((3/2) * np.log10((2/3) * np.pi * FittingParam * Time - * np.sqrt(EnergyDis - / (pc.viscosity_kinematic(Temp).magnitude) + * np.sqrt(EnergyDis / (pc.viscosity_kinematic(Temp).magnitude) ) - * alpha(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, + * alpha(DiamTube, ConcClay, ConcAl, ConcNatOrgMat, NatOrgMat, coag, material, RatioHeightDiameter) * (np.pi/6)**(2/3) - * (material.Diameter - / sep_dist_clay(ConcClay, material).magnitude + * (material.Diameter / sep_dist_clay(ConcClay, material).magnitude ) ** 2 + 1 ) @@ -373,20 +412,20 @@ def pc_viscous(EnergyDis, Temp, Time, DiamTube, @u.wraps(u.kg/u.m**3, [u.kg/u.m**3, u.kg/u.m**3, u.dimensionless, u.m, None, None, u.degK], False) -def dens_floc(ConcAl, ConcClay, DiamFractal, DiamTarget, coag, material, Temp): +def dens_floc(ConcAl, ConcClay, DIM_FRACTAL, DiamTarget, coag, material, Temp): """Calculate floc density as a function of size.""" WaterDensity = pc.density_water(Temp).magnitude return ((dens_floc_init(ConcAl, ConcClay, coag, material).magnitude - WaterDensity ) - * (material.Diameter / DiamTarget)**(3 - DiamFractal) + * (material.Diameter / DiamTarget)**(3 - DIM_FRACTAL) + WaterDensity ) @u.wraps(u.m/u.s, [u.kg/u.m**3, u.kg/u.m**3, None, None, u.dimensionless, u.m, u.degK], False) -def vel_term_floc(ConcAl, ConcClay, coag, material, DiamFractal, +def vel_term_floc(ConcAl, ConcClay, coag, material, DIM_FRACTAL, DiamTarget, Temp): """Calculate floc terminal velocity.""" WaterDensity = pc.density_water(Temp).magnitude @@ -398,14 +437,14 @@ def vel_term_floc(ConcAl, ConcClay, coag, material, DiamFractal, ) / WaterDensity ) - * (DiamTarget / material.Diameter) ** (DiamFractal - 1) + * (DiamTarget / material.Diameter) ** (DIM_FRACTAL - 1) ) @u.wraps(u.m, [u.kg/u.m**3, u.kg/u.m**3, None, None, u.dimensionless, u.m/u.s, u.degK], False) def diam_floc_vel_term(ConcAl, ConcClay, coag, material, - DiamFractal, VelTerm, Temp): + DIM_FRACTAL, VelTerm, Temp): """Calculate floc diamter as a function of terminal velocity.""" WaterDensity = pc.density_water(Temp).magnitude return (material.Diameter * (((18 * VelTerm * PHI_FLOC @@ -414,20 +453,20 @@ def diam_floc_vel_term(ConcAl, ConcClay, coag, material, / (pc.gravity.magnitude * material.Diameter**2) ) * (WaterDensity - / (dens_floc_init(ConcAl, ConcClay, coag, + / (dens_floc_init(ConcAl, ConcClay, coag, material).magnitude - WaterDensity ) ) - ) ** (1 / (DiamFractal - 1)) + ) ** (1 / (DIM_FRACTAL - 1)) ) @u.wraps(u.s, [u.W/u.kg, u.degK, u.kg/u.m**3, u.kg/u.m**3, None, None, u.m, u.m, u.dimensionless, u.dimensionless], False) -def time_col_laminar(EnergyDis, Temp, ConcAl, ConcClay, coag, material, - DiamTarget, DiamTube, DiamFractal, RatioHeightDiameter): +def time_col_laminar(EnergyDis, Temp, ConcAl, ConcClay, coag, material, + DiamTarget, DiamTube, DIM_FRACTAL, RatioHeightDiameter): """Calculate single collision time for laminar flow mediated collisions. Calculated as a function of floc size. @@ -435,7 +474,7 @@ def time_col_laminar(EnergyDis, Temp, ConcAl, ConcClay, coag, material, return (((1/6) * ((6/np.pi)**(1/3)) * frac_vol_floc_initial(ConcAl, ConcClay, coag, material)**(-2/3) * (pc.viscosity_kinematic(Temp).magnitude / EnergyDis)**(1/2) - * (DiamTarget / material.Diameter)**(2*DiamFractal/3 - 2) + * (DiamTarget / material.Diameter)**(2*DIM_FRACTAL/3 - 2) ) # End of the numerator / (gamma_coag(ConcClay, ConcAl, coag, material, DiamTube, RatioHeightDiameter) @@ -446,14 +485,14 @@ def time_col_laminar(EnergyDis, Temp, ConcAl, ConcClay, coag, material, @u.wraps(u.s, [u.W/u.kg, u.kg/u.m**3, u.kg/u.m**3, None, None, u.m, u.dimensionless], False) def time_col_turbulent(EnergyDis, ConcAl, ConcClay, coag, material, - DiamTarget, DiamFractal): + DiamTarget, DIM_FRACTAL): """Calculate single collision time for turbulent flow mediated collisions. Calculated as a function of floc size. """ return((1/6) * (6/np.pi)**(1/9) * EnergyDis**(-1/3) * DiamTarget**(2/3) * frac_vol_floc_initial(ConcAl, ConcClay, coag, material)**(-8/9) - * (DiamTarget / material.Diameter)**((8*(DiamFractal-3)) / 9) + * (DiamTarget / material.Diameter)**((8*(DIM_FRACTAL-3)) / 9) ) @@ -468,10 +507,10 @@ def lambda_vel(EnergyDis, Temp): return RATIO_KOLMOGOROV * eta_kolmogorov(EnergyDis, Temp).magnitude -@u.wraps(u.m, [u.W/u.kg, u.degK, u.kg/u.m**3, u.kg/u.m**3, None, None, +@u.wraps(u.m, [u.W/u.kg, u.degK, u.kg/u.m**3, u.kg/u.m**3, None, None, u.dimensionless], False) def diam_kolmogorov(EnergyDis, Temp, ConcAl, ConcClay, coag, material, - DiamFractal): + DIM_FRACTAL): """Return the size of the floc with separation distances equal to the Kolmogorov length and the inner viscous length scale. """ @@ -480,19 +519,19 @@ def diam_kolmogorov(EnergyDis, Temp, ConcAl, ConcClay, coag, material, * ((6 * frac_vol_floc_initial(ConcAl, ConcClay, coag, material)) / np.pi )**(1/3) - )**(3 / DiamFractal) + )**(3 / DIM_FRACTAL) ) -@u.wraps(u.m, [u.W/u.kg, u.degK, u.kg/u.m**3, u.kg/u.m**3, None, None, +@u.wraps(u.m, [u.W/u.kg, u.degK, u.kg/u.m**3, u.kg/u.m**3, None, None, u.dimensionless], False) -def diam_vel(EnergyDis, Temp, ConcAl, ConcClay, coag, material, DiamFractal): +def diam_vel(EnergyDis, Temp, ConcAl, ConcClay, coag, material, DIM_FRACTAL): return (material.Diameter * ((lambda_vel(EnergyDis, Temp).magnitude / material.Diameter) * ((6 * frac_vol_floc_initial(ConcAl, ConcClay, coag, material)) / np.pi )**(1/3) - )**(3/DiamFractal) + )**(3/DIM_FRACTAL) ) @@ -557,9 +596,9 @@ def g_coil(FlowPlant, IDTube, RadiusCoil, Temp): Karen's thesis likely has this equation and the reference. """ return (g_straight(FlowPlant, IDTube).magnitude - * (1 - + 0.033 * np.log10(dean_number(FlowPlant,IDTube,RadiusCoil,Temp) - ) ** 4 + * (1 + 0.033 * + np.log10(dean_number(FlowPlant, IDTube, RadiusCoil, Temp) + ) ** 4 ) ** (1/2) ) diff --git a/aide_design/k_value_of_reductions_utility.py b/aide_design/k_value_of_reductions_utility.py index 37b86ee2..c290fbe3 100644 --- a/aide_design/k_value_of_reductions_utility.py +++ b/aide_design/k_value_of_reductions_utility.py @@ -1,22 +1,23 @@ +""" Minor Loss Coefficients of Reductions Module. + +This module includes all minor loss coefficient calculations for common complex +geometries. For reductions and expansions, the following resource is used: +https://neutrium.net/fluid_flow/pressure-loss-from-fittings-expansion-and-reduction-in-pipe-size/ + +""" + import aide_design.pipedatabase as pipe from aide_design.units import unit_registry as u from aide_design import physchem as pc -import aide_design.expert_inputs as exp +import aide_design.constants as con import aide_design.materials_database as mats import numpy as np import aide_design.utility as ut -""" Minor Loss Coefficients of Reductions Module. - -This module includes all minor loss coefficient calculations for common complex geometries. -For reductions and expansions, the following resource is used: -https://neutrium.net/fluid_flow/pressure-loss-from-fittings-expansion-and-reduction-in-pipe-size/ - -""" @u.wraps(u.dimensionless, [u.m, u.m, u.L/u.s]) @ut.list_handler -def k_value_expansion(id_entrance:float, id_exit:float, flow, NU=exp.NU_WATER, ROUGHNESS=mats.PIPE_ROUGH_PVC, theta=180, ROUNDED=False) -> float: +def k_value_expansion(id_entrance:float, id_exit:float, flow, NU=con.NU_WATER, ROUGHNESS=mats.PIPE_ROUGH_PVC, theta=180, ROUNDED=False) -> float: """This function calculates the minor loss coefficient of a square, tapered or rounded expansion in a pipe using the equation defined here, where Re is the reynolds number on the inlet side, D_in and D_out are the inner diameter of the entrance pipe and exit pipe, respectively, and f_in is the Darcy friction factor of the inlet pipe: @@ -71,7 +72,7 @@ def k_value_expansion(id_entrance:float, id_exit:float, flow, NU=exp.NU_WATER, R @u.wraps(u.dimensionless, [u.m, u.m, u.L/u.s]) @ut.list_handler -def k_value_reduction(id_entrance:float, id_exit:float, flow, NU=exp.NU_WATER, ROUGHNESS=mats.PIPE_ROUGH_PVC, theta=180, ROUNDED=False) -> float: +def k_value_reduction(id_entrance:float, id_exit:float, flow, NU=con.NU_WATER, ROUGHNESS=mats.PIPE_ROUGH_PVC, theta=180, ROUNDED=False) -> float: """This function calculates the minor loss coefficient of a square, tapered or round reduction in a pipe using the equation defined here, where Re is the reynolds number on the inlet side, D_in and D_out are the inner diameter of the entrance pipe and exit pipe, respectively, and f_in is the Darcy friction factor of the inlet pipe: @@ -130,7 +131,7 @@ def k_value_reduction(id_entrance:float, id_exit:float, flow, NU=exp.NU_WATER, R @u.wraps(u.dimensionless, [u.m, u.m, u.m, u.m**3/u.s]) @ut.list_handler -def k_value_orifice(id_pipe: float, id_orifice: float, length_orifice: float, flow: float, NU=exp.NU_WATER) -> float: +def k_value_orifice(id_pipe: float, id_orifice: float, length_orifice: float, flow: float, NU=con.NU_WATER) -> float: """This function calculates the minor loss coefficient of a thick and thin orifice plate in a pipe using the equation defined here, where Re is the reynolds number on the inlet side, and D_pipe and D_orifice are the inner diameter of the enclosing pipe and orifice, respectively, and L is the length of the orifice: diff --git a/aide_design/materials_database.py b/aide_design/materials_database.py index 4198ae1f..d9c803c1 100644 --- a/aide_design/materials_database.py +++ b/aide_design/materials_database.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- + """ -Created on Mon Aug 7 12:05:59 2017 -@author: kn348 """ import math @@ -12,12 +10,12 @@ try: from aide_design.units import unit_registry as u from aide_design import utility as ut + from aide_design import constants as con except ModuleNotFoundError: from aide_design.units import unit_registry as u from aide_design import utility as ut + from aide_design import constants as con -gravity = 9.80665 * u.m/u.s**2 -"""Define the gravitational constant, in m/s².""" ########### Materials Constants - general ############ @@ -29,8 +27,8 @@ THICKNESS_CONCRETE_MIN = 5*u.cm #used throughout the code -#0 is English, 1 is metric, drill series is needed for the -#drill series at the bottom of this sheet and tube series +#0 is English, 1 is metric, drill series is needed for the +#drill series at the bottom of this sheet and tube series #is needed for the Cdc code EN_DRILL_SERIES = 0 @@ -40,6 +38,8 @@ ########### Material constants - entrance tank ############ +SDR_LFOM = 26 + THICKNESS_LFOM_SHEET = THICKNESS_CONCRETE_MIN NOM_DIAM_ENT_TANK_FLOAT = 8*u.inch @@ -63,9 +63,9 @@ NOM_DIAM_RAPID_MIX_AIR_RELEASE = 1*u.inch ############ Material constants - chem storage tanks ############ - + THICKNESS_CHEM_TANK_WALL = 5*u.mm - + #Supplier Information: #http://www.rotoplas.com/assets/files/industria/catalogo.pdf @@ -73,8 +73,8 @@ VOL_SUPPLIER_CHEM_TANK = [208.198, 450, 600, 750, 1100, 2500]*u.L -#the following array is a 2D array in which -#in each element, the first element is tank diameter +#the following array is a 2D array in which +#in each element, the first element is tank diameter #and the second element is tank height DIMENSIONS_SUPPLIER_CHEM_TANK = [[0.571, 0.851], [0.85, 0.99], [0.96, 1.10], [1.10, 1.02], [1.10, 1.39], [1.55, 1.65]]*u.m @@ -100,29 +100,12 @@ NOM_DIAM_FLOC_MODULES_LARGE = 1.5*u.inch ############ Material constants - sedimentation ############# - -WIDTH_SED_PLATE = 1.06*u.m - -THICKNESS_SED_PLATE = 0.2*u.cm - -SPACE_SED_PLATE = 2.5*u.cm - -ANGLE_SED_PLATE = 60*u.deg - -THICKNESS_SED_WEIR = 5*u.cm - -#Maximum length of sed plate sticking out past module pipes without any +#Maximum length of sed plate sticking out past module pipes without any #additional support. The goal is to prevent floppy modules that don't maintain # constant distances between the plates LENGTH_SED_PLATE_CANTILEVERED = 20*u.cm -DIST_CENTER_SED_PLATE = SPACE_SED_PLATE + THICKNESS_SED_PLATE - -N_SED_MODULE_PLATES_MAX = math.floor((LENGTH_SED_PLATE_CANTILEVERED/DIST_CENTER_SED_PLATE*np.tan(ANGLE_SED_PLATE ))+1) - -N_SED_MODULE_PLATES_MIN = 8 - NOM_DIAM_SED_HOPPER_DRAIN = 1*u.inch NOM_DIAM_SED_HOPPER_VIEWER = 2*u.inch @@ -151,13 +134,13 @@ NOM_DIAM_FILTER_BACKWASH_BRANCH_HOLDER = 2*u.inch -#Minimum vertical spacing between trunk line pipes going through +#Minimum vertical spacing between trunk line pipes going through #the filter wall for concrete construction SPACE_FILTER_TRUNK_MIN = 3*u.cm -#Space between the ends of the branch receiver pipes and the walls so that +#Space between the ends of the branch receiver pipes and the walls so that #the manifold assemblies are easy to lower into the filter boxes -# (if the branch receivers extended the entire length of the box they would +# (if the branch receivers extended the entire length of the box they would #just barely fit and it would be hard to get into place) SPACE_FILTER_MANIFOLD_ASSEMBLY = 1*u.cm @@ -193,18 +176,18 @@ while DIAM_DRILL_MET[counter] <= 4.98*u.mm: counter+=1 DIAM_DRILL_MET.append(DIAM_DRILL_MET[counter-1] + 0.1*u.mm) - + while DIAM_DRILL_MET[counter] < 20*u.mm: counter+=1 DIAM_DRILL_MET.append(DIAM_DRILL_MET[counter-1] + 1*u.mm) - + while DIAM_DRILL_MET[counter] < 50*u.mm: counter+=1 DIAM_DRILL_MET.append(DIAM_DRILL_MET[counter-1] + 2*u.mm) def diam_drill(EN_DRILL_SERIES): - if EN_DRILL_SERIES == 0: + if EN_DRILL_SERIES == 0: DIAM_DRILL = DIAM_DRILL_ENG else: DIAM_DRILL = DIAM_DRILL_MET - return DIAM_DRILL \ No newline at end of file + return DIAM_DRILL diff --git a/aide_design/optional_inputs.py b/aide_design/optional_inputs.py new file mode 100644 index 00000000..2ab5eb9a --- /dev/null +++ b/aide_design/optional_inputs.py @@ -0,0 +1,31 @@ +"""This file contains the default values which may be overriden by user inputs +for design parameters of AguaClara plants. + +""" +from aide_design.units import unit_registry as u + +## ETLF + +# Entrance Tank +L_ET_MAX = 2.2 * u.m + +# LFOM +HL_LFOM = 20 * u.cm + +S_LFOM_ORIFICE = 1 * u.cm # minimum wall distance between orifices, for lfom structural stability + +# Flocculator +HL_FLOC = 0.4 * u.m + +COLL_POT = 37000 # collision potential, also referred to as Gt + +FREEBOARD = 10 * u.cm + +# Sedimentation tank +THICKNESS_PLANT_FLOOR = 0.2 * u.m # plant floor slab thickness + +THICKNESS_SED_WALL = 0.15 * u.m # thickness of sed tank dividing wall + +FLOC_BLANKET_HEIGHT = 0.25 * u.m # vertical height of floc blanket from peak of slope to weir + +HL_OUTLET_MAN = 4 * u.cm # head loss through the outlet manifold diff --git a/aide_design/physchem.py b/aide_design/physchem.py index c19e7252..e48d0ed1 100644 --- a/aide_design/physchem.py +++ b/aide_design/physchem.py @@ -1,716 +1,709 @@ -""" -Created on Thu Jun 15 14:07:28 2017 - -@author: Karan Newatia - -Last modified: Mon Aug 7 2017 -By: Sage Weber-Shirk - - -This file contains unit process functions pertaining to the design of -physical/chemical unit processes for AguaClara water treatment plants. -""" - -########################## Imports ########################## -import numpy as np -from scipy import interpolate, integrate - -try: - from aide_design.units import unit_registry as u - from aide_design import utility as ut -except ModuleNotFoundError: - from aide_design.units import unit_registry as u - from aide_design import utility as ut - -gravity = 9.80665 * u.m/u.s**2 -"""Define the gravitational constant, in m/s².""" - -###################### Simple geometry ###################### -"""A few equations for useful geometry. -Is there a geometry package that we should be using?""" - -@u.wraps(u.m**2, u.m, False) -def area_circle(DiamCircle): - """Return the area of a circle.""" - ut.check_range([DiamCircle, ">0", "DiamCircle"]) - return np.pi / 4 * DiamCircle**2 - - -@u.wraps(u.m, u.m**2, False) -def diam_circle(AreaCircle): - """Return the diameter of a circle.""" - ut.check_range([AreaCircle, ">0", "AreaCircle"]) - return np.sqrt(4 * AreaCircle / np.pi) - -######################### Hydraulics ######################### -RATIO_VC_ORIFICE = 0.62 - -RE_TRANSITION_PIPE = 2100 - -K_KOZENY=5 - - -WATER_DENSITY_TABLE = [(273.15, 278.15, 283.15, 293.15, 303.15, 313.15, - 323.15, 333.15, 343.15, 353.15, 363.15, 373.15 - ), (999.9, 1000, 999.7, 998.2, 995.7, 992.2, - 988.1, 983.2, 977.8, 971.8, 965.3, 958.4 - ) - ] -"""Table of temperatures and the corresponding water density. - -Index[0] is a list of water temperatures, in Kelvin. -Index[1] is the corresponding densities, in kg/m³. -""" - - -@u.wraps(u.kg/(u.m*u.s), [u.degK], False) -def viscosity_dynamic(temp): - """Return the dynamic viscosity of water at a given temperature. - - If given units, the function will automatically convert to Kelvin. - If not given units, the function will assume Kelvin. - """ - ut.check_range([temp, ">0", "Temperature in Kelvin"]) - return 2.414 * (10**-5) * 10**(247.8 / (temp-140)) - - -@u.wraps(u.kg/u.m**3, [u.degK], False) -def density_water(temp): - """Return the density of water at a given temperature. - - If given units, the function will automatically convert to Kelvin. - If not given units, the function will assume Kelvin. - """ - ut.check_range([temp, ">0", "Temperature in Kelvin"]) - rhointerpolated = interpolate.CubicSpline(WATER_DENSITY_TABLE[0], - WATER_DENSITY_TABLE[1]) - return rhointerpolated(temp) - - -@u.wraps(u.m**2/u.s, [u.degK], False) -def viscosity_kinematic(temp): - """Return the kinematic viscosity of water at a given temperature. - - If given units, the function will automatically convert to Kelvin. - If not given units, the function will assume Kelvin. - """ - ut.check_range([temp, ">0", "Temperature in Kelvin"]) - return (viscosity_dynamic(temp).magnitude - / density_water(temp).magnitude) - - -@u.wraps(None, [u.m**3/u.s, u.m, u.m**2/u.s], False) -def re_pipe(FlowRate, Diam, Nu): - """Return the Reynolds Number for a pipe.""" - #Checking input validity - ut.check_range([FlowRate, ">0", "Flow rate"], [Diam, ">0", "Diameter"], - [Nu, ">0", "Nu"]) - return (4 * FlowRate) / (np.pi * Diam * Nu) - - -@u.wraps(u.m, [u.m, u.m, u.dimensionless], False) -@ut.list_handler -def radius_hydraulic(Width, DistCenter, openchannel): - """Return the hydraulic radius. - - Width and DistCenter are length values and openchannel is a boolean. - """ - ut.check_range([Width, ">0", "Width"], [DistCenter, ">0", "DistCenter"], - [openchannel, "boolean", "openchannel"]) - if openchannel: - return (Width*DistCenter) / (Width + 2*DistCenter) - # if openchannel is True, the channel is open. Otherwise, the channel - # is assumed to have a top. - else: - return (Width*DistCenter) / (2 * (Width+DistCenter)) - - -@u.wraps(u.m, [u.m**2, u.m], False) -def radius_hydraulic_general(Area, PerimWetted): - """Return the general hydraulic radius.""" - ut.check_range([Area, ">0", "Area"], [PerimWetted, ">0", "Wetted perimeter"]) - return Area / PerimWetted - - -@u.wraps(None, [u.m**3/u.s, u.m, u.m, u.m**2/u.s, u.dimensionless], False) -def re_rect(FlowRate, Width, DistCenter, Nu, openchannel): - """Return the Reynolds Number for a rectangular channel.""" - #Checking input validity - inputs not checked here are checked by - #functions this function calls. - ut.check_range([FlowRate, ">0", "Flow rate"], [Nu, ">0", "Nu"]) - return (4 * FlowRate - * radius_hydraulic(Width, DistCenter, openchannel).magnitude - / (Width * DistCenter * Nu)) - #Reynolds Number for rectangular channel; open = False if all sides - #are wetted; l = Diam and Diam = 4*R.h - - -@u.wraps(None, [u.m/u.s, u.m**2, u.m, u.m**2/u.s], False) -def re_general(Vel, Area, PerimWetted, Nu): - """Return the Reynolds Number for a general cross section.""" - #Checking input validity - inputs not checked here are checked by - #functions this function calls. - ut.check_range([Vel, ">=0", "Velocity"], [Nu, ">0", "Nu"]) - return 4 * radius_hydraulic_general(Area, PerimWetted).magnitude * Vel / Nu - - -@u.wraps(None, [u.m**3/u.s, u.m, u.m**2/u.s, u.m], False) -@ut.list_handler -def fric(FlowRate, Diam, Nu, PipeRough): - """Return the friction factor for pipe flow. - - This equation applies to both laminar and turbulent flows. - """ - #Checking input validity - inputs not checked here are checked by - #functions this function calls. - ut.check_range([PipeRough, "0-1", "Pipe roughness"]) - if re_pipe(FlowRate, Diam, Nu) >= RE_TRANSITION_PIPE: - #Swamee-Jain friction factor for turbulent flow; best for - #Re>3000 and ε/Diam < 0.02 - f = (0.25 / (np.log10(PipeRough / (3.7 * Diam) - + 5.74 / re_pipe(FlowRate, Diam, Nu) ** 0.9 - ) - ) ** 2 - ) - else: - f = 64 / re_pipe(FlowRate, Diam, Nu) - return f - - -@u.wraps(None, [u.m**3/u.s, u.m, u.m, u.m**2/u.s, u.m, u.dimensionless], False) -@ut.list_handler -def fric_rect(FlowRate, Width, DistCenter, Nu, PipeRough, openchannel): - """Return the friction factor for a rectangular channel.""" - #Checking input validity - inputs not checked here are checked by - #functions this function calls. - ut.check_range([PipeRough, "0-1", "Pipe roughness"]) - if re_rect(FlowRate,Width,DistCenter,Nu,openchannel) >= RE_TRANSITION_PIPE: - #Swamee-Jain friction factor adapted for rectangular channel. - #Diam = 4*R_h in this case. - return (0.25 - / (np.log10((PipeRough - / (3.7 * 4 - * radius_hydraulic(Width, DistCenter, - openchannel).magnitude - ) - ) - + (5.74 / (re_rect(FlowRate, Width, DistCenter, - Nu, openchannel) ** 0.9) - ) - ) - ) ** 2 - ) - else: - return 64 / re_rect(FlowRate, Width, DistCenter, Nu, openchannel) - - -@u.wraps(None, [u.m**2, u.m, u.m/u.s, u.m**2/u.s, u.m], False) -@ut.list_handler -def fric_general(Area, PerimWetted, Vel, Nu, PipeRough): - """Return the friction factor for a general channel.""" - #Checking input validity - inputs not checked here are checked by - #functions this function calls. - ut.check_range([PipeRough, "0-1", "Pipe roughness"]) - if re_general(Vel, Area, PerimWetted, Nu) >= RE_TRANSITION_PIPE: - #Swamee-Jain friction factor adapted for any cross-section. - #Diam = 4*R*h - f= (0.25 / - (np.log10((PipeRough - / (3.7 * 4 - * radius_hydraulic_general(Area, PerimWetted).magnitude - ) - ) - + (5.74 - / re_general(Vel, Area, PerimWetted, Nu) ** 0.9 - ) - ) - ) ** 2 - ) - else: - f = 64 / re_general(Vel, Area, PerimWetted, Nu) - return f - - -@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m**2/u.s, u.m], False) -def headloss_fric(FlowRate, Diam, Length, Nu, PipeRough): - """Return the major head loss (due to wall shear) in a pipe. - - This equation applies to both laminar and turbulent flows. - """ - #Checking input validity - inputs not checked here are checked by - #functions this function calls. - ut.check_range([Length, ">0", "Length"]) - return (fric(FlowRate, Diam, Nu, PipeRough) - * 8 / (gravity.magnitude * np.pi**2) - * (Length * FlowRate**2) / Diam**5 - ) - - -@u.wraps(u.m, [u.m**3/u.s, u.m, u.dimensionless], False) -def headloss_exp(FlowRate, Diam, KMinor): - """Return the minor head loss (due to expansions) in a pipe. - - This equation applies to both laminar and turbulent flows. - """ - #Checking input validity - ut.check_range([FlowRate, ">0", "Flow rate"], [Diam, ">0", "Diameter"], - [KMinor, ">=0", "K minor"]) - return KMinor * 8 / (gravity.magnitude * np.pi**2) * FlowRate**2 / Diam**4 - - -@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m**2/u.s, u.m, u.dimensionless], False) -def headloss(FlowRate, Diam, Length, Nu, PipeRough, KMinor): - """Return the total head loss from major and minor losses in a pipe. - - This equation applies to both laminar and turbulent flows. - """ - #Inputs do not need to be checked here because they are checked by - #functions this function calls. - return (headloss_fric(FlowRate, Diam, Length, Nu, PipeRough).magnitude - + headloss_exp(FlowRate, Diam, KMinor).magnitude) - - -@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m, u.m**2/u.s, u.m, u.dimensionless], False) -def headloss_fric_rect(FlowRate, Width, DistCenter, Length, Nu, PipeRough, openchannel): - """Return the major head loss due to wall shear in a rectangular channel. - - This equation applies to both laminar and turbulent flows. - """ - #Checking input validity - inputs not checked here are checked by - #functions this function calls. - ut.check_range([Length, ">0", "Length"]) - return (fric_rect(FlowRate, Width, DistCenter, Nu, - PipeRough, openchannel) - * Length - / (4 * radius_hydraulic(Width, DistCenter, openchannel).magnitude) - * FlowRate**2 - / (2 * gravity.magnitude * (Width*DistCenter)**2) - ) - - -@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.dimensionless], False) -def headloss_exp_rect(FlowRate, Width, DistCenter, KMinor): - """Return the minor head loss due to expansion in a rectangular channel. - - This equation applies to both laminar and turbulent flows. - """ - #Checking input validity - ut.check_range([FlowRate, ">0", "Flow rate"], [Width, ">0", "Width"], - [DistCenter, ">0", "DistCenter"], [KMinor, ">=0", "K minor"]) - return (KMinor * FlowRate**2 - / (2 * gravity.magnitude * (Width*DistCenter)**2) - ) - - -@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m, u.dimensionless, u.m**2/u.s, u.m, u.dimensionless], False) -def headloss_rect(FlowRate, Width, DistCenter, Length, - KMinor, Nu, PipeRough, openchannel): - """Return the total head loss in a rectangular channel. - - Total head loss is a combination of the major and minor losses. - This equation applies to both laminar and turbulent flows. - """ - #Inputs do not need to be checked here because they are checked by - #functions this function calls. - return (headloss_exp_rect(FlowRate, Width, DistCenter, KMinor).magnitude - + headloss_fric_rect(FlowRate, Width, DistCenter, Length, - Nu, PipeRough, openchannel).magnitude) - - -@u.wraps(u.m, [u.m**2, u.m, u.m/u.s, u.m, u.m**2/u.s, u.m], False) -def headloss_fric_general(Area, PerimWetted, Vel, Length, Nu, PipeRough): - """Return the major head loss due to wall shear in the general case. - - This equation applies to both laminar and turbulent flows. - """ - #Checking input validity - inputs not checked here are checked by - #functions this function calls. - ut.check_range([Length, ">0", "Length"]) - return (fric_general(Area, PerimWetted, Vel, Nu, PipeRough) * Length - / (4 * radius_hydraulic_general(Area, PerimWetted).magnitude) - * Vel**2 / (2*gravity.magnitude) - ) - - -@u.wraps(u.m, [u.m/u.s, u.dimensionless], False) -def headloss_exp_general(Vel, KMinor): - """Return the minor head loss due to expansion in the general case. - - This equation applies to both laminar and turbulent flows. - """ - #Checking input validity - ut.check_range([Vel, ">0", "Velocity"], [KMinor, '>=0', 'K minor']) - return KMinor * Vel**2 / (2*gravity.magnitude) - - -@u.wraps(u.m, [u.m**2, u.m/u.s, u.m, u.m, u.dimensionless, u.m**2/u.s, u.m], False) -def headloss_gen(Area, Vel, PerimWetted, Length, KMinor, Nu, PipeRough): - """Return the total head lossin the general case. - - Total head loss is a combination of major and minor losses. - This equation applies to both laminar and turbulent flows. - """ - #Inputs do not need to be checked here because they are checked by - #functions this function calls. - return (headloss_exp_general(Vel, KMinor).magnitude - + headloss_fric_general(Area, PerimWetted, Vel, - Length, Nu, PipeRough).magnitude) - - -@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.dimensionless, - u.m**2/u.s, u.m, u.dimensionless], False) -def headloss_manifold(FlowRate, Diam, Length, KMinor, Nu, PipeRough, NumOutlets): - """Return the total head loss through the manifold.""" - #Checking input validity - inputs not checked here are checked by - #functions this function calls. - ut.check_range([NumOutlets, ">0, int", 'Number of outlets']) - return (headloss(FlowRate, Diam, Length, Nu, PipeRough, KMinor).magnitude - * ((1/3 ) - + (1 / (2*NumOutlets)) - + (1 / (6*NumOutlets**2)) - ) - ) - - -@u.wraps(u.m**3/u.s, [u.m, u.m, u.dimensionless], False) -@ut.list_handler -def flow_orifice(Diam, Height, RatioVCOrifice): - """Return the flow rate of the orifice.""" - #Checking input validity - ut.check_range([Diam, ">0", "Diameter"], - [RatioVCOrifice, "0-1", "VC orifice ratio"]) - if Height > 0: - return (RatioVCOrifice * area_circle(Diam).magnitude - * np.sqrt(2 * gravity.magnitude * Height)) - else: - return 0 - - -#Deviates from the MathCad at the 6th decimal place. Worth investigating or not? -@u.wraps(u.m**3/u.s, [u.m, u.m, u.dimensionless], False) -@ut.list_handler -def flow_orifice_vert(Diam, Height, RatioVCOrifice): - """Return the vertical flow rate of the orifice.""" - #Checking input validity - ut.check_range([RatioVCOrifice, "0-1", "VC orifice ratio"]) - if Height > -Diam / 2: - flow_vert = integrate.quad(lambda z: (Diam * np.sin(np.arccos(z/(Diam/2))) - * np.sqrt(Height - z) - ), - - Diam / 2, - min(Diam/2, Height)) - return flow_vert[0] * RatioVCOrifice * np.sqrt(2 * gravity.magnitude) - else: - return 0 - - -@u.wraps(u.m, [u.m, u.dimensionless, u.m**3/u.s], False) -def head_orifice(Diam, RatioVCOrifice, FlowRate): - """Return the head of the orifice.""" - #Checking input validity - ut.check_range([Diam, ">0", "Diameter"], [FlowRate, ">0", "Flow rate"], - [RatioVCOrifice, "0-1", "VC orifice ratio"]) - return ((FlowRate - / (RatioVCOrifice * area_circle(Diam).magnitude) - )**2 - / (2*gravity.magnitude) - ) - - -@u.wraps(u.m**2, [u.m, u.dimensionless, u.m**3/u.s], False) -def area_orifice(Height, RatioVCOrifice, FlowRate): - """Return the area of the orifice.""" - #Checking input validity - ut.check_range([Height, ">0", "Height"], [FlowRate, ">0", "Flow rate"], - [RatioVCOrifice, "0-1, >0", "VC orifice ratio"]) - return FlowRate / (RatioVCOrifice * np.sqrt(2 * gravity.magnitude * Height)) - - -@u.wraps(None, [u.m**3/u.s, u.dimensionless, u.m, u.m], False) -def num_orifices(FlowPlant, RatioVCOrifice, HeadLossOrifice, DiamOrifice): - """Return the number of orifices.""" - #Inputs do not need to be checked here because they are checked by - #functions this function calls. - return np.ceil(area_orifice(HeadLossOrifice, RatioVCOrifice, - FlowPlant).magnitude - / area_circle(DiamOrifice).magnitude) - - -# Here we define functions that return the flow rate. -@u.wraps(u.m**3/u.s, [u.m, u.m**2/u.s], False) -def flow_transition(Diam, Nu): - """Return the flow rate for the laminar/turbulent transition. - - This equation is used in some of the other equations for flow. - """ - #Checking input validity - ut.check_range([Diam, ">0", "Diameter"], [Nu, ">0", "Nu"]) - return np.pi * Diam * RE_TRANSITION_PIPE * Nu / 4 - - -@u.wraps(u.m**3/u.s, [u.m, u.m, u.m, u.m**2/u.s], False) -def flow_hagen(Diam, HeadLossFric, Length, Nu): - """Return the flow rate for laminar flow with only major losses.""" - #Checking input validity - ut.check_range([Diam, ">0", "Diameter"], [Length, ">0", "Length"], - [HeadLossFric, ">=0", "Headloss due to friction"], - [Nu, ">0", "Nu"]) - return (np.pi*Diam**4) / (128*Nu) * gravity.magnitude * HeadLossFric / Length - - -@u.wraps(u.m**3/u.s, [u.m, u.m, u.m, u.m**2/u.s, u.m], False) -def flow_swamee(Diam, HeadLossFric, Length, Nu, PipeRough): - """Return the flow rate for turbulent flow with only major losses.""" - #Checking input validity - ut.check_range([Diam, ">0", "Diameter"], [Length, ">0", "Length"], - [HeadLossFric, ">0", "Headloss due to friction"], - [Nu, ">0", "Nu"], [PipeRough, "0-1", "Pipe roughness"]) - logterm = np.log10(PipeRough / (3.7 * Diam) - + 2.51 * Nu * np.sqrt(Length / (2 * gravity.magnitude - * HeadLossFric - * Diam**3) - ) - ) - return ((-np.pi / np.sqrt(2)) * Diam**(5/2) * logterm - * np.sqrt(gravity.magnitude * HeadLossFric / Length) - ) - - -@u.wraps(u.m**3/u.s, [u.m, u.m, u.m, u.m**2/u.s, u.m], False) -@ut.list_handler -def flow_pipemajor(Diam, HeadLossFric, Length, Nu, PipeRough): - """Return the flow rate with only major losses. - - This function applies to both laminar and turbulent flows. - """ - #Inputs do not need to be checked here because they are checked by - #functions this function calls. - FlowHagen = flow_hagen(Diam, HeadLossFric, Length, Nu).magnitude - if FlowHagen < flow_transition(Diam, Nu).magnitude: - return FlowHagen - else: - return flow_swamee(Diam, HeadLossFric, Length, Nu, PipeRough).magnitude - - -@u.wraps(u.m**3/u.s, [u.m, u.m, u.dimensionless], False) -def flow_pipeminor(Diam, HeadLossExpans, KMinor): - """Return the flow rate with only minor losses. - - This function applies to both laminar and turbulent flows. - """ - #Checking input validity - inputs not checked here are checked by - #functions this function calls. - ut.check_range([HeadLossExpans, ">=0", "Headloss due to expansion"], - [KMinor, ">0", "K minor"]) - return (area_circle(Diam).magnitude * np.sqrt(2 * gravity.magnitude - * HeadLossExpans - / KMinor) - ) - -# Now we put all of the flow equations together and calculate the flow in a -# straight pipe that has both major and minor losses and might be either -# laminar or turbulent. -@u.wraps(u.m**3/u.s, [u.m, u.m, u.m, u.m**2/u.s, u.m, u.dimensionless], False) -@ut.list_handler -def flow_pipe(Diam, HeadLoss, Length, Nu, PipeRough, KMinor): - """Return the the flow in a straight pipe. - - This function works for both major and minor losses and - works whether the flow is laminar or turbulent. - """ - #Inputs do not need to be checked here because they are checked by - #functions this function calls. - if KMinor == 0: - FlowRate = flow_pipemajor(Diam, HeadLoss, Length, Nu, - PipeRough).magnitude - else: - FlowRatePrev = 0 - err = 1.0 - FlowRate = min(flow_pipemajor(Diam, HeadLoss, Length, - Nu, PipeRough).magnitude, - flow_pipeminor(Diam, HeadLoss, KMinor).magnitude - ) - while err > 0.01: - FlowRatePrev = FlowRate - HLFricNew = (HeadLoss * headloss_fric(FlowRate, Diam, Length, - Nu, PipeRough).magnitude - / (headloss_fric(FlowRate, Diam, Length, - Nu, PipeRough).magnitude - + headloss_exp(FlowRate, Diam, KMinor).magnitude - ) - ) - FlowRate = flow_pipemajor(Diam, HLFricNew, Length, - Nu, PipeRough).magnitude - if FlowRate == 0: - err = 0.0 - else: - err = (abs(FlowRate - FlowRatePrev) - / ((FlowRate + FlowRatePrev) / 2) - ) - return FlowRate - - -@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m**2/u.s], False) -def diam_hagen(FlowRate, HeadLossFric, Length, Nu): - #Checking input validity - ut.check_range([FlowRate, ">0", "Flow rate"], [Length, ">0", "Length"], - [HeadLossFric, ">0", "Headloss due to friction"], - [Nu, ">0", "Nu"]) - return ((128 * Nu * FlowRate * Length) - / (gravity.magnitude * HeadLossFric * np.pi) - ) ** (1/4) - - -@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m**2/u.s, u.m], False) -def diam_swamee(FlowRate, HeadLossFric, Length, Nu, PipeRough): - """Return the inner diameter of a pipe. - - The Swamee Jain equation is dimensionally correct and returns the - inner diameter of a pipe given the flow rate and the head loss due - to shear on the pipe walls. The Swamee Jain equation does NOT take - minor losses into account. This equation ONLY applies to turbulent - flow. - """ - #Checking input validity - ut.check_range([FlowRate, ">0", "Flow rate"], [Length, ">0", "Length"], - [HeadLossFric, ">0", "Headloss due to friction"], - [Nu, ">0", "Nu"], [PipeRough, "0-1", "Pipe roughness"]) - a = ((PipeRough ** 1.25) - * ((Length * FlowRate**2) - / (gravity.magnitude * HeadLossFric) - )**4.75 - ) - b = (Nu * FlowRate**9.4 - * (Length / (gravity.magnitude * HeadLossFric)) ** 5.2 - ) - return 0.66 * (a+b)**0.04 - - -@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m**2/u.s, u.m], False) -@ut.list_handler -def diam_pipemajor(FlowRate, HeadLossFric, Length, Nu, PipeRough): - """Return the pipe IDiam that would result in given major losses. - - This function applies to both laminar and turbulent flow. - """ - #Inputs do not need to be checked here because they are checked by - #functions this function calls. - DiamLaminar = diam_hagen(FlowRate, HeadLossFric, Length, Nu).magnitude - if re_pipe(FlowRate, DiamLaminar, Nu) <= RE_TRANSITION_PIPE: - return DiamLaminar - else: - return diam_swamee(FlowRate, HeadLossFric, Length, - Nu, PipeRough).magnitude - - -@u.wraps(u.m, [u.m**3/u.s, u.m, u.dimensionless], False) -def diam_pipeminor(FlowRate, HeadLossExpans, KMinor): - """Return the pipe ID that would result in the given minor losses. - - This function applies to both laminar and turbulent flow. - """ - #Checking input validity - ut.check_range([FlowRate, ">0", "Flow rate"], [KMinor, ">=0", "K minor"], - [HeadLossExpans, ">0", "Headloss due to expansion"]) - return (np.sqrt(4 * FlowRate / np.pi) - * (KMinor / (2 * gravity.magnitude * HeadLossExpans)) ** (1/4) - ) - - -@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m**2/u.s, u.m, None], False) -@ut.list_handler -def diam_pipe(FlowRate, HeadLoss, Length, Nu, PipeRough, KMinor): - """Return the pipe ID that would result in the given total head loss. - - This function applies to both laminar and turbulent flow and - incorporates both minor and major losses. - """ - #Inputs do not need to be checked here because they are checked by - #functions this function calls. - if KMinor == 0: - Diam = diam_pipemajor(FlowRate, HeadLoss, Length, Nu, - PipeRough).magnitude - else: - Diam = max(diam_pipemajor(FlowRate, HeadLoss, - Length, Nu, PipeRough).magnitude, - diam_pipeminor(FlowRate, HeadLoss, KMinor).magnitude) - err = 1.00 - while err > 0.001: - DiamPrev = Diam - HLFricNew = (HeadLoss * headloss_fric(FlowRate, Diam, Length, - Nu, PipeRough - ).magnitude - / (headloss_fric(FlowRate, Diam, Length, - Nu, PipeRough - ).magnitude - + headloss_exp(FlowRate, - Diam, KMinor - ).magnitude - ) - ) - Diam = diam_pipemajor(FlowRate, HLFricNew, Length, Nu, PipeRough - ).magnitude - err = abs(Diam - DiamPrev) / ((Diam + DiamPrev) / 2) - return Diam - -# Weir head loss equations -@u.wraps(u.m, [u.m**3/u.s, u.m], False) -def width_rect_weir(FlowRate, Height): - """Return the width of a rectangular weir.""" - #Checking input validity - ut.check_range([FlowRate, ">0", "Flow rate"], [Height, ">0", "Height"]) - return ((3 / 2) * FlowRate - / (RATIO_VC_ORIFICE * np.sqrt(2*gravity.magnitude) * Height**(3/2)) - ) - - -# For a pipe, Width is the circumference of the pipe. -# Head loss for a weir is the difference in height between the water -# upstream of the weir and the top of the weir. -@u.wraps(u.m, [u.m**3/u.s, u.m], False) -def headloss_weir(FlowRate, Width): - """Return the headloss of a weir.""" - #Checking input validity - ut.check_range([FlowRate, ">0", "Flow rate"], [Width, ">0", "Width"]) - return (((3/2) * FlowRate - / (RATIO_VC_ORIFICE * np.sqrt(2*gravity.magnitude) * Width) - ) ** (2/3)) - - -@u.wraps(u.m, [u.m, u.m], False) -def flow_rect_weir(Height, Width): - """Return the flow of a rectangular weir.""" - #Checking input validity - ut.check_range([Height, ">0", "Height"], [Width, ">0", "Width"]) - return ((2/3) * RATIO_VC_ORIFICE - * (np.sqrt(2*gravity.magnitude) * Height**(3/2)) - * Width) - - -@u.wraps(u.m, [u.m**3/u.s, u.m], False) -def height_water_critical(FlowRate, Width): - """Return the critical local water depth.""" - #Checking input validity - ut.check_range([FlowRate, ">0", "Flow rate"], [Width, ">0", "Width"]) - return (FlowRate / (Width * np.sqrt(gravity.magnitude))) ** (2/3) - - -@u.wraps(u.m/u.s, u.m, False) -def vel_horizontal(HeightWaterCritical): - """Return the horizontal velocity.""" - #Checking input validity - ut.check_range([HeightWaterCritical, ">0", "Critical height of water"]) - return np.sqrt(gravity.magnitude * HeightWaterCritical) - - -@u.wraps(u.m, [u.m, u.m, u.m/u.s, u.m, u.m**2/u.s], False) -def headloss_kozeny(Length, Diam, Vel, PipeRough, Nu): - """Return the Carmen Kozeny Sand Bed head loss.""" - #Checking input validity - ut.check_range([Length, ">0", "Length"], [Diam, ">0", "Diam"], - [Vel, ">0", "Velocity"], [Nu, ">0", "Nu"], - [PipeRough, "0-1", "Pipe roughness"]) - return (K_KOZENY * Length * Nu - / gravity.magnitude * (1-PipeRough)**2 - / PipeRough**3 * 36 * Vel - / Diam ** 2) +"""This file contains unit process functions pertaining to the design of +physical/chemical unit processes for AguaClara water treatment plants. + +""" + +########################## Imports ########################## +import numpy as np +from scipy import interpolate, integrate + +try: + from aide_design.units import unit_registry as u + from aide_design import utility as ut + from aide_design import constants as con + from aide_design import materials_database as mat +except ModuleNotFoundError: + from aide_design.units import unit_registry as u + from aide_design import utility as ut + from aide_design import constants as con + from aide_design import materials_database as mat + +gravity = 9.80665 * u.m/u.s**2 +"""Define the gravitational constant, in m/s².""" + +###################### Simple geometry ###################### +"""A few equations for useful geometry. +Is there a geometry package that we should be using?""" + +@u.wraps(u.m**2, u.m, False) +def area_circle(DiamCircle): + """Return the area of a circle.""" + ut.check_range([DiamCircle, ">0", "DiamCircle"]) + return np.pi / 4 * DiamCircle**2 + + +@u.wraps(u.m, u.m**2, False) +def diam_circle(AreaCircle): + """Return the diameter of a circle.""" + ut.check_range([AreaCircle, ">0", "AreaCircle"]) + return np.sqrt(4 * AreaCircle / np.pi) + +######################### Hydraulics ######################### +RE_TRANSITION_PIPE = 2100 + +K_KOZENY = mat.K_KOZENY + +WATER_DENSITY_TABLE = [(273.15, 278.15, 283.15, 293.15, 303.15, 313.15, + 323.15, 333.15, 343.15, 353.15, 363.15, 373.15 + ), (999.9, 1000, 999.7, 998.2, 995.7, 992.2, + 988.1, 983.2, 977.8, 971.8, 965.3, 958.4 + ) + ] +"""Table of temperatures and the corresponding water density. + +Index[0] is a list of water temperatures, in Kelvin. +Index[1] is the corresponding densities, in kg/m³. +""" + + +@u.wraps(u.kg/(u.m*u.s), [u.degK], False) +def viscosity_dynamic(temp): + """Return the dynamic viscosity of water at a given temperature. + + If given units, the function will automatically convert to Kelvin. + If not given units, the function will assume Kelvin. + """ + ut.check_range([temp, ">0", "Temperature in Kelvin"]) + return 2.414 * (10**-5) * 10**(247.8 / (temp-140)) + + +@u.wraps(u.kg/u.m**3, [u.degK], False) +def density_water(temp): + """Return the density of water at a given temperature. + + If given units, the function will automatically convert to Kelvin. + If not given units, the function will assume Kelvin. + """ + ut.check_range([temp, ">0", "Temperature in Kelvin"]) + rhointerpolated = interpolate.CubicSpline(WATER_DENSITY_TABLE[0], + WATER_DENSITY_TABLE[1]) + return rhointerpolated(temp) + + +@u.wraps(u.m**2/u.s, [u.degK], False) +def viscosity_kinematic(temp): + """Return the kinematic viscosity of water at a given temperature. + + If given units, the function will automatically convert to Kelvin. + If not given units, the function will assume Kelvin. + """ + ut.check_range([temp, ">0", "Temperature in Kelvin"]) + return (viscosity_dynamic(temp).magnitude + / density_water(temp).magnitude) + + +@u.wraps(None, [u.m**3/u.s, u.m, u.m**2/u.s], False) +def re_pipe(FlowRate, Diam, Nu): + """Return the Reynolds Number for a pipe.""" + #Checking input validity + ut.check_range([FlowRate, ">0", "Flow rate"], [Diam, ">0", "Diameter"], + [Nu, ">0", "Nu"]) + return (4 * FlowRate) / (np.pi * Diam * Nu) + + +@u.wraps(u.m, [u.m, u.m, u.dimensionless], False) +@ut.list_handler +def radius_hydraulic(Width, DistCenter, openchannel): + """Return the hydraulic radius. + + Width and DistCenter are length values and openchannel is a boolean. + """ + ut.check_range([Width, ">0", "Width"], [DistCenter, ">0", "DistCenter"], + [openchannel, "boolean", "openchannel"]) + if openchannel: + return (Width*DistCenter) / (Width + 2*DistCenter) + # if openchannel is True, the channel is open. Otherwise, the channel + # is assumed to have a top. + else: + return (Width*DistCenter) / (2 * (Width+DistCenter)) + + +@u.wraps(u.m, [u.m**2, u.m], False) +def radius_hydraulic_general(Area, PerimWetted): + """Return the general hydraulic radius.""" + ut.check_range([Area, ">0", "Area"], [PerimWetted, ">0", "Wetted perimeter"]) + return Area / PerimWetted + + +@u.wraps(None, [u.m**3/u.s, u.m, u.m, u.m**2/u.s, u.dimensionless], False) +def re_rect(FlowRate, Width, DistCenter, Nu, openchannel): + """Return the Reynolds Number for a rectangular channel.""" + #Checking input validity - inputs not checked here are checked by + #functions this function calls. + ut.check_range([FlowRate, ">0", "Flow rate"], [Nu, ">0", "Nu"]) + return (4 * FlowRate + * radius_hydraulic(Width, DistCenter, openchannel).magnitude + / (Width * DistCenter * Nu)) + #Reynolds Number for rectangular channel; open = False if all sides + #are wetted; l = Diam and Diam = 4*R.h + + +@u.wraps(None, [u.m/u.s, u.m**2, u.m, u.m**2/u.s], False) +def re_general(Vel, Area, PerimWetted, Nu): + """Return the Reynolds Number for a general cross section.""" + #Checking input validity - inputs not checked here are checked by + #functions this function calls. + ut.check_range([Vel, ">=0", "Velocity"], [Nu, ">0", "Nu"]) + return 4 * radius_hydraulic_general(Area, PerimWetted).magnitude * Vel / Nu + + +@u.wraps(None, [u.m**3/u.s, u.m, u.m**2/u.s, u.m], False) +@ut.list_handler +def fric(FlowRate, Diam, Nu, PipeRough): + """Return the friction factor for pipe flow. + + This equation applies to both laminar and turbulent flows. + """ + #Checking input validity - inputs not checked here are checked by + #functions this function calls. + ut.check_range([PipeRough, "0-1", "Pipe roughness"]) + if re_pipe(FlowRate, Diam, Nu) >= RE_TRANSITION_PIPE: + #Swamee-Jain friction factor for turbulent flow; best for + #Re>3000 and ε/Diam < 0.02 + f = (0.25 / (np.log10(PipeRough / (3.7 * Diam) + + 5.74 / re_pipe(FlowRate, Diam, Nu) ** 0.9 + ) + ) ** 2 + ) + else: + f = 64 / re_pipe(FlowRate, Diam, Nu) + return f + + +@u.wraps(None, [u.m**3/u.s, u.m, u.m, u.m**2/u.s, u.m, u.dimensionless], False) +@ut.list_handler +def fric_rect(FlowRate, Width, DistCenter, Nu, PipeRough, openchannel): + """Return the friction factor for a rectangular channel.""" + #Checking input validity - inputs not checked here are checked by + #functions this function calls. + ut.check_range([PipeRough, "0-1", "Pipe roughness"]) + if re_rect(FlowRate,Width,DistCenter,Nu,openchannel) >= RE_TRANSITION_PIPE: + #Swamee-Jain friction factor adapted for rectangular channel. + #Diam = 4*R_h in this case. + return (0.25 + / (np.log10((PipeRough + / (3.7 * 4 + * radius_hydraulic(Width, DistCenter, + openchannel).magnitude + ) + ) + + (5.74 / (re_rect(FlowRate, Width, DistCenter, + Nu, openchannel) ** 0.9) + ) + ) + ) ** 2 + ) + else: + return 64 / re_rect(FlowRate, Width, DistCenter, Nu, openchannel) + + +@u.wraps(None, [u.m**2, u.m, u.m/u.s, u.m**2/u.s, u.m], False) +@ut.list_handler +def fric_general(Area, PerimWetted, Vel, Nu, PipeRough): + """Return the friction factor for a general channel.""" + #Checking input validity - inputs not checked here are checked by + #functions this function calls. + ut.check_range([PipeRough, "0-1", "Pipe roughness"]) + if re_general(Vel, Area, PerimWetted, Nu) >= RE_TRANSITION_PIPE: + #Swamee-Jain friction factor adapted for any cross-section. + #Diam = 4*R*h + f= (0.25 / + (np.log10((PipeRough + / (3.7 * 4 + * radius_hydraulic_general(Area, PerimWetted).magnitude + ) + ) + + (5.74 + / re_general(Vel, Area, PerimWetted, Nu) ** 0.9 + ) + ) + ) ** 2 + ) + else: + f = 64 / re_general(Vel, Area, PerimWetted, Nu) + return f + + +@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m**2/u.s, u.m], False) +def headloss_fric(FlowRate, Diam, Length, Nu, PipeRough): + """Return the major head loss (due to wall shear) in a pipe. + + This equation applies to both laminar and turbulent flows. + """ + #Checking input validity - inputs not checked here are checked by + #functions this function calls. + ut.check_range([Length, ">0", "Length"]) + return (fric(FlowRate, Diam, Nu, PipeRough) + * 8 / (gravity.magnitude * np.pi**2) + * (Length * FlowRate**2) / Diam**5 + ) + + +@u.wraps(u.m, [u.m**3/u.s, u.m, u.dimensionless], False) +def headloss_exp(FlowRate, Diam, KMinor): + """Return the minor head loss (due to expansions) in a pipe. + + This equation applies to both laminar and turbulent flows. + """ + #Checking input validity + ut.check_range([FlowRate, ">0", "Flow rate"], [Diam, ">0", "Diameter"], + [KMinor, ">=0", "K minor"]) + return KMinor * 8 / (gravity.magnitude * np.pi**2) * FlowRate**2 / Diam**4 + + +@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m**2/u.s, u.m, u.dimensionless], False) +def headloss(FlowRate, Diam, Length, Nu, PipeRough, KMinor): + """Return the total head loss from major and minor losses in a pipe. + + This equation applies to both laminar and turbulent flows. + """ + #Inputs do not need to be checked here because they are checked by + #functions this function calls. + return (headloss_fric(FlowRate, Diam, Length, Nu, PipeRough).magnitude + + headloss_exp(FlowRate, Diam, KMinor).magnitude) + + +@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m, u.m**2/u.s, u.m, u.dimensionless], False) +def headloss_fric_rect(FlowRate, Width, DistCenter, Length, Nu, PipeRough, openchannel): + """Return the major head loss due to wall shear in a rectangular channel. + + This equation applies to both laminar and turbulent flows. + """ + #Checking input validity - inputs not checked here are checked by + #functions this function calls. + ut.check_range([Length, ">0", "Length"]) + return (fric_rect(FlowRate, Width, DistCenter, Nu, + PipeRough, openchannel) + * Length + / (4 * radius_hydraulic(Width, DistCenter, openchannel).magnitude) + * FlowRate**2 + / (2 * gravity.magnitude * (Width*DistCenter)**2) + ) + + +@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.dimensionless], False) +def headloss_exp_rect(FlowRate, Width, DistCenter, KMinor): + """Return the minor head loss due to expansion in a rectangular channel. + + This equation applies to both laminar and turbulent flows. + """ + #Checking input validity + ut.check_range([FlowRate, ">0", "Flow rate"], [Width, ">0", "Width"], + [DistCenter, ">0", "DistCenter"], [KMinor, ">=0", "K minor"]) + return (KMinor * FlowRate**2 + / (2 * gravity.magnitude * (Width*DistCenter)**2) + ) + + +@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m, u.dimensionless, u.m**2/u.s, u.m, u.dimensionless], False) +def headloss_rect(FlowRate, Width, DistCenter, Length, + KMinor, Nu, PipeRough, openchannel): + """Return the total head loss in a rectangular channel. + + Total head loss is a combination of the major and minor losses. + This equation applies to both laminar and turbulent flows. + """ + #Inputs do not need to be checked here because they are checked by + #functions this function calls. + return (headloss_exp_rect(FlowRate, Width, DistCenter, KMinor).magnitude + + headloss_fric_rect(FlowRate, Width, DistCenter, Length, + Nu, PipeRough, openchannel).magnitude) + + +@u.wraps(u.m, [u.m**2, u.m, u.m/u.s, u.m, u.m**2/u.s, u.m], False) +def headloss_fric_general(Area, PerimWetted, Vel, Length, Nu, PipeRough): + """Return the major head loss due to wall shear in the general case. + + This equation applies to both laminar and turbulent flows. + """ + #Checking input validity - inputs not checked here are checked by + #functions this function calls. + ut.check_range([Length, ">0", "Length"]) + return (fric_general(Area, PerimWetted, Vel, Nu, PipeRough) * Length + / (4 * radius_hydraulic_general(Area, PerimWetted).magnitude) + * Vel**2 / (2*gravity.magnitude) + ) + + +@u.wraps(u.m, [u.m/u.s, u.dimensionless], False) +def headloss_exp_general(Vel, KMinor): + """Return the minor head loss due to expansion in the general case. + + This equation applies to both laminar and turbulent flows. + """ + #Checking input validity + ut.check_range([Vel, ">0", "Velocity"], [KMinor, '>=0', 'K minor']) + return KMinor * Vel**2 / (2*gravity.magnitude) + + +@u.wraps(u.m, [u.m**2, u.m/u.s, u.m, u.m, u.dimensionless, u.m**2/u.s, u.m], False) +def headloss_gen(Area, Vel, PerimWetted, Length, KMinor, Nu, PipeRough): + """Return the total head lossin the general case. + + Total head loss is a combination of major and minor losses. + This equation applies to both laminar and turbulent flows. + """ + #Inputs do not need to be checked here because they are checked by + #functions this function calls. + return (headloss_exp_general(Vel, KMinor).magnitude + + headloss_fric_general(Area, PerimWetted, Vel, + Length, Nu, PipeRough).magnitude) + + +@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.dimensionless, + u.m**2/u.s, u.m, u.dimensionless], False) +def headloss_manifold(FlowRate, Diam, Length, KMinor, Nu, PipeRough, NumOutlets): + """Return the total head loss through the manifold.""" + #Checking input validity - inputs not checked here are checked by + #functions this function calls. + ut.check_range([NumOutlets, ">0, int", 'Number of outlets']) + return (headloss(FlowRate, Diam, Length, Nu, PipeRough, KMinor).magnitude + * ((1/3 ) + + (1 / (2*NumOutlets)) + + (1 / (6*NumOutlets**2)) + ) + ) + + +@u.wraps(u.m**3/u.s, [u.m, u.m, u.dimensionless], False) +@ut.list_handler +def flow_orifice(Diam, Height, RatioVCOrifice): + """Return the flow rate of the orifice.""" + #Checking input validity + ut.check_range([Diam, ">0", "Diameter"], + [RatioVCOrifice, "0-1", "VC orifice ratio"]) + if Height > 0: + return (RatioVCOrifice * area_circle(Diam).magnitude + * np.sqrt(2 * gravity.magnitude * Height)) + else: + return 0 + + +#Deviates from the MathCad at the 6th decimal place. Worth investigating or not? +@u.wraps(u.m**3/u.s, [u.m, u.m, u.dimensionless], False) +@ut.list_handler +def flow_orifice_vert(Diam, Height, RatioVCOrifice): + """Return the vertical flow rate of the orifice.""" + #Checking input validity + ut.check_range([RatioVCOrifice, "0-1", "VC orifice ratio"]) + if Height > -Diam / 2: + flow_vert = integrate.quad(lambda z: (Diam * np.sin(np.arccos(z/(Diam/2))) + * np.sqrt(Height - z) + ), + - Diam / 2, + min(Diam/2, Height)) + return flow_vert[0] * RatioVCOrifice * np.sqrt(2 * gravity.magnitude) + else: + return 0 + + +@u.wraps(u.m, [u.m, u.dimensionless, u.m**3/u.s], False) +def head_orifice(Diam, RatioVCOrifice, FlowRate): + """Return the head of the orifice.""" + #Checking input validity + ut.check_range([Diam, ">0", "Diameter"], [FlowRate, ">0", "Flow rate"], + [RatioVCOrifice, "0-1", "VC orifice ratio"]) + return ((FlowRate + / (RatioVCOrifice * area_circle(Diam).magnitude) + )**2 + / (2*gravity.magnitude) + ) + + +@u.wraps(u.m**2, [u.m, u.dimensionless, u.m**3/u.s], False) +def area_orifice(Height, RatioVCOrifice, FlowRate): + """Return the area of the orifice.""" + #Checking input validity + ut.check_range([Height, ">0", "Height"], [FlowRate, ">0", "Flow rate"], + [RatioVCOrifice, "0-1, >0", "VC orifice ratio"]) + return FlowRate / (RatioVCOrifice * np.sqrt(2 * gravity.magnitude * Height)) + + +@u.wraps(None, [u.m**3/u.s, u.dimensionless, u.m, u.m], False) +def num_orifices(FlowPlant, RatioVCOrifice, HeadLossOrifice, DiamOrifice): + """Return the number of orifices.""" + #Inputs do not need to be checked here because they are checked by + #functions this function calls. + return np.ceil(area_orifice(HeadLossOrifice, RatioVCOrifice, + FlowPlant).magnitude + / area_circle(DiamOrifice).magnitude) + + +# Here we define functions that return the flow rate. +@u.wraps(u.m**3/u.s, [u.m, u.m**2/u.s], False) +def flow_transition(Diam, Nu): + """Return the flow rate for the laminar/turbulent transition. + + This equation is used in some of the other equations for flow. + """ + #Checking input validity + ut.check_range([Diam, ">0", "Diameter"], [Nu, ">0", "Nu"]) + return np.pi * Diam * RE_TRANSITION_PIPE * Nu / 4 + + +@u.wraps(u.m**3/u.s, [u.m, u.m, u.m, u.m**2/u.s], False) +def flow_hagen(Diam, HeadLossFric, Length, Nu): + """Return the flow rate for laminar flow with only major losses.""" + #Checking input validity + ut.check_range([Diam, ">0", "Diameter"], [Length, ">0", "Length"], + [HeadLossFric, ">=0", "Headloss due to friction"], + [Nu, ">0", "Nu"]) + return (np.pi*Diam**4) / (128*Nu) * gravity.magnitude * HeadLossFric / Length + + +@u.wraps(u.m**3/u.s, [u.m, u.m, u.m, u.m**2/u.s, u.m], False) +def flow_swamee(Diam, HeadLossFric, Length, Nu, PipeRough): + """Return the flow rate for turbulent flow with only major losses.""" + #Checking input validity + ut.check_range([Diam, ">0", "Diameter"], [Length, ">0", "Length"], + [HeadLossFric, ">0", "Headloss due to friction"], + [Nu, ">0", "Nu"], [PipeRough, "0-1", "Pipe roughness"]) + logterm = np.log10(PipeRough / (3.7 * Diam) + + 2.51 * Nu * np.sqrt(Length / (2 * gravity.magnitude + * HeadLossFric + * Diam**3) + ) + ) + return ((-np.pi / np.sqrt(2)) * Diam**(5/2) * logterm + * np.sqrt(gravity.magnitude * HeadLossFric / Length) + ) + + +@u.wraps(u.m**3/u.s, [u.m, u.m, u.m, u.m**2/u.s, u.m], False) +@ut.list_handler +def flow_pipemajor(Diam, HeadLossFric, Length, Nu, PipeRough): + """Return the flow rate with only major losses. + + This function applies to both laminar and turbulent flows. + """ + #Inputs do not need to be checked here because they are checked by + #functions this function calls. + FlowHagen = flow_hagen(Diam, HeadLossFric, Length, Nu).magnitude + if FlowHagen < flow_transition(Diam, Nu).magnitude: + return FlowHagen + else: + return flow_swamee(Diam, HeadLossFric, Length, Nu, PipeRough).magnitude + + +@u.wraps(u.m**3/u.s, [u.m, u.m, u.dimensionless], False) +def flow_pipeminor(Diam, HeadLossExpans, KMinor): + """Return the flow rate with only minor losses. + + This function applies to both laminar and turbulent flows. + """ + #Checking input validity - inputs not checked here are checked by + #functions this function calls. + ut.check_range([HeadLossExpans, ">=0", "Headloss due to expansion"], + [KMinor, ">0", "K minor"]) + return (area_circle(Diam).magnitude * np.sqrt(2 * gravity.magnitude + * HeadLossExpans + / KMinor) + ) + +# Now we put all of the flow equations together and calculate the flow in a +# straight pipe that has both major and minor losses and might be either +# laminar or turbulent. +@u.wraps(u.m**3/u.s, [u.m, u.m, u.m, u.m**2/u.s, u.m, u.dimensionless], False) +@ut.list_handler +def flow_pipe(Diam, HeadLoss, Length, Nu, PipeRough, KMinor): + """Return the the flow in a straight pipe. + + This function works for both major and minor losses and + works whether the flow is laminar or turbulent. + """ + #Inputs do not need to be checked here because they are checked by + #functions this function calls. + if KMinor == 0: + FlowRate = flow_pipemajor(Diam, HeadLoss, Length, Nu, + PipeRough).magnitude + else: + FlowRatePrev = 0 + err = 1.0 + FlowRate = min(flow_pipemajor(Diam, HeadLoss, Length, + Nu, PipeRough).magnitude, + flow_pipeminor(Diam, HeadLoss, KMinor).magnitude + ) + while err > 0.01: + FlowRatePrev = FlowRate + HLFricNew = (HeadLoss * headloss_fric(FlowRate, Diam, Length, + Nu, PipeRough).magnitude + / (headloss_fric(FlowRate, Diam, Length, + Nu, PipeRough).magnitude + + headloss_exp(FlowRate, Diam, KMinor).magnitude + ) + ) + FlowRate = flow_pipemajor(Diam, HLFricNew, Length, + Nu, PipeRough).magnitude + if FlowRate == 0: + err = 0.0 + else: + err = (abs(FlowRate - FlowRatePrev) + / ((FlowRate + FlowRatePrev) / 2) + ) + return FlowRate + + +@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m**2/u.s], False) +def diam_hagen(FlowRate, HeadLossFric, Length, Nu): + #Checking input validity + ut.check_range([FlowRate, ">0", "Flow rate"], [Length, ">0", "Length"], + [HeadLossFric, ">0", "Headloss due to friction"], + [Nu, ">0", "Nu"]) + return ((128 * Nu * FlowRate * Length) + / (gravity.magnitude * HeadLossFric * np.pi) + ) ** (1/4) + + +@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m**2/u.s, u.m], False) +def diam_swamee(FlowRate, HeadLossFric, Length, Nu, PipeRough): + """Return the inner diameter of a pipe. + + The Swamee Jain equation is dimensionally correct and returns the + inner diameter of a pipe given the flow rate and the head loss due + to shear on the pipe walls. The Swamee Jain equation does NOT take + minor losses into account. This equation ONLY applies to turbulent + flow. + """ + #Checking input validity + ut.check_range([FlowRate, ">0", "Flow rate"], [Length, ">0", "Length"], + [HeadLossFric, ">0", "Headloss due to friction"], + [Nu, ">0", "Nu"], [PipeRough, "0-1", "Pipe roughness"]) + a = ((PipeRough ** 1.25) + * ((Length * FlowRate**2) + / (gravity.magnitude * HeadLossFric) + )**4.75 + ) + b = (Nu * FlowRate**9.4 + * (Length / (gravity.magnitude * HeadLossFric)) ** 5.2 + ) + return 0.66 * (a+b)**0.04 + + +@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m**2/u.s, u.m], False) +@ut.list_handler +def diam_pipemajor(FlowRate, HeadLossFric, Length, Nu, PipeRough): + """Return the pipe IDiam that would result in given major losses. + + This function applies to both laminar and turbulent flow. + """ + #Inputs do not need to be checked here because they are checked by + #functions this function calls. + DiamLaminar = diam_hagen(FlowRate, HeadLossFric, Length, Nu).magnitude + if re_pipe(FlowRate, DiamLaminar, Nu) <= RE_TRANSITION_PIPE: + return DiamLaminar + else: + return diam_swamee(FlowRate, HeadLossFric, Length, + Nu, PipeRough).magnitude + + +@u.wraps(u.m, [u.m**3/u.s, u.m, u.dimensionless], False) +def diam_pipeminor(FlowRate, HeadLossExpans, KMinor): + """Return the pipe ID that would result in the given minor losses. + + This function applies to both laminar and turbulent flow. + """ + #Checking input validity + ut.check_range([FlowRate, ">0", "Flow rate"], [KMinor, ">=0", "K minor"], + [HeadLossExpans, ">0", "Headloss due to expansion"]) + return (np.sqrt(4 * FlowRate / np.pi) + * (KMinor / (2 * gravity.magnitude * HeadLossExpans)) ** (1/4) + ) + + +@u.wraps(u.m, [u.m**3/u.s, u.m, u.m, u.m**2/u.s, u.m, None], False) +@ut.list_handler +def diam_pipe(FlowRate, HeadLoss, Length, Nu, PipeRough, KMinor): + """Return the pipe ID that would result in the given total head loss. + + This function applies to both laminar and turbulent flow and + incorporates both minor and major losses. + """ + #Inputs do not need to be checked here because they are checked by + #functions this function calls. + if KMinor == 0: + Diam = diam_pipemajor(FlowRate, HeadLoss, Length, Nu, + PipeRough).magnitude + else: + Diam = max(diam_pipemajor(FlowRate, HeadLoss, + Length, Nu, PipeRough).magnitude, + diam_pipeminor(FlowRate, HeadLoss, KMinor).magnitude) + err = 1.00 + while err > 0.001: + DiamPrev = Diam + HLFricNew = (HeadLoss * headloss_fric(FlowRate, Diam, Length, + Nu, PipeRough + ).magnitude + / (headloss_fric(FlowRate, Diam, Length, + Nu, PipeRough + ).magnitude + + headloss_exp(FlowRate, + Diam, KMinor + ).magnitude + ) + ) + Diam = diam_pipemajor(FlowRate, HLFricNew, Length, Nu, PipeRough + ).magnitude + err = abs(Diam - DiamPrev) / ((Diam + DiamPrev) / 2) + return Diam + +# Weir head loss equations +@u.wraps(u.m, [u.m**3/u.s, u.m], False) +def width_rect_weir(FlowRate, Height): + """Return the width of a rectangular weir.""" + #Checking input validity + ut.check_range([FlowRate, ">0", "Flow rate"], [Height, ">0", "Height"]) + return ((3 / 2) * FlowRate + / (con.RATIO_VC_ORIFICE * np.sqrt(2*gravity.magnitude) * Height**(3/2)) + ) + + +# For a pipe, Width is the circumference of the pipe. +# Head loss for a weir is the difference in height between the water +# upstream of the weir and the top of the weir. +@u.wraps(u.m, [u.m**3/u.s, u.m], False) +def headloss_weir(FlowRate, Width): + """Return the headloss of a weir.""" + #Checking input validity + ut.check_range([FlowRate, ">0", "Flow rate"], [Width, ">0", "Width"]) + return (((3/2) * FlowRate + / (con.RATIO_VC_ORIFICE * np.sqrt(2*gravity.magnitude) * Width) + ) ** (2/3)) + + +@u.wraps(u.m, [u.m, u.m], False) +def flow_rect_weir(Height, Width): + """Return the flow of a rectangular weir.""" + #Checking input validity + ut.check_range([Height, ">0", "Height"], [Width, ">0", "Width"]) + return ((2/3) * con.RATIO_VC_ORIFICE + * (np.sqrt(2*gravity.magnitude) * Height**(3/2)) + * Width) + + +@u.wraps(u.m, [u.m**3/u.s, u.m], False) +def height_water_critical(FlowRate, Width): + """Return the critical local water depth.""" + #Checking input validity + ut.check_range([FlowRate, ">0", "Flow rate"], [Width, ">0", "Width"]) + return (FlowRate / (Width * np.sqrt(gravity.magnitude))) ** (2/3) + + +@u.wraps(u.m/u.s, u.m, False) +def vel_horizontal(HeightWaterCritical): + """Return the horizontal velocity.""" + #Checking input validity + ut.check_range([HeightWaterCritical, ">0", "Critical height of water"]) + return np.sqrt(gravity.magnitude * HeightWaterCritical) + + +@u.wraps(u.m, [u.m, u.m, u.m/u.s, u.m, u.m**2/u.s], False) +def headloss_kozeny(Length, Diam, Vel, PipeRough, Nu): + """Return the Carmen Kozeny Sand Bed head loss.""" + #Checking input validity + ut.check_range([Length, ">0", "Length"], [Diam, ">0", "Diam"], + [Vel, ">0", "Velocity"], [Nu, ">0", "Nu"], + [PipeRough, "0-1", "Pipe roughness"]) + return (K_KOZENY * Length * Nu + / gravity.magnitude * (1-PipeRough)**2 + / PipeRough**3 * 36 * Vel + / Diam ** 2) diff --git a/aide_design/pipedatabase.py b/aide_design/pipedatabase.py index e97e347b..6e25588b 100644 --- a/aide_design/pipedatabase.py +++ b/aide_design/pipedatabase.py @@ -1,11 +1,6 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Jun 8 18:20:38 2017 - -@author: Monroe Weber-Shirk +"""This file contains functions which convert between the nominal, inner, and +outer diameters of pipes based on their standard dimension ratio (SDR). -Last modified: Fri Jun 23 2017 -By: Sage Weber-Shirk """ #Let's begin to create the pipe database @@ -15,60 +10,55 @@ # We will use Pandas import pandas as pd # load the pipedb from a csv file - -import os.path + +import os.path dir_path = os.path.dirname(__file__) csv_path = os.path.join(dir_path, 'data/pipedatabase.csv') with open(csv_path) as pipedbfile: pipedb = pd.read_csv(pipedbfile) - @u.wraps(u.inch, u.inch, False) def OD(ND): """Return a pipe's outer diameter according to its nominal diameter. - + The pipe schedule is not required here because all of the pipes of a given nominal diameter have the same outer diameter. - + Steps: 1. Find the index of the closest nominal diameter. (Should this be changed to find the next largest ND?) 2. Take the values of the array, subtract the ND, take the absolute value, find the index of the minimium value. """ - index = (np.abs(np.array(pipedb['NDinch']) - - (ND))).argmin() + index = (np.abs(np.array(pipedb['NDinch']) - (ND))).argmin() return pipedb.iloc[index, 1] - +@u.wraps(u.inch, [u.inch, None], False) def ID_SDR(ND, SDR): """Return the inner diameter for SDR(standard diameter ratio) pipes. - - For these pipes the wall thickness is the outer diameter divided by + + For these pipes the wall thickness is the outer diameter divided by the SDR. """ - ID = OD(ND) * (SDR-2) / SDR - return ID + return OD(ND).magnitude * (SDR-2) / SDR @u.wraps(u.inch, u.inch, False) def ID_sch40(ND): - """Return the inner diameter for schedule 40 pipes. - + """Return the inner diameter for schedule 40 pipes. + The wall thickness for these pipes is in the pipedb. - - Take the values of the array, subtract the ND, take the absolute + + Take the values of the array, subtract the ND, take the absolute value, find the index of the minimium value. """ - myindex = (np.abs(np.array(pipedb['NDinch']) - - (ND)) - ).argmin() - return (pipedb.iloc[myindex, 1] - 2*(pipedb.iloc[myindex,5])) + myindex = (np.abs(np.array(pipedb['NDinch']) - (ND))).argmin() + return (pipedb.iloc[myindex, 1] - 2*(pipedb.iloc[myindex, 5])) def ND_all_available(): """Return an array of available nominal diameters. - - NDs available are those commonly used as based on the 'Used' column + + NDs available are those commonly used as based on the 'Used' column in the pipedb. """ ND_all_available = [] @@ -80,8 +70,8 @@ def ND_all_available(): def ID_SDR_all_available(SDR): """Return an array of inner diameters with a given SDR. - - IDs available are those commonly used based on the 'Used' column + + IDs available are those commonly used based on the 'Used' column in the pipedb. """ ID = [] @@ -91,23 +81,23 @@ def ID_SDR_all_available(SDR): return ID * u.inch -def ND_SDR_available(ID,SDR): +def ND_SDR_available(ID, SDR): """ Return an available ND given an ID and a schedule. - - Takes the values of the array, compares to the ID, and finds the index + + Takes the values of the array, compares to the ID, and finds the index of the first value greater or equal. """ for i in range(len(np.array(ID_SDR_all_available(SDR)))): if np.array(ID_SDR_all_available(SDR))[i] >= (ID.to(u.inch)).magnitude: return ND_all_available()[i] - + def ND_available(NDguess): """Return the minimum ND that is available. - + 1. Extract the magnitude in inches from the nominal diameter. 2. Find the index of the closest nominal diameter. - 3. Take the values of the array, subtract the ND, take the + 3. Take the values of the array, subtract the ND, take the absolute value, find the index of the minimium value. """ myindex = (ND_all_available() >= NDguess) diff --git a/aide_design/pipeline_utility.py b/aide_design/pipeline_utility.py index 190f5953..72772121 100644 --- a/aide_design/pipeline_utility.py +++ b/aide_design/pipeline_utility.py @@ -1,17 +1,17 @@ -""" -This utility is used to provide useful functions for all things related to pipeline -design. +"""This utility is used to provide useful functions for all things related to +pipeline design. + """ from aide_design.units import unit_registry as u from aide_design import physchem as pc -import aide_design.expert_inputs as exp +import aide_design.constants as con import aide_design.materials_database as mats import numpy as 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, - nu=exp.NU_WATER, pipe_rough=mats.PIPE_ROUGH_PVC): + nu=con.NU_WATER, pipe_rough=mats.PIPE_ROUGH_PVC): """ 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. @@ -47,4 +47,4 @@ def flow_pipeline(diameters: numpy.ndarray, lengths: numpy.ndarray, k_minors: nu # The flow should be reduced, and vice-versa. flow = flow + err * flow - return flow \ No newline at end of file + return flow diff --git a/aide_design/play.py b/aide_design/play.py index 4d23e00f..c57ad9bb 100644 --- a/aide_design/play.py +++ b/aide_design/play.py @@ -1,10 +1,11 @@ -""" -This module is intended to provide easy set up for an aide design playground/environment. +"""This module is intended to provide easy set up for an aide design playground +/environment. -It imports all commonly used aide packages with one line, ensures Python is run in the correct -virtual environment, sets sig figs correctly and provides any additional environment -massaging to get to designing as quickly as possible. This should NOT be used by other -modules within aide_design as it results in unnecessary imports. +It imports all commonly used aide packages with one line, ensures Python is +run in the correct virtual environment, sets sig figs correctly and provides +any additional environment massaging to get to designing as quickly as +possible. This should NOT be used by other modules within aide_design as it +results in unnecessary imports. Usage: @@ -14,9 +15,11 @@ Now you should be able to execute: *`np.array([1,2,3,4]) And your numbers should be limited to four significant figures when printed. + """ # Third-party imports +import math import numpy as np import pandas as pd import matplotlib.pyplot as plt @@ -27,13 +30,17 @@ import aide_design.pipedatabase as pipe from aide_design.units import unit_registry as u from aide_design import physchem as pc -import aide_design.expert_inputs as exp +import aide_design.constants as con import aide_design.materials_database as mat import aide_design.utility as ut import aide_design.k_value_of_reductions_utility as k import aide_design.pipeline_utility as pipeline +import aide_design.optional_inputs as opt import warnings +# deprecated imports +import aide_design.expert_inputs as exp + def setup_aide(): """ diff --git a/aide_design/unit_process_design/prefab/StaRS_prefab.py b/aide_design/unit_process_design/StaRS.py similarity index 100% rename from aide_design/unit_process_design/prefab/StaRS_prefab.py rename to aide_design/unit_process_design/StaRS.py diff --git a/aide_design/unit_process_design/prefab/cdc_prefab.py b/aide_design/unit_process_design/cdc.py similarity index 100% rename from aide_design/unit_process_design/prefab/cdc_prefab.py rename to aide_design/unit_process_design/cdc.py diff --git a/aide_design/unit_process_design/ent_tank.py b/aide_design/unit_process_design/ent_tank.py new file mode 100644 index 00000000..486c7e95 --- /dev/null +++ b/aide_design/unit_process_design/ent_tank.py @@ -0,0 +1,98 @@ +"""This file contains all the functions needed to design an entrance tank for +an AguaClara plant. + +""" +from aide_design.play import* + +@u.wraps(u.inch, [u.m**3/u.s, u.degK, u.m, None], False) +def drain_OD(q_plant, T, depth_end, SDR): + """Return the nominal diameter of the entrance tank drain pipe. Depth at the + end of the flocculator is used for headloss and length calculation inputs in + the diam_pipe calculation. + + Parameters + ---------- + q_plant: float + Plant flow rate + + T: float + Design temperature + + depth_end: float + The depth of water at the end of the flocculator + + SDR: float + Standard dimension ratio + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + ?? + """ + nu = pc.viscosity_kinematic(T) + K_minor = con.K_MINOR_PIPE_ENTRANCE + con.K_MINOR_PIPE_EXIT + con.K_MINOR_EL90 + drain_ID = pc.diam_pipe(q_plant, depth_end, depth_end, nu, mat.PIPE_ROUGH_PVC, K_minor) + drain_ND = pipe.ND_SDR_available(drain_ID, SDR) + return pipe.OD(drain_ND).magnitude + +@u.wraps(None, [u.m**3/u.s, u.m], False) +def num_plates_ET(q_plant, W_chan): + """Return the number of plates in the entrance tank. + + This number minimizes the total length of the plate settler unit. + + Parameters + ---------- + q_plant: float + Plant flow rate + + W_chan: float + Width of channel + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> num_plates_ET(20*u.L/u.s,2*u.m) + 1.0 + """ + num_plates = np.ceil(np.sqrt(q_plant/(con.DIST_CENTER_ENT_TANK_PLATE.magnitude + * W_chan * con.VEL_ENT_TANK_CAPTURE_BOD.magnitude * np.sin(con.AN_ENT_TANK_PLATE.to(u.rad).magnitude)))) + return num_plates + +@u.wraps(u.m, [u.m**3/u.s, u.m], False) +def L_plate_ET(q_plant, W_chan): + """Return the length of the plates in the entrance tank. + + Parameters + ---------- + q_plant: float + Plant flow rate + + W_chan: float + Width of channel + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> L_plate_ET(20*u.L/u.s,2*u.m) + 0.00194 + """ + L_plate = (q_plant/(num_plates_ET(q_plant, W_chan) * W_chan * + con.VEL_ENT_TANK_CAPTURE_BOD.magnitude * np.cos(con.AN_ENT_TANK_PLATE.to(u.rad).magnitude))) + - (con.SPACE_ENT_TANK_PLATE.magnitude * np.tan(con.AN_ENT_TANK_PLATE.to(u.rad).magnitude)) + return L_plate diff --git a/aide_design/unit_process_design/ent_tank_dict_test.py b/aide_design/unit_process_design/ent_tank_dict_test.py new file mode 100644 index 00000000..7acbeda8 --- /dev/null +++ b/aide_design/unit_process_design/ent_tank_dict_test.py @@ -0,0 +1,156 @@ +"""This file contains all the functions needed to design an entrance tank for +an AguaClara plant. + +Attributes +---------- +sdr : int + Ratio between outer diameter and wall thickness + +S_plate : float + Edge to edge distance between plates in the plate settler module + +angle_plate : float + Angle of plates in the plate settler module + +vel_capture : floats + Design capture velocity + +L_max : float + Maximum length of the entire entrance tank + +thickness_plate : float + thickness of plates in the plate settelr module + +""" +from aide_design.play import* + +# document what's in ent_tank_dict here +ent_tank_dict = {'sdr': 26, 'S_plate': 2.5*u.cm, 'angle_plate': 50*u.deg, + 'vel_capture': 8 * u.mm/u.s, 'L_max': 2.2*u.m, + 'thickness_plate': 2*u.mm} + +@u.wraps(u.inch, [u.m**3/u.s, u.degK, u.m, None], False) +def drain_OD(q_plant, temp, depth_end, ent_tank_inputs=ent_tank_dict): + """Return the outer diameter of the entrance tank drain pipe. Depth at the + end of the flocculator is used for headloss and length calculation inputs in + the diam_pipe calculation. + + Parameters + ---------- + q_plant : float + Plant flow rate + + temp: float + Design temperature + + depth_end: float + The depth of water at the end of the flocculator + + Returns + ------- + float + the outer diameter of the entrance tank drain pipe + + Examples + -------- + >>> from aide_design.play import* + >>> ??? + """ + nu = pc.viscosity_kinematic(temp) + K_minor = (con.K_MINOR_PIPE_ENTRANCE + con.K_MINOR_PIPE_EXIT + con.K_MINOR_EL90) + drain_ID = pc.diam_pipe(q_plant, depth_end, depth_end, nu, mat.PIPE_ROUGH_PVC, K_minor) + drain_ND = pipe.ND_SDR_available(drain_ID, ent_tank_inputs['sdr']) + return pipe.OD(drain_ND) + +@u.wraps(None, [u.m**3/u.s, u.m, None], False) +def num_plates_ent_tank(q_plant, W_chan, ent_tank_inputs=ent_tank_dict): + """Return the number of plates in the entrance tank. + + This number minimizes the total length of the plate settler unit. + + Parameters + ---------- + q_plant : float + Plant flow rate + + W_chan : float + Width of channel + + Returns + ------- + float + the number of plates in the entrance tank + + Examples + -------- + >>> from aide_design.play import* + >>> ??? + """ + B_plate = ent_tank_inputs['S_plate'] + ent_tank_inputs['thickness_plate'] + N_plates = np.ceil(np.sqrt(q_plant/B_plate.to(u.m).magnitude + * W_chan * ent_tank_inputs['vel_capture'].to(u.m/u.s).magnitude * + np.sin(ent_tank_inputs['angle_plate'].to(u.rad).magnitude))) + return N_plates + +@u.wraps(u.m, [u.m**3/u.s, u.m, None], False) +def L_plate_ent_tank(q_plant, W_chan, ent_tank_inputs=ent_tank_dict): + """Return the length of the plates in the entrance tank. + + Parameters + ---------- + q_plant : float + Plant flow rate + + W_chan : float + Width of channel + + Returns + ------- + float + the length of the plates in the entrance tank + + Examples + -------- + >>> from aide_design.play import* + >>> ??? + """ + L_plate = ((q_plant/(num_plates_ent_tank(q_plant, W_chan) * W_chan * + ent_tank_inputs['vel_capture'].to(u.m/u.s).magnitude * + np.cos(ent_tank_inputs['angle_plate'].to(u.rad).magnitude))) + - (ent_tank_inputs['S_plate'].to(u.m).magnitude * + np.tan(ent_tank_inputs['angle_plate'].to(u.rad).magnitude))) + return L_plate + +@u.wraps([u.inch, None, u.m], [u.m**3/u.s, u.degK, u.m, u.m, None], False) +def ent_tank_agg(q_plant, temp, depth_end, W_chan, ent_tank_inputs=ent_tank_dict): +""" +Parameters +---------- +q_plant : float + Plant flow rate + +temp: float + Design temperature + +depth_end: float + The depth of water at the end of the flocculator + +W_chan: float + The width of the channel + +Returns +------- +float + ? + +Examples +-------- +>>> from aide_design.play import* +>>> ??? +"""" + OD_drain = drain_OD(q_plant, temp, depth_end, ent_tank_inputs).magnitude + N_plates = num_plates_ent_tank(q_plant, W_chan, ent_tank_inputs).magnitude + L_plate = L_plate_ent_tank(q_plant, W_chan, ent_tank_inputs).magnitude + return ent_tank_inputs.update({'OD_drain': OD_drain, 'N_plates': N_plates, + 'L_plate': L_plate}) diff --git a/aide_design/unit_process_design/prefab/__init__.py b/aide_design/unit_process_design/filter_dict_test.py similarity index 100% rename from aide_design/unit_process_design/prefab/__init__.py rename to aide_design/unit_process_design/filter_dict_test.py diff --git a/aide_design/unit_process_design/floc.py b/aide_design/unit_process_design/floc.py new file mode 100644 index 00000000..196b2388 --- /dev/null +++ b/aide_design/unit_process_design/floc.py @@ -0,0 +1,446 @@ +"""This file contains all the functions needed to design a flocculator for +an AguaClara plant. + +""" +from aide_design.play import* + +# expansion minor loss coefficient for 180 degree bend +K_e = (1 / con.RATIO_VC_ORIFICE**2 - 1)**2 + +@u.wraps(1/u.s, [u.m, None, u.degK], False) +def G_avg(hl, Gt, T): + """Return the average velocity gradient of a flocculator given head + loss, collision potential and temperature. + + Parameters + ---------- + hl: float + Headloss through the flocculator + + Gt: float + Target collision potential + + T: float + Design temperature + + Returns + ------- + ? + + Examples + -------- + >>> from aide_design.play import* + >>>G_avg(40*u.cm, 37000, 25*u.degC) + 118.715 1/second + """ + G = (pc.gravity.magnitude * hl) / (Gt * pc.viscosity_kinematic(T).magnitude) + return G + +@u.wraps(u.m**3, [u.m**3/u.s, u.m, None, u.degK], False) +def vol_floc(q_plant, hl, Gt, T): + """Return the total volume of the flocculator using plant flow rate, head + loss, collision potential and temperature. + + Uses an estimation of flocculator residence time (ignoring the decrease + in water depth caused by head loss in the flocculator.) Volume does not take + into account the extra volume that the flocculator will have due to changing + water level caused by head loss. + + Parameters + ---------- + q_plant: float + Plant flow rate + + hl: float + Headloss through the flocculator + + Gt: float + Target collision potential + + T: float + Design temperature + + Returns + ------- + ? + + Examples + -------- + >>> from aide_design.play import* + >>>vol_floc(20*u.L/u.s, 40*u.cm, 37000, 25*u.degC) + 6.233 meter3 + """ + vol = (Gt / G_avg(hl, Gt, T).magnitude)*q_plant + return vol + +@u.wraps(u.cm, [u.m**3/u.s, u.m, None, u.degK, u.m], False) +def width_HS_min(q_plant, hl, Gt, T, depth_end): + """Return the minimum channel width required to achieve H/S > 3. + + The channel can be wider than this, but this is the absolute minimum width + for a channel. The minimum width occurs when there is only one expansion per + baffle and thus the distance between expansions is the same as the depth of + water at the end of the flocculator. + + Parameters + ---------- + q_plant: float + Plant flow rate + + hl: float + Headloss through the flocculator + + Gt: float + Target collision potential + + T: float + Design temperature + + depth_end: float + The depth of water at the end of the flocculator + + Returns + ------- + float + The minimum channel width required to achieve H/S > 3 + + Examples + -------- + >>> from aide_design.play import* + >>> width_HS_min(20*u.L/u.s, 40*u.cm, 37000, 25*u.degC, 2*u.m) + 0.1074 centimeter + """ + nu = pc.viscosity_kinematic(T).magnitude + + w = con.RATIO_HS_MIN*((K_e/(2 * depth_end * (G_avg(hl, Gt, T).magnitude**2) + * nu))**(1/3))*q_plant/depth_end + return w + + + +@u.wraps(u.cm, [u.m**3/u.s, u.m, None, u.degK, u.m], False) +def width_floc_min(q_plant, hl, Gt, T, depth_end): + """Return the minimum channel width required. + + This takes the maximum of the minimum required to achieve H/S > 3 and the + minimum required for constructability based on the width of the human hip. + + Parameters + ---------- + q_plant: float + Plant flow rate + + hl: float + Headloss through the flocculator + + Gt: float + Target collision potential + + T: float + Design temperature + + depth_end: float + The depth of water at the end of the flocculator + + Returns + ------- + float + The minimum channel width required to achieve H/S > 3 + + Examples + -------- + >>> from aide_design.play import* + >>> width_floc_min(20*u.L/u.s, 40*u.cm, 37000, 25*u.degC, 2*u.m) + 45 centimeter + """ + return max(width_HS_min(q_plant, hl, Gt, T, depth_end).magnitude, con.FLOC_WIDTH_MIN_CONST.magnitude) + +@u.wraps(None, [u.m**3/u.s, u.m, None, u.degK, u.m, u.m], False) +def num_channel(q_plant, hl, Gt, T, W_tot, depth_end): + """Return the number of channels in the entrance tank/flocculator (ETF). + + This takes the total width of the flocculator and divides it by the minimum + channel width. A floor function is used to ensure that there are an even + number of channels. + + Parameters + ---------- + q_plant: float + Plant flow rate + + hl: float + Headloss through the flocculator + + Gt: float + Target collision potential + + T: float + Design temperature + + W_tot: float + Total width + + depth_end: float + The depth of water at the end of the flocculator + + Returns + ------- + ? + + Examples + -------- + >>> from aide_design.play import* + >>> num_channel(20*u.L/u.s, 40*u.cm, 37000, 25*u.degC, 20*u.m, 2*u.m) + 2 + """ + num = W_tot/(width_floc_min(q_plant, hl, Gt, T, depth_end).magnitude) + # floor function with step size 2 + num = np.floor(num/2)*2 + return int(max(num, 2)) + + +@u.wraps(u.m**2, [u.m**3/u.s, u.m, None, u.degK, u.m], False) +def area_ent_tank(q_plant, hl, Gt, T, depth_end): + """Return the planview area of the entrance tank given plant flow rate, + headloss, target collision potential, design temperature, and depth of + water at the end of the flocculator. + + Parameters + ---------- + q_plant: float + Plant flow rate + + hl: float + Headloss through the flocculator + + Gt: float + Target collision potential + + T: float + Design temperature + + depth_end: float + The depth of water at the end of the flocculator + + Returns + ------- + float + The planview area of the entrance tank + + Examples + -------- + >>> from aide_design.play import* + >>> area_ent_tank(20*u.L/u.s, 40*u.cm, 37000, 25*u.degC, 2*u.m) + 1 meter ** 2 + """ + # guess the planview area before starting iteration + A_new = 1*u.m**2 + A_ratio = 0 + + while (A_ratio) > 1.01 and (A_ET_PV/A_new) < 0.99: + A_ET_PV = A_new + + vol_floc = vol_floc(q_plant, hl, Gt, T) + A_floc_PV = vol_floc/(depth_end + hl/2) + A_ETF_PV = A_ET_PV + A_floc_PV + + W_min = width_floc_min(q_plant, hl, Gt, T, depth_end) + + W_tot = A_ETF_PV/opt.L_sed + + num_chan = num_channel(q_plant, hl, Gt, T, W_tot) + W_chan = W_tot/num_chan + + A_new = opt.L_ET_max*W_chan + + A_ratio = A_new/A_ET_PV + + return A_new.to(u.m**2).magnitude + +### Baffle calculations +@u.wraps(u.m, [u.m**3/u.s, u.m, None, u.degK, u.m], False) +def exp_dist_max(q_plant, hl, Gt, T, W_chan): + """"Return the maximum distance between expansions for the largest + allowable H/S ratio. + + Parameters + ---------- + q_plant: float + Plant flow rate + + hl: float + Headloss through the flocculator + + Gt: float + Target collision potential + + T: float + Design temperature + + W_chan: float + Channel width + + Returns + ------- + ? + + Examples + -------- + >>> from aide_design.play import* + >>> exp_dist_max(20*u.L/u.s, 40*u.cm, 37000, 25*u.degC, 2*u.m) + 0.375 meter + """ + g_avg = G_avg(hl, Gt, T).magnitude + nu = pc.viscosity_kinematic(T).magnitude + term1 = (K_e/(2 * (g_avg**2) * nu))**(1/4) + term2 = (con.RATIO_HS_MAX*q_plant/W_chan)**(3/4) + exp_dist_max = term1*term2 + return exp_dist_max + +@u.wraps(None, [u.m**3/u.s, u.m, None, u.degK, u.m], False) +def num_expansions(q_plant, hl, Gt, T, depth_end): + """"Return the minimum number of expansions per baffle space. + + Parameters + ---------- + q_plant: float + Plant flow rate + + hl: float + Headloss through the flocculator + + Gt: float + Target collision potential + + T: float + Design temperature + + depth_end: float + The depth of water at the end of the flocculator + + Returns + ------- + ? + + Examples + -------- + >>> from aide_design.play import* + ??? + """ + return int(np.ceil(depth_end/(exp_dist_max(q_plant, hl, Gt, T)).magnitude)) + +@u.wraps(u.m, [u.m**3/u.s, u.m, None, u.degK, u.m], False) +def height_exp(q_plant, hl, Gt, T, depth_end): + """Return the actual distance between expansions given the integer + requirement for the number of expansions per flocculator depth. + + Parameters + ---------- + q_plant: float + Plant flow rate + + hl: float + Headloss through the flocculator + + Gt: float + Target collision potential + + T: float + Design temperature + + depth_end: float + The depth of water at the end of the flocculator + + Returns + ------- + ? + + Examples + -------- + >>> from aide_design.play import* + ??? + """ + return depth_end/num_expansions(q_plant, hl, Gt, T) + +@u.wraps(u.m, [u.m**3/u.s, u.m, None, u.degK, u.m], False) +def baffle_spacing(q_plant, hl, Gt, T, W_chan): + """Return the spacing between baffles based on the target velocity gradient + + Parameters + ---------- + q_plant: float + Plant flow rate + + hl: float + Headloss through the flocculator + + Gt: float + Target collision potential + + T: float + Design temperature + + W_chan: float + Channel width + + Returns + ------- + ? + + Examples + -------- + >>> from aide_design.play import* + >>> baffle_spacing(20*u.L/u.s, 40*u.cm, 37000, 25*u.degC, 2*u.m) + 0.063 meter + .""" + g_avg = G_avg(hl, Gt, T).magnitude + nu = pc.viscosity_kinematic(T).magnitude + term1 = (K_e/(2 * exp_dist_max(q_plant, hl, Gt, T, W_chan).magnitude * (g_avg**2) * nu))**(1/3) + return term1 * q_plant/W_chan +baffle_spacing(20*u.L/u.s, 40*u.cm, 37000, 25*u.degC, 2*u.m) + +@u.wraps(None, [u.m**3/u.s, u.m, None, u.degK, u.m, u.m, u.m], False) +def num_baffles(q_plant, hl, Gt, T, W_chan, L, baffle_thickness): + """Return the number of baffles that would fit in the channel given the + channel length and spacing between baffles. + + Parameters + ---------- + q_plant: float + Plant flow rate + + hl: float + Headloss through the flocculator + + Gt: float + Target collision potential + + T: float + Design temperature + + W_chan: float + Channel width + + L: float + Length + + baffle_thickness: float + Baffle thickness + + Returns + ------- + ? + + Examples + -------- + >>> from aide_design.play import* + >>> num_baffles(20*u.L/u.s, 40*u.cm, 37000, 25*u.degC, 2*u.m, 2*u.m, 2*u.m) + 0 + >>> num_baffles(20*u.L/u.s, 20*u.cm, 37000, 25*u.degC, 2*u.m, 2*u.m, 21*u.m) + -1 + """ + num = round((L / (baffle_spacing(q_plant, hl, Gt, T, W_chan).magnitude + baffle_thickness))) + # the one is subtracted because the equation for num gives the number of + # baffle spaces and there is always one less baffle than baffle spaces due + # to geometry + return int(num) - 1 diff --git a/aide_design/unit_process_design/floc_dict_test.py b/aide_design/unit_process_design/floc_dict_test.py new file mode 100644 index 00000000..e321f77f --- /dev/null +++ b/aide_design/unit_process_design/floc_dict_test.py @@ -0,0 +1,551 @@ +"""This file contains all the functions needed to design a flocculator for +an AguaClara plant. + +Attributes +---------- +L_ent_tank_max : float + The maximum length of the entrance tank + +L_sed : float + The length of the sedimentation tank + +hl : float + Headloss through the flocculator + +coll_pot : int + Desired collision potential in the flocculator + +freeboard: float + The height between the water and top of the flocculator channels + +ratior_HS_min : int + Minimum allowable ratio between the water depth and edge to edge distance + between baffles + +ratio_HS_max : int + Maximum allowable ratio between the water depth and edge to edge distance + between baffles + +W_min_construct : float + Minimum width of a flocculator channel based on the width of the human hip + +K_minor : float + Minor loss coefficient used in flocculator design + +baffle_thickness : float + Thickness of a baffle + +""" +from aide_design.play import* + +floc_dict = {'L_ent_tank_max': 2.2*u.m, 'baffle_thickness' : 15*u.cm, + 'L_sed': 5.8*u.m, 'hl': 40*u.cm, 'coll_pot': 37000, + 'freeboard': 10*u.cm, 'ratio_HS_min': 3, 'ratio_HS_max': 6, + 'W_min_construct': 45*u.cm, 'K_minor': 2.31} + +@u.wraps(1/u.s, [u.degK, None], False) +def G_avg(temp, floc_inputs=floc_dict): + """Return the average velocity gradient of a flocculator given head + loss, collision potential and temperature. + + Parameters + ---------- + temp : float + Design temperature + + floc_inputs : dict + a dictionary of all of the constant inputs needed for flocculator + calculations + + Returns + ------- + float + average velocity gradient of a flocculator given head + loss, collision potential and temperature. + + Examples + -------- + >>> from aide_design.play import* + >>> floc_dict = {'L_ent_tank_max': 2.2*u.m, 'baffle_thickness' : 15*u.cm, + ... 'L_sed': 5.8*u.m, 'hl': 40*u.cm, 'coll_pot': 37000, + ... 'freeboard': 10*u.cm, 'ratio_HS_min': 3, 'ratio_HS_max': 6, + ... 'W_min_construct': 45*u.cm, 'K_minor': 2.31} + >>> G_avg(15 * u.degC) + 93.24255814245437 1/second + >>> G_avg(20 * u.degC) + 105.64226282862515 1/second + + """ + G = ((pc.gravity.magnitude * floc_inputs['hl'].to(u.m).magnitude) / + (floc_inputs['coll_pot'] * pc.viscosity_kinematic(temp).magnitude)) + return G + +@u.wraps(u.m**3, [u.m**3/u.s, u.degK, None], False) +def vol_floc(Q_plant, temp, floc_inputs=floc_dict): + """Return the total volume of the flocculator using plant flow rate, head + loss, collision potential and temperature. + + Uses an estimation of flocculator residence time (ignoring the decrease + in water depth caused by head loss in the flocculator.) Volume does not take + into account the extra volume that the flocculator will have due to changing + water level caused by head loss. + + Parameters + ---------- + Q_plant: float + Flow through the plant + + temp: float + Design temperature + + floc_inputs : dict + a dictionary of all of the constant inputs needed for flocculator + calculations + + Returns + ------- + float + total volume of the flocculator given head + loss, collision potential and temperature. + + Examples + -------- + >>> from aide_design.play import* + >>> floc_dict = {'L_ent_tank_max': 2.2*u.m, 'baffle_thickness' : 15*u.cm, + ... 'L_sed': 5.8*u.m, 'hl': 40*u.cm, 'coll_pot': 37000, + ... 'freeboard': 10*u.cm, 'ratio_HS_min': 3, 'ratio_HS_max': 6, + ... 'W_min_construct': 45*u.cm, 'K_minor': 2.31} + >>> vol_floc(40*u.L/u.s, 15*u.degC) + 15.872580391229524 meter3 + >>> vol_floc(40*u.L/u.s, 20*u.degC) + 14.009544668698396 meter3 + + """ + vol = (floc_dict['coll_pot'] / G_avg(temp, floc_inputs).magnitude)*Q_plant + return vol + +@u.wraps(u.cm, [u.m**3/u.s, u.degK, u.m, None], False) +def width_HS_min(Q_plant, temp, depth_end, floc_inputs=floc_dict): + """Return the minimum channel width required to achieve H/S > 3. + + The channel can be wider than this, but this is the absolute minimum width + for a channel. The minimum width occurs when there is only one expansion per + baffle and thus the distance between expansions is the same as the depth of + water at the end of the flocculator. + + Parameters + ---------- + Q_plant : float + Plant flow rate + + temp: float + Design temperature + + depth_end: float + The depth of water at the end of the flocculator + + floc_inputs : dict + a dictionary of all of the constant inputs needed for flocculator + calculations + + Returns + ------- + float + The minimum channel width required to achieve H/S > 3 + + Examples + -------- + >>> from aide_design.play import* + >>> floc_dict = {'L_ent_tank_max': 2.2*u.m, 'baffle_thickness' : 15*u.cm, + ... 'L_sed': 5.8*u.m, 'hl': 40*u.cm, 'coll_pot': 37000, + ... 'freeboard': 10*u.cm, 'ratio_HS_min': 3, 'ratio_HS_max': 6, + ... 'W_min_construct': 45*u.cm, 'K_minor': 2.31} + >>> width_HS_min(20*u.L/u.s, 25*u.degC, 2*u.m, floc_dict) + 0.10740157183590993 centimeter + >>> width_HS_min(40*u.L/u.s, 15*u.degC, 5*u.m, floc_dict) + 0.06861475664688545 centimeter + + """ + nu = pc.viscosity_kinematic(temp).magnitude + + w = (floc_inputs['ratio_HS_min'] * + ((floc_inputs['K_minor'] / + (2 * depth_end * (G_avg(temp, floc_inputs).magnitude**2) + * nu))**(1/3))*Q_plant/depth_end) + return w + +@u.wraps(u.cm, [u.m**3/u.s, u.degK, u.m, None], False) +def width_floc_min(Q_plant, temp, depth_end, floc_inputs=floc_dict): + """Return the minimum channel width required. + + This takes the maximum of the minimum required to achieve H/S > 3 and the + minimum required for constructability based on the width of the human hip. + + Parameters + ---------- + Q_plant : float + Plant flow rate + + temp: float + Design temperature + + depth_end: float + The depth of water at the end of the flocculator + + floc_inputs : dict + a dictionary of all of the constant inputs needed for flocculator + calculations + + Returns + ------- + float + minimum channel width required. + + Examples + -------- + >>> from aide_design.play import* + >>> floc_dict = {'L_ent_tank_max': 2.2*u.m, 'baffle_thickness' : 15*u.cm, + ... 'L_sed': 5.8*u.m, 'hl': 40*u.cm, 'coll_pot': 37000, + ... 'freeboard': 10*u.cm, 'ratio_HS_min': 3, 'ratio_HS_max': 6, + ... 'W_min_construct': 45*u.cm, 'K_minor': 2.31} + >>> width_floc_min(20*u.L/u.s, 25*u.degC, 2*u.m, floc_dict) + 45 centimeter + >>> width_floc_min(40*u.L/u.s, 15*u.degC, 2*u.m, floc_dict) + 45 centimeter + + """ + return max(width_HS_min(Q_plant, temp, depth_end, floc_inputs).magnitude, + floc_inputs['W_min_construct'].magnitude) + +@u.wraps(None, [u.m**3/u.s, u.degK, u.m, u.cm, None], False) +def num_channel(Q_plant, temp, depth_end, W_tot, floc_inputs=floc_dict): + """Return the number of channels in the entrance tank/flocculator (ETF). + + This takes the total width of the flocculator and divides it by the minimum + channel width. A floor function is used to ensure that there are an even + number of channels. + + Parameters + ---------- + Q_plant : float + Plant flow rate + + temp: float + Design temperature + + depth_end: float + The depth of water at the end of the flocculator + + W_tot: float + Total width + + floc_inputs : dict + a dictionary of all of the constant inputs needed for flocculator + calculations + + Returns + ------- + int + the number of channels in the entrance tank/flocculator (ETF) + + Examples + -------- + >>> from aide_design.play import* + >>> from aide_design.play import* + >>> floc_dict = {'L_ent_tank_max': 2.2*u.m, 'baffle_thickness' : 15*u.cm, + ... 'L_sed': 5.8*u.m, 'hl': 40*u.cm, 'coll_pot': 37000, + ... 'freeboard': 10*u.cm, 'ratio_HS_min': 3, 'ratio_HS_max': 6, + ... 'W_min_construct': 45*u.cm, 'K_minor': 2.31} + >>> num_channel(20*u.L/u.s, 25*u.degC, 2*u.m, 5*u.m, floc_dict) + 10 + >>> num_channel(40*u.L/u.s, 15*u.degC, 4*u.m, 10*u.m, floc_dict) + 22 + + """ + num = W_tot/(width_floc_min(Q_plant, temp, depth_end, floc_inputs).magnitude) + # floor function with step size 2 + num = np.floor(num/2)*2 + return int(max(num, 2)) + +@u.wraps(u.m**2, [u.m**3/u.s, u.degK, u.m, None], False) +def area_ent_tank(Q_plant, temp, depth_end, floc_inputs=floc_dict): + """Return the planview area of the entrance tank given plant flow rate, + headloss, target collision potential, design temperature, and depth of + water at the end of the flocculator. + + Parameters + ---------- + Q_plant: float + Plant flow rate + + temp: float + Design temperature + + depth_end: float + The depth of water at the end of the flocculator + + floc_inputs : dict + a dictionary of all of the constant inputs needed for flocculator + calculations + + Returns + ------- + float + The planview area of the entrance tank + + Examples + -------- + >>> from aide_design.play import* + >>> floc_dict = {'L_ent_tank_max': 2.2*u.m, 'baffle_thickness' : 15*u.cm, + ... 'L_sed': 5.8*u.m, 'hl': 40*u.cm, 'coll_pot': 37000, + ... 'freeboard': 10*u.cm, 'ratio_HS_min': 3, 'ratio_HS_max': 6, + ... 'W_min_construct': 45*u.cm, 'K_minor': 2.31} + >>> area_ent_tank(20*u.L/u.s, 25*u.degC, 2*u.m) + 1 meter ** 2 + >>> area_ent_tank(40*u.L/u.s, 15*u.degC, 2*u.m) + 1 meter2 + """ + # guess the planview area before starting iteration + A_new = 1*u.m**2 + A_ratio = 0 + + while (A_ratio) > 1.01 and (A_ET_PV/A_new) < 0.99: + A_ET_PV = A_new + + vol_floc = vol_floc(Q_plant, temp, floc_inputs) + A_floc_PV = vol_floc/(depth_end + hl/2) + A_ETF_PV = A_ET_PV + A_floc_PV + + W_min = width_floc_min(Q_plant, temp, depth_end, floc_inputs) + + W_tot = A_ETF_PV/floc_inputs['L_sed'] + + num_chan = num_channel(Q_plant, temp, depth_end, W_tot, floc_inputs) + W_chan = W_tot/num_chan + + A_new = floc_inputs['L_ent_tank_max']*W_chan + + A_ratio = A_new/A_ET_PV + + return A_new.to(u.m**2).magnitude + +### Baffle calculations +@u.wraps(u.m, [u.m**3/u.s, u.degK, u.m, None], False) +def expansion_dist_max(Q_plant, temp, W_chan, floc_inputs=floc_dict): + """"Return the maximum distance between expansions for the largest + allowable H/S ratio. + + Parameters + ---------- + Q_plant: float + Plant flow rate + + temp: float + Design temperature + + W_chan: float + Channel width + + floc_inputs : dict + a dictionary of all of the constant inputs needed for flocculator + calculations + + Returns + ------- + float + maximum distance between expansions for the largest + allowable H/S ratio. + + Examples + -------- + >>> from aide_design.play import* + >>> floc_dict = {'L_ent_tank_max': 2.2*u.m, 'baffle_thickness' : 15*u.cm, + ... 'L_sed': 5.8*u.m, 'hl': 40*u.cm, 'coll_pot': 37000, + ... 'freeboard': 10*u.cm, 'ratio_HS_min': 3, 'ratio_HS_max': 6, + ... 'W_min_construct': 45*u.cm, 'K_minor': 2.31} + >>> expansion_dist_max(20*u.L/u.s, 15*u.degC, 0.45*u.m) + 1.2200391430074593 meter + >>> expansion_dist_max(40*u.L/u.s, 25*u.degC, 0.45*u.m) + 1.931628399157619 meter + """ + g_avg = G_avg(temp, floc_inputs).magnitude + nu = pc.viscosity_kinematic(temp).magnitude + term1 = (floc_inputs['K_minor']/(2 * (g_avg**2) * nu))**(1/4) + term2 = (floc_inputs['ratio_HS_max']*Q_plant/W_chan)**(3/4) + exp_dist_max = term1*term2 + return exp_dist_max + +@u.wraps(None, [u.m**3/u.s, u.degK, u.m, u.m, None], False) +def num_expansions(Q_plant, temp, depth_end, W_chan, floc_inputs=floc_dict): + """"Return the minimum number of expansions per baffle space. + + Parameters + ---------- + Q_plant: float + Plant flow rate + + temp: float + Design temperature + + depth_end: float + The depth of water at the end of the flocculator + + W_chan: float + Channel width + + floc_inputs : dict + a dictionary of all of the constant inputs needed for flocculator + calculations + + Returns + ------- + int + minimum number of expansions per baffle space + + Examples + -------- + >>> from aide_design.play import* + >>> floc_dict = {'L_ent_tank_max': 2.2*u.m, 'baffle_thickness' : 15*u.cm, + ... 'L_sed': 5.8*u.m, 'hl': 40*u.cm, 'coll_pot': 37000, + ... 'freeboard': 10*u.cm, 'ratio_HS_min': 3, 'ratio_HS_max': 6, + ... 'W_min_construct': 45*u.cm, 'K_minor': 2.31} + >>> num_expansions(20*u.L/u.s, 15*u.degC, 2*u.m, 0.45*u.m) + 2 + >>> num_expansions(40*u.L/u.s, 25*u.degC, 4*u.m, 0.45*u.m) + 3 + + """ + return int(np.ceil(depth_end / + (expansion_dist_max(Q_plant, temp, W_chan, floc_inputs)).magnitude)) + +@u.wraps(u.m, [u.m**3/u.s, u.degK, u.m, u.m, None], False) +def height_exp(Q_plant, temp, depth_end, W_chan, floc_inputs=floc_dict): + """Return the actual distance between expansions given the integer + requirement for the number of expansions per flocculator depth. + + Parameters + ---------- + Q_plant: float + Plant flow rate + + temp: float + Design temperature + + depth_end: float + The depth of water at the end of the flocculator + + W_chan: float + Channel width + + floc_inputs : dict + a dictionary of all of the constant inputs needed for flocculator + calculations + + Returns + ------- + float + the actual distance between expansions + + Examples + -------- + >>> from aide_design.play import* + >>> floc_dict = {'L_ent_tank_max': 2.2*u.m, 'baffle_thickness' : 15*u.cm, + ... 'L_sed': 5.8*u.m, 'hl': 40*u.cm, 'coll_pot': 37000, + ... 'freeboard': 10*u.cm, 'ratio_HS_min': 3, 'ratio_HS_max': 6, + ... 'W_min_construct': 45*u.cm, 'K_minor': 2.31} + >>> height_exp(20*u.L/u.s, 15*u.degC, 2*u.m, 0.45*u.m) + 1.0 meter + >>> height_exp(40*u.L/u.s, 25*u.degC, 4*u.m, 0.45*u.m) + 1.3333333333333333 meter + + """ + return depth_end/num_expansions(Q_plant, temp, depth_end, W_chan, floc_inputs) + +@u.wraps(u.m, [u.m**3/u.s, u.degK, u.m, None], False) +def baffle_spacing(Q_plant, temp, W_chan, floc_inputs=floc_dict): + """Return the spacing between baffles based on the target velocity gradient + + Parameters + ---------- + Q_plant: float + Plant flow rate + + temp: float + Design temperature + + W_chan: float + Channel width + + floc_inputs : dict + a dictionary of all of the constant inputs needed for flocculator + calculations + + Returns + ------- + float + the spacing between baffles + + Examples + -------- + >>> from aide_design.play import* + >>> floc_dict = {'L_ent_tank_max': 2.2*u.m, 'baffle_thickness' : 15*u.cm, + ... 'L_sed': 5.8*u.m, 'hl': 40*u.cm, 'coll_pot': 37000, + ... 'freeboard': 10*u.cm, 'ratio_HS_min': 3, 'ratio_HS_max': 6, + ... 'W_min_construct': 45*u.cm, 'K_minor': 2.31} + >>> baffle_spacing(40*u.L/u.s, 25*u.degC, 0.45*u.m) + 0.3219380665262699 meter + >>> baffle_spacing(20*u.L/u.s, 15*u.degC, 0.45*u.m) + 0.2033398571679099 meter + """ + g_avg = G_avg(temp, floc_inputs).magnitude + nu = pc.viscosity_kinematic(temp).magnitude + term1 = (floc_inputs['K_minor']/(2 * expansion_dist_max(Q_plant, temp, W_chan, floc_inputs).magnitude * (g_avg**2) * nu))**(1/3) + return term1 * Q_plant/W_chan + +@u.wraps(None, [u.m**3/u.s, u.degK, u.m, u.m, None], False) +def num_baffles(Q_plant, temp, W_chan, L, floc_inputs=floc_dict): + """Return the number of baffles that would fit in the channel given the + channel length and spacing between baffles. + + Parameters + ---------- + Q_plant: float + Plant flow rate + + temp: float + Design temperature + + W_chan: float + Channel width + + L : float + Length of the flocculator + + floc_inputs : dict + a dictionary of all of the constant inputs needed for flocculator + calculations + + Returns + ------- + int + the number of baffles that would fit in the channel + + Examples + -------- + >>> from aide_design.play import* + >>> floc_dict = {'L_ent_tank_max': 2.2*u.m, 'baffle_thickness' : 15*u.cm, + ... 'L_sed': 5.8*u.m, 'hl': 40*u.cm, 'coll_pot': 37000, + ... 'freeboard': 10*u.cm, 'ratio_HS_min': 3, 'ratio_HS_max': 6, + ... 'W_min_construct': 45*u.cm, 'K_minor': 2.31} + >>> num_baffles(20*u.L/u.s, 15*u.degC, 0.45*u.m, 6*u.m) + 16 + >>> num_baffles(40*u.L/u.s, 25*u.degC, 0.45*u.m, 6*u.m) + 12 + + """ + N = round(L / (baffle_spacing(Q_plant, temp, W_chan, floc_inputs).magnitude + + floc_inputs['baffle_thickness'].to(u.m).magnitude)) + # the one is subtracted because the equation for num gives the number of + # baffle spaces and there is always one less baffle than baffle spaces due + # to geometry + return int(N) - 1 diff --git a/aide_design/unit_process_design/lfom.py b/aide_design/unit_process_design/lfom.py new file mode 100644 index 00000000..5415fec1 --- /dev/null +++ b/aide_design/unit_process_design/lfom.py @@ -0,0 +1,210 @@ + +"""This file contains all the functions needed to design the linear flow +orifice meter (LFOM) for an AguaClara plant. + +""" + +#Here we import packages that we will need for this notebook. You can find out about these packages in the Help menu. +from aide_design.play import* + +#primary outputs from this file are +#Nominal diameter nom_diam_lfom_pipe(FLOW,HL_LFOM,con.RATIO_LFOM_SAFETY) +#number of rows n_lfom_rows(FLOW,HL_LFOM) +#orifice diameter orifice_diameter(FLOW,HL_LFOM,drill_series_uom) +#number of orifices in each row n_lfom_orifices(FLOW,HL_LFOM,drill_series_uom) +#height of the center of each row height_lfom_orifices(FLOW,HL_LFOM,drill_series_uom) + +# output is width per flow rate. +@u.wraps(u.s/(u.m**2), [u.m,u.m], False) +def width_stout(HL_LFOM,z): + return (2/((2*pc.gravity*z)**(1/2)*con.RATIO_VC_ORIFICE*np.pi*HL_LFOM)).magnitude + + +@u.wraps(None, [u.m**3/u.s,u.m], False) +def n_lfom_rows(FLOW,HL_LFOM): + """This equation states that the open area corresponding to one row can be + set equal to two orifices of diameter=row height. If there are more than + two orifices per row at the top of the LFOM then there are more orifices + than are convenient to drill and more than necessary for good accuracy. + Thus this relationship can be used to increase the spacing between the + rows and thus increase the diameter of the orifices. This spacing function + also sets the lower depth on the high flow rate LFOM with no accurate + flows below a depth equal to the first row height. + + But it might be better to always set then number of rows to 10. + The challenge is to figure out a reasonable system of constraints that + reliably returns a valid solution. + """ + N_estimated = (HL_LFOM*np.pi/(2*width_stout(HL_LFOM,HL_LFOM)*FLOW)) + variablerow = min(10,max(4,math.trunc(N_estimated.magnitude))) + # Forcing the LFOM to either have 4 or 8 rows, for design purposes + # If the hydraulic calculation finds that there should be 4 rows, then there + # will be 4 rows. If anything other besides 4 rows is found, then assign 8 + # rows. + # This can be improved in the future. + if variablerow == 4: + variablerow = 4 + else: + variablerow = 8 + return variablerow + +@u.wraps(u.m, [u.m**3/u.s, u.m], False) +def dist_center_lfom_rows(FLOW,HL_LFOM): + return HL_LFOM/n_lfom_rows(FLOW,HL_LFOM) + +@u.wraps(u.m/u.s, [u.m], False) +def vel_lfom_pipe_critical(HL_LFOM): + """The average vertical velocity of the water inside the LFOM pipe + at the very bottom of the bottom row of orifices + The speed of falling water is 0.841 m/s for all linear flow orifice meters + of height 20 cm, independent of total plant flow rate.""" + return 4/(3*math.pi)*(2*pc.gravity.magnitude*HL_LFOM)**(1/2) + +@u.wraps(u.m**2, [u.m**3/u.s, u.m], False) +def area_lfom_pipe_min(FLOW, HL_LFOM): + return (con.RATIO_LFOM_SAFETY*FLOW/vel_lfom_pipe_critical(HL_LFOM).magnitude) + +@u.wraps(u.inch, [u.m**3/u.s, u.m], False) +def nom_diam_lfom_pipe(FLOW,HL_LFOM): + ID = pc.diam_circle(area_lfom_pipe_min(FLOW, HL_LFOM)) + return pipe.ND_SDR_available(ID, mat.SDR_LFOM).magnitude + +@u.wraps(u.m**2, [u.m**3/u.s, u.m], False) +def area_lfom_orifices_top(FLOW,HL_LFOM): + """Estimate the orifice area corresponding to the top row of orifices. + Another solution method is to use integration to solve this problem. + Here we use the width of the stout weir in the center of the top row + to estimate the area of the top orifice + """ + return ((FLOW*width_stout(HL_LFOM*u.m,HL_LFOM*u.m-0.5*dist_center_lfom_rows(FLOW,HL_LFOM)).magnitude * + dist_center_lfom_rows(FLOW,HL_LFOM).magnitude)) + +@u.wraps(u.m, [u.m**3/u.s, u.m], False) +def d_lfom_orifices_max(FLOW, HL_LFOM): + return (pc.diam_circle(area_lfom_orifices_top(FLOW,HL_LFOM).magnitude).magnitude) + +@u.wraps(u.m, [u.m**3/u.s, u.m, u.inch], False) +def orifice_diameter(FLOW,HL_LFOM,drill_bits): + maxdrill = (min((dist_center_lfom_rows(FLOW,HL_LFOM).magnitude),(d_lfom_orifices_max(FLOW,HL_LFOM).magnitude))) + return ut.floor_nearest(maxdrill,drill_bits) + +@u.wraps(u.m**2, [u.m**3/u.s, u.m, u.inch], False) +def drillbit_area(FLOW,HL_LFOM,drill_bits): + return pc.area_circle(orifice_diameter(FLOW,HL_LFOM,drill_bits)).magnitude + +@u.wraps(None, [u.m**3/u.s, u.m, u.inch], False) +def n_lfom_orifices_per_row_max(FLOW,HL_LFOM,drill_bits): + """A bound on the number of orifices allowed in each row. + The distance between consecutive orifices must be enough to retain + structural integrity of the pipe. + """ + return math.floor(math.pi*(pipe.ID_SDR( + nom_diam_lfom_pipe(FLOW, HL_LFOM), mat.SDR_LFOM).magnitude) + / (orifice_diameter(FLOW, HL_LFOM, drill_bits).magnitude + + opt.S_LFOM_ORIFICE.magnitude)) + +@u.wraps(u.m**3/u.s, [u.m**3/u.s, u.m], False) +def flow_ramp(FLOW,HL_LFOM): + n_rows = n_lfom_rows(FLOW,HL_LFOM) + return np.linspace(FLOW/n_rows,FLOW,n_rows) + +@u.wraps(u.m, [u.m**3/u.s, u.m, u.inch], False) +def height_lfom_orifices(FLOW,HL_LFOM,drill_bits): + """Calculates the height of the center of each row of orifices. + The bottom of the bottom row orifices is at the zero elevation + point of the LFOM so that the flow goes to zero when the water height + is at zero. + """ + return (np.arange((orifice_diameter(FLOW,HL_LFOM,drill_bits)*0.5), + HL_LFOM, + (dist_center_lfom_rows(FLOW,HL_LFOM)))) + +@u.wraps(u.m**3/u.s, [u.m**3/u.s, u.m, u.inch, None, None], False) +def flow_lfom_actual(FLOW,HL_LFOM,drill_bits,Row_Index_Submerged,N_LFOM_Orifices): + """Calculates the flow for a given number of submerged rows of orifices + harray is the distance from the water level to the center of the orifices + when the water is at the max level + """ + D_LFOM_Orifices=orifice_diameter(FLOW, HL_LFOM, drill_bits).magnitude + row_height=dist_center_lfom_rows(FLOW, HL_LFOM).magnitude + harray = (np.linspace(row_height, HL_LFOM, n_lfom_rows(FLOW, HL_LFOM))) - 0.5 * D_LFOM_Orifices + FLOW_new = 0 + for i in range(Row_Index_Submerged+1): + FLOW_new = FLOW_new + (N_LFOM_Orifices[i]*( + pc.flow_orifice_vert(D_LFOM_Orifices, harray[Row_Index_Submerged-i], + con.RATIO_VC_ORIFICE).magnitude)) + return FLOW_new + + +#Calculate number of orifices at each level given a diameter +@u.wraps(None, [u.m**3/u.s, u.m, u.inch], False) +def n_lfom_orifices(FLOW,HL_LFOM,drill_bits): + FLOW_ramp_local = flow_ramp(FLOW,HL_LFOM).magnitude + n_orifices_max =n_lfom_orifices_per_row_max(FLOW,HL_LFOM,drill_bits) + n_rows = (n_lfom_rows(FLOW,HL_LFOM)) + D_LFOM_Orifices = orifice_diameter(FLOW,HL_LFOM,drill_bits).magnitude + # H is distance from the elevation between two rows of orifices down to the center of the orifices + H=dist_center_lfom_rows(FLOW,HL_LFOM).magnitude-D_LFOM_Orifices*0.5 + n=[] + for i in range(n_rows): + #place zero in the row that we are going to calculate the required number of orifices + n=np.append(n,0) + #calculate the ideal number of orifices at the current row without constraining to an integer + n_orifices_real=((FLOW_ramp_local[i]-flow_lfom_actual(FLOW,HL_LFOM,drill_bits,i,n).magnitude)/ + pc.flow_orifice_vert(D_LFOM_Orifices,H,con.RATIO_VC_ORIFICE)).magnitude + #constrain number of orifices to be less than the max per row and greater or equal to 0 + n[i]=min((max(0,round(n_orifices_real))),n_orifices_max) + return n + +#This function takes the output of n_lfom_orifices and converts it to a list with 8 +#entries that corresponds to the 8 possible rows. This is necessary to make the lfom +# easier to construct in Fusion using patterns +@u.wraps(None, [u.m**3/u.s, u.m, u.inch, None], False) +def n_lfom_orifices_fusion(FLOW,HL_LFOM,drill_bits,num_rows): + num_orifices_per_row = n_lfom_orifices(FLOW, HL_LFOM, drill_bits) + num_orifices_final = np.zeros(8) + centerline = np.zeros(8) + center = True + for i in range(8): + if i % 2 == 1 and num_rows == 4: + centerline[i] = int(center) + elif num_rows == 4: + num_orifices_final[i] = num_orifices_per_row[i/2] + centerline[i] = int(center) + center = not center + else: + num_orifices_final[i] = num_orifices_per_row[i] + centerline[i] = int(center) + center = not center + + return num_orifices_final, centerline + + + +#This function calculates the error of the design based on the differences between the predicted flow rate +#and the actual flow rate through the LFOM. +@u.wraps(u.m**3/u.s, [u.m**3/u.s, u.m, u.inch], False) +def flow_lfom_error(FLOW,HL_LFOM,drill_bits): + N_lfom_orifices=n_lfom_orifices(FLOW,HL_LFOM,drill_bits,mat.SDR_LFOM) + FLOW_lfom_error=[] + for j in range (len(N_lfom_orifices)-1): + FLOW_lfom_error.append((flow_lfom_actual( + FLOW, HL_LFOM, drill_bits, j, N_lfom_orifices).magnitude-flow_ramp( + FLOW, HL_LFOM)[j].magnitude)/FLOW) + return FLOW_lfom_error + + +@u.wraps(u.m**3/u.s, [u.m**3/u.s, u.m, u.m], False) +def flow_lfom_ideal(FLOW, HL_LFOM, H): + flow_lfom_ideal=(FLOW*H)/HL_LFOM + return flow_lfom_ideal + +@u.wraps(u.m**3/u.s, [u.m**3/u.s, u.m, u.inch, u.m], False) +def flow_lfom(FLOW,HL_LFOM,drill_bits,H): + D_lfom_orifices=orifice_diameter(FLOW,HL_LFOM,drill_bits).magnitude + H_submerged=np.arange(H-0.5*D_lfom_orifices, HL_LFOM, H-dist_center_lfom_rows(FLOW,HL_LFOM).magnitude,dtype=object) + N_lfom_orifices=n_lfom_orifices(FLOW,HL_LFOM,drill_bits,mat.SDR_LFOM) + flow=[] + for i in range(len(H_submerged)): + flow.append(pc.flow_orifice_vert(D_lfom_orifices,H_submerged[i],con.RATIO_VC_ORIFICE)*N_lfom_orifices[i]) + return sum(flow) diff --git a/aide_design/unit_process_design/lfom_dict_test.py b/aide_design/unit_process_design/lfom_dict_test.py new file mode 100644 index 00000000..7ba48a83 --- /dev/null +++ b/aide_design/unit_process_design/lfom_dict_test.py @@ -0,0 +1,744 @@ + +"""This file contains all the functions needed to design the linear flow +orifice meter (LFOM) for an AguaClara plant. + +Attributes +---------- +sdr: int + Ratio between outer diameter and wall thickness + +ratio_safety : float + Factor of safety + +S_orifice : float + Edge to edge spacing between orifices + +hl : float + Headloss through the LFOM + +""" + +#Here we import packages that we will need for this notebook. You can find out about these packages in the Help menu. +from aide_design.play import* + +#primary outputs from this file are +#Nominal diameter nom_diam_lfom_pipe(FLOW,HL_LFOM,con.RATIO_LFOM_SAFETY) +#number of rows n_lfom_rows(FLOW,HL_LFOM) +#orifice diameter orifice_diameter(FLOW,HL_LFOM,drill_series_uom) +#number of orifices in each row n_lfom_orifices(FLOW,HL_LFOM,drill_series_uom) +#height of the center of each row height_lfom_orifices(FLOW,HL_LFOM,drill_series_uom) + +# will eventually define this by rendering a template, but we'll get to that later: +lfom_dict = {'sdr': 26, 'ratio_safety': 1.5, 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + +# output is width per flow rate. +@u.wraps(u.s/(u.m**2), [u.m, None], False) +def width_stout(depth, lfom_inputs=lfom_dict): + """This equation relates the LFOM to a stout weir. A stout weir controls + flow through the width of a stout. The specific weir we reference is the + sutro weir, which is designed to linearly relate flow to height of water. + The LFOM mimics this linear relationship through a series of orifices. + + Parameters + ---------- + depth : float + depth of water + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + width_stout: float + equivalent width of stout in width per flow rate + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 40*u.cm} + >>> width_stout(40*u.cm, lfom_dict) + 0.9019329453483474 second/meter² + """ + return (2/((2 * pc.gravity.magnitude*depth)**(1/2) + * con.RATIO_VC_ORIFICE*np.pi*lfom_inputs['hl'].to(u.m).magnitude)) + +@u.wraps(None, [u.m**3/u.s, None], False) +def n_lfom_rows(Q, lfom_inputs=lfom_dict): + """This equation states that the open area corresponding to one row can be + set equal to two orifices of diameter=row height. If there are more than + two orifices per row at the top of the LFOM then there are more orifices + than are convenient to drill and more than necessary for good accuracy. + Thus this relationship can be used to increase the spacing between the + rows and thus increase the diameter of the orifices. This spacing function + also sets the lower depth on the high flow rate LFOM with no accurate + flows below a depth equal to the first row height. + + But it might be better to always set then number of rows to 10. + The challenge is to figure out a reasonable system of constraints that + reliably returns a valid solution. + + Parameters + ---------- + Q: float + flow through the LFOM + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + n_lfom_rows : int + number of rows the LFOM should contain + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + >>> n_lfom_rows(20 *u.L/u.s) + 8 + >>> n_lfom_rows(60 *u.L/u.s) + 4 + + """ + N_est = lfom_inputs['hl'].to(u.m).magnitude*np.pi/(2*width_stout(lfom_inputs['hl'], lfom_inputs).magnitude*Q) + variablerow = min(10, max(4, math.trunc(N_est))) + # Forcing the LFOM to either have 4 or 8 rows, for design purposes + # If the hydraulic calculation finds that there should be 4 rows, then there + # will be 4 rows. If anything other besides 4 rows is found, then assign 8 + # rows. + # This can be improved in the future. + if variablerow != 4: + variablerow = 8 + return variablerow + +@u.wraps(u.m, [u.m**3/u.s, None], False) +def dist_center_lfom_rows(Q, lfom_inputs=lfom_dict): + """ + ? + + Parameters + ---------- + Q: float + flow through the LFOM + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + >>> dist_center_lfom_rows(20*u.L/u.s) + 0.025 meter + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 60*u.cm} + >>> dist_center_lfom_rows(60*u.L/u.s, lfom_dict) + 0.075 centimeter + """ + return lfom_inputs['hl'].to(u.m).magnitude/n_lfom_rows(Q, lfom_inputs) + +@u.wraps(u.m/u.s, [None], False) +def vel_lfom_pipe_critical(lfom_inputs=lfom_dict): + """ + The average vertical velocity of the water inside the LFOM pipe + at the very bottom of the bottom row of orifices + The speed of falling water is 0.841 m/s for all linear flow orifice meters + of height 20 cm, independent of total plant flow rate. + + Parameters + ---------- + hl: float + headloss through the LFOM + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> vel_lfom_pipe_critical() + 0.8405802802312778 meter/second + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 60*u.cm} + >>> vel_lfom_pipe_critical(lfom_dict) + 1.4559277532010582 meter/second + """ + return 4/(3*math.pi)*(2*pc.gravity.magnitude*lfom_inputs['hl'].to(u.m).magnitude)**(1/2) + +@u.wraps(u.m**2, [u.m**3/u.s, None], False) +def area_lfom_pipe_min(Q, lfom_inputs=lfom_dict): + """ + ? + + Parameters + ---------- + Q: float + flow through the LFOM + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + >>> area_lfom_pipe_min(20*u.L/u.s) + 0.035689630967485675 meter2 + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 60*u.cm} + >>> area_lfom_pipe_min(60*u.L/u.s, lfom_dict) + 0.061816254139068764 meter2 + + """ + return (lfom_inputs['ratio_safety']*Q/vel_lfom_pipe_critical(lfom_inputs).magnitude) + +@u.wraps(u.inch, [u.m**3/u.s, None], False) +def nom_diam_lfom_pipe(Q, lfom_inputs=lfom_dict): + """ + ? + + Parameters + ---------- + Q: float + flow through the LFOM + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + >>> nom_diam_lfom_pipe(20*u.L/u.s) + 10.0 inch + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 60*u.cm} + >>> nom_diam_lfom_pipe(60*u.L/u.s, lfom_dict) + 12.0 inch + """ + ID = pc.diam_circle(area_lfom_pipe_min(Q, lfom_inputs)) + return pipe.ND_SDR_available(ID, lfom_inputs['sdr']).magnitude + +@u.wraps(u.m**2, [u.m**3/u.s, None], False) +def area_lfom_orifices_top(Q, lfom_inputs=lfom_dict): + """Estimate the orifice area corresponding to the top row of orifices. + Another solution method is to use integration to solve this problem. + Here we use the width of the stout weir in the center of the top row + to estimate the area of the top orifice + + Parameters + ---------- + Q: float + flow through the LFOM + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + >>> area_lfom_orifices_top(20*u.L/u.s) + 0.0013173573853983045 meter2 + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 60*u.cm} + >>> area_lfom_orifices_top(60*u.L/u.s, lfom_dict) + 0.002281729923235958 meter2 + """ + return ((Q * width_stout(lfom_inputs['hl'].to(u.m)-0.5 * + dist_center_lfom_rows(Q, lfom_inputs), lfom_inputs).magnitude * + dist_center_lfom_rows(Q, lfom_inputs).magnitude)) + +@u.wraps(u.m, [u.m**3/u.s, None], False) +def d_lfom_orifices_max(Q, lfom_inputs=lfom_dict): + """ + ? + + Parameters + ---------- + Q: float + flow through the LFOM + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + >>> d_lfom_orifices_max(20*u.L/u.s, lfom_dict) + 0.04095499380586013 meter + """ + return (pc.diam_circle( + area_lfom_orifices_top(Q, lfom_inputs)).magnitude) + +@u.wraps(u.m, [u.m**3/u.s, u.inch, None], False) +def orifice_diameter(Q, drill_bits, lfom_inputs=lfom_dict): + """ + ? + + Parameters + ---------- + Q: float + flow through the LFOM + + drill_bits: array of floats + an array of potential drill bit sizes to create the orifices + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + >>> orifice_diameter(20*u.L/u.s, mat.DIAM_DRILL_ENG) + 0.022224999999999998 meter + + """ + maxdrill = (min((dist_center_lfom_rows(Q, lfom_inputs).magnitude), + (d_lfom_orifices_max(Q, lfom_inputs).magnitude))) + + return (ut.floor_nearest(maxdrill, (drill_bits * u.inch).to(u.m).magnitude)) + + orifice_diameter(20*u.L/u.s, mat.DIAM_DRILL_ENG) + +@u.wraps(u.m**2, [u.m**3/u.s, u.inch, None], False) +def drillbit_area(Q, drill_bits, lfom_inputs=lfom_dict): + """ + ? + + Parameters + ---------- + Q: float + flow through the LFOM + + drill_bits: array of floats + an array of potential drill bit sizes to create the orifices + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + >>> drillbit_area(20*u.L/u.s,mat.DIAM_DRILL_ENG) + 0.00038794791368402165 meter2 + + """ + return pc.area_circle( + orifice_diameter(Q, drill_bits, lfom_inputs)).magnitude + +@u.wraps(None, [u.m**3/u.s, u.inch, None], False) +def n_lfom_orifices_per_row_max(Q, drill_bits, lfom_inputs=lfom_dict): + """A bound on the number of orifices allowed in each row. + The distance between consecutive orifices must be enough to retain + structural integrity of the pipe. + + Parameters + ---------- + Q: float + flow through the LFOM + + drill_bits: array of floats + an array of potential drill bit sizes to create the orifices + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + >>> n_lfom_orifices_per_row_max(20*u.L/u.s,mat.DIAM_DRILL_ENG) + 30 + + """ + return math.floor(math.pi*(pipe.ID_SDR( + nom_diam_lfom_pipe(Q, lfom_inputs), lfom_inputs['sdr']).magnitude) + / (orifice_diameter(Q, drill_bits, lfom_inputs).magnitude + + lfom_inputs['S_orifice'].magnitude)) + +@u.wraps(u.m**3/u.s, [u.m**3/u.s, None], False) +def flow_ramp(Q, lfom_inputs=lfom_dict): + """ + ? + + Parameters + ---------- + Q: float + flow through the LFOM + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 40*u.cm} + >>> flow_ramp(20*u.L/u.s,lfom_dict) + \\[\\begin{pmatrix}0.0025 & 0.005 & 0.0075 & 0.01 & 0.0125 & 0.015000000000000001 & 0.017499999999999998 & 0.02\\end{pmatrix} meter3/second\\] + """ + n_rows = n_lfom_rows(Q, lfom_inputs) + return np.linspace(Q/n_rows, Q, n_rows) + + +@u.wraps(u.m, [u.m**3/u.s, u.inch, None], False) +def height_lfom_orifices(Q, drill_bits, lfom_inputs=lfom_dict): + """Calculates the height of the center of each row of orifices. + The bottom of the bottom row orifices is at the zero elevation + point of the LFOM so that the flow goes to zero when the water height + is at zero. + + Parameters + ---------- + Q: float + flow through the LFOM + + drill_bits: array of floats + an array of potential drill bit sizes to create the orifices + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + >>> height_lfom_orifices(20*u.L/u.s,mat.DIAM_DRILL_ENG) + \\[\\begin{pmatrix}0.011112499999999999 & 0.0361125 & 0.0611125 & 0.08611250000000001 & 0.1111125 & 0.1361125 & 0.16111250000000002 & 0.18611250000000001\\end{pmatrix} meter\\] + + """ + return (np.arange((orifice_diameter(Q, drill_bits, lfom_inputs).magnitude*0.5), + lfom_inputs['hl'].to(u.m).magnitude, + (dist_center_lfom_rows(Q, lfom_inputs)).magnitude)) + +@u.wraps(u.m**3/u.s, [u.m**3/u.s, u.inch, None, None, None], False) +def flow_lfom_actual(Q, drill_bits, Row_Index_Submerged, N_lfom_Orifices, lfom_inputs=lfom_dict): + """Calculates the flow for a given number of submerged rows of orifices + harray is the distance from the water level to the center of the orifices + when the water is at the max level + + Parameters + ---------- + Q: float + flow through the LFOM + + drill_bits: array of floats + an array of potential drill bit sizes to create the orifices + + Row_Index_Submerged : int + index of the row in the n_lfom_orifices array up to which the orifices + are submerged + + N_lfom_Orifices: + list with each entry being the number of orifices in that row of the + LFOM + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + >>> flow_lfom_actual(20*u.L/u.s, mat.DIAM_DRILL_ENG, 1, [1,1]) + 0.00033756816936411334 meter3/second + + """ + D_lfom_Orifices = orifice_diameter(Q, drill_bits, lfom_inputs).magnitude + row_height = dist_center_lfom_rows(Q, lfom_inputs).magnitude + harray = (np.linspace(row_height, lfom_inputs['hl'].to(u.m).magnitude, + n_lfom_rows(Q, lfom_inputs))) - 0.5 * D_lfom_Orifices + Q_new = 0 + for i in range(Row_Index_Submerged+1): + Q_new = Q_new + (N_lfom_Orifices[i]*( + pc.flow_orifice_vert(D_lfom_Orifices, + harray[Row_Index_Submerged-i], + con.RATIO_VC_ORIFICE))) + return Q_new.magnitude + + flow_lfom_actual(20*u.L/u.s, mat.DIAM_DRILL_ENG, 1, [1,1]) + +#Calculate number of orifices at each level given a diameter +@u.wraps(None, [u.m**3/u.s, u.inch, None], False) +def n_lfom_orifices(Q, drill_bits, lfom_inputs=lfom_dict): + Q = 20*u.L/u.s + drill_bits = mat.DIAM_DRILL_ENG + lfom_inputs = lfom_dict + Q_ramp_local = flow_ramp(Q, lfom_inputs).magnitude + N_orifices_max = n_lfom_orifices_per_row_max(Q, drill_bits, lfom_inputs) + N_rows = (n_lfom_rows(Q, lfom_inputs)) + D_lfom_Orifices = orifice_diameter(Q, drill_bits, lfom_inputs).magnitude + # H is distance from the elevation between two rows of orifices down to the center of the orifices + H = dist_center_lfom_rows(Q, lfom_inputs).magnitude - D_lfom_Orifices*0.5 + n = [] + for i in range(N_rows): + #place zero in the row that we are going to calculate the required number of orifices + n = np.append(n, 0) + #calculate the ideal number of orifices at the current row without constraining to an integer + N_orifices_real = ((Q_ramp_local[i] - flow_lfom_actual(Q, drill_bits, i, n, lfom_inputs).magnitude) / + pc.flow_orifice_vert(D_lfom_Orifices, H, con.RATIO_VC_ORIFICE).magnitude) + #constrain number of orifices to be less than the max per row and greater or equal to 0 + n[i] = min((max(0, round(N_orifices_real))), N_orifices_max) + return n + +#This function takes the output of n_lfom_orifices and converts it to a list with 8 +#entries that corresponds to the 8 possible rows. This is necessary to make the lfom +# easier to construct in Fusion using patterns +@u.wraps(None, [u.m**3/u.s, u.inch, None, None], False) +def n_lfom_orifices_fusion(Q, drill_bits, num_rows, lfom_inputs=lfom_dict): + """This function takes the output of n_lfom_orifices and converts it to a list with 8 + entries that corresponds to the 8 possible rows. This is necessary to make the lfom + easier to construct in Fusion using patterns + + Parameters + ---------- + Q: float + flow through the LFOM + + drill_bits: array of floats + an array of potential drill bit sizes to create the orifices + + num_rows : int + number of rows of orifices in the LFOM + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + >>> n_lfom_orifices_fusion(20*u.L/u.s, mat.DIAM_DRILL_ENG, 8, lfom_dict) + (array([20., 6., 6., 5., 4., 5., 3., 3.]), + array([1., 0., 1., 0., 1., 0., 1., 0.])) + + """ + N_orifices_per_row = n_lfom_orifices(Q, drill_bits, lfom_inputs) + N_orifices_final = np.zeros(8) + centerline = np.zeros(8) + center = True + for i in range(8): + if i % 2 == 1 and num_rows == 4: + centerline[i] = int(center) + elif num_rows == 4: + N_orifices_final[i] = N_orifices_per_row[i/2] + centerline[i] = int(center) + center = not center + else: + N_orifices_final[i] = N_orifices_per_row[i] + centerline[i] = int(center) + center = not center + + return N_orifices_final, centerline + +#This function calculates the error of the design based on the differences between the predicted flow rate +#and the actual flow rate through the LFOM. +@u.wraps(u.m**3/u.s, [u.m**3/u.s, u.inch, None], False) +def flow_lfom_error(Q, drill_bits, lfom_inputs=lfom_dict): + """? + + Parameters + ---------- + Q: float + flow through the LFOM + + drill_bits: array of floats + an array of potential drill bit sizes to create the orifices + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + >>> flow_lfom_error(20*u.L/u.s, mat.DIAM_DRILL_ENG) + \\[\\begin{pmatrix}-0.0003291229516060772 & 0.00029855543023757543 & -0.00040128229002386884 & -0.00041467605669878727 & -0.0029056699882666973 & 0.001701216757524042 & 0.000898141605292814\\end{pmatrix} meter3/second\\] + """ + N_lfom_orifices = n_lfom_orifices(Q, drill_bits, lfom_inputs) + Q_lfom_error = [] + for j in range(len(N_lfom_orifices)-1): + Q_lfom_error.append((flow_lfom_actual( + Q, drill_bits, j, N_lfom_orifices, lfom_inputs).magnitude - + flow_ramp(Q, lfom_inputs)[j].magnitude)/Q) + return Q_lfom_error + +@u.wraps(u.m**3/u.s, [u.m**3/u.s, u.m, None], False) +def flow_lfom_ideal(Q, H, lfom_inputs=lfom_dict): + """? + + Parameters + ---------- + Q: float + flow through the LFOM + + H: float + height (includes freeboard) + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + >>> flow_lfom_ideal(20*u.L/u.s,20*u.cm) + 0.02 meter3 / second + """ + Q_lfom_ideal = (Q*H)/lfom_inputs['hl'].to(u.m).magnitude + return Q_lfom_ideal + +@u.wraps(u.m**3/u.s, [u.m**3/u.s, u.inch, u.m, None], False) +def flow_lfom(Q, drill_bits, H, lfom_inputs=lfom_dict): + """? + + Parameters + ---------- + Q: float + designed flow through the LFOM + + drill_bits : array of floats + an array of potential drill bit sizes to create the orifices + + H : float + depth of the water + + lfom_inputs : dict + a dictionary of all of the constant inputs needed for LFOM calculations + can be found in lfom.yaml + + Returns + ------- + float + ? + + Examples + -------- + >>> from aide_design.play import* + >>> lfom_dict = {'sdr': 26, ratio_safety': 1.5, + ... 'S_orifice': 1*u.cm, 'hl': 20*u.cm} + >>> flow_lfom(20*u.L/u.s, mat.DIAM_DRILL_ENG, 20*u.cm, lfom_dict) + 0.00940749311972628 meter3/second + + """ + D_lfom_orifices = orifice_diameter(Q, drill_bits, lfom_inputs).magnitude + H_submerged = np.arange(H - 0.5 * D_lfom_orifices, lfom_inputs['hl'].to(u.m).magnitude, + H - dist_center_lfom_rows(Q, lfom_inputs).magnitude, dtype=object) + N_lfom_orifices = n_lfom_orifices(Q, drill_bits, lfom_inputs) + Q = [] + for i in range(len(H_submerged)): + Q.append(pc.flow_orifice_vert(D_lfom_orifices, H_submerged[i], + con.RATIO_VC_ORIFICE) * + N_lfom_orifices[i]) + return sum(Q).magnitude diff --git a/aide_design/unit_process_design/prefab/ent_tank_prefab.py b/aide_design/unit_process_design/prefab/ent_tank_prefab.py deleted file mode 100644 index 41fe0a53..00000000 --- a/aide_design/unit_process_design/prefab/ent_tank_prefab.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 10 15:30:35 2017 - -@author: cc2467 -""" - diff --git a/aide_design/unit_process_design/prefab/floc_prefab.py b/aide_design/unit_process_design/prefab/floc_prefab.py deleted file mode 100644 index fcded34b..00000000 --- a/aide_design/unit_process_design/prefab/floc_prefab.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 10 15:31:16 2017 - -@author: cc2467 -""" - diff --git a/aide_design/unit_process_design/prefab/lfom_prefab.py b/aide_design/unit_process_design/prefab/lfom_prefab.py deleted file mode 100644 index e12322aa..00000000 --- a/aide_design/unit_process_design/prefab/lfom_prefab.py +++ /dev/null @@ -1,215 +0,0 @@ -#Here we import packages that we will need for this notebook. You can find out about these packages in the Help menu. - -# although math is "built in" it needs to be imported so it's functions can be used. -import math - -from scipy import constants, interpolate - -#see numpy cheat sheet https://www.dataquest.io/blog/images/cheat-sheets/numpy-cheat-sheet.pdf -#The numpy import is needed because it is renamed here as np. -import numpy as np - -import pandas as pd - -import matplotlib.pyplot as plt - -# add imports for AguaClara code that will be needed -# physchem has functions related to hydraulics, fractal flocs, flocculation, sedimentation, etc. -from aide_design import physchem as pc - -# pipedatabase has functions related to pipe diameters -from aide_design import pipedatabase as pipe - -# units allows us to include units in all of our calculations -from aide_design.units import unit_registry as u - -# utility has the significant digit display function -from aide_design import utility as ut - -##---## - -# The following constants need to go into the constants file -Pi_LFOM_safety = 1.2 -# pipe schedule for LFOM -SDR_LFOM = 26 - -FLOW = 5*u.L/u.s -HL_LFOM = 20*u.cm - -ratio_VC_orifice= 0.62 - -from enum import Enum - -class uomeasure(Enum): - english = 0 - metric = 1 - -def drill_series(uomeasure): - if uomeasure is uomeasure.english: - ds=np.arange(1/32, 1/4, 1/32) - ds=np.append(ds,np.arange(3/8, 1, 1/8)) - ds=np.append(ds,np.arange(1.25, 3.25, 1/4)) - ds=ds*u.inch - else: - ds=np.arange(0.5, 4.9, 0.1) - ds=np.append(ds,np.arange(5, 19, 1)) - ds=np.append(ds,np.arange(20, 50, 2)) - ds=ds*u.mm - return ds - -# Take the values of the array, compare to x, find the index of the first value less than or equal to x -def floor_nearest(x,array): - myindex = np.argmax(array <= x) - return array[myindex] - -# Take the values of the array, compare to x, find the index of the first value greater or equal to x -def ceil_nearest(x,array): - myindex = np.argmax(array >= x) - return array[myindex] - -class LFOM: - - def __init__(self, flow, hl, ratio_safety, sdr, drill_series_uom): - self.flow = flow - self.hl = hl - self.ratio_safety = ratio_safety - self.sdr = sdr - self.drill_series_uom = drill_series_uom - - def set_flow(self, flow): - self.flow = flow - - def set_hl(self, hl): - self.hl = hl - - def set_ratio_safety(self, ratio_safety): - self.ratio_safety = ratio_safety - - def set_sdr(self, sdr): - self.sdr = sdr - - def set_drill_series(self, drill_series_uom): - self.drill_series_uom = drill_series_uom - - - def __width_stout(self, z): - return 2 / ((2 * u.g_0 * z)**(1/2) * math.pi * self.hl) - - def __n_rows(self): - n_est = (self.hl * math.pi / (2 * self.__width_stout(self.hl) * self.flow)).to(u.dimensionless) - return min(10, max(4, math.trunc(n_est.magnitude))) - - def __dist_center_rows(self): - return self.hl / self.__n_rows() - - # average vertical velocity of the water inside the LFOM pipe - # at the very bottom of the bottom row of orifices - # The speed of falling water is 0.841 m/s for all linear flow orifice meters of height 20cm, - # independent of total plant flow rate. - def __vol_pipe_critical(self): - return (4 / (3 * math.pi) * (2 * u.g_0 * self.hl)**(1/2)).to(u.m/u.s) - - def __area_pipe_min(self): - return (self.ratio_safety * self.flow / self.__vol_pipe_critical()).to(u.m**2) - - def nom_diam_pipe(self): - id = pc.diam_circle(self.__area_pipe_min()) - return pipe.ND_SDR_available(id, self.sdr) - - # another possibility is to use integration to solve this problem. - # Here we use the width of the stout weir in the center of the top row - # to estimate the area of the top orifice - def __area_orifices_max(self): - z = self.hl - 0.5 * self.__dist_center_rows() - return self.flow * self.__width_stout(z) * self.__dist_center_rows() - - def __d_orifices_max(self): - return pc.diam_circle(self.__area_orifices_max()) - - def drillbit_diameter(self): - return ceil_nearest(self.__d_orifices_max(), drill_series(self.drill_series_uom)) - - def __drillbit_area(self): - return pc.area_circle(self.drillbit_diameter()) - - ##A bound on the number of orifices allowed in each row. - ##The distance between consecutive orifices must be enough to retain structural integrity of the pipe - def __n_orifices_per_row_max(self): - S_lfom_orifices_Min= 3 * u.mm - nom_diam = self.nom_diam_pipe() - drillbit_diam = self.drillbit_diameter() + S_lfom_orifices_Min - return math.floor(math.pi * (pipe.ID_SDR(nom_diam, self.sdr)) / (drillbit_diam)) - - #locations where we will try to get the target flows is in between orifices at elevation Pi.H - def __flow_ramp(self): - dist_center = self.__dist_center_rows() / u.cm - return np.arange(dist_center, self.hl / u.cm, dist_center) * self.flow * u.cm / self.hl - - def height_orifices(self): - drillbit_diam = self.drillbit_diameter() * 0.5 - return np.arange(drillbit_diam, self.hl, self.__dist_center_rows(), dtype= object) - - #Calculate the flow for a given number of submerged rows of orifices - def __flow_actual(self, Row_Index_Submerged, N_LFOM_Orifices): - D_LFOM_Orifices = self.drillbit_diameter().to(u.m) - FLOW_new=[] - dist_center = self.__dist_center_rows() - for i in range(Row_Index_Submerged): - h = np.arange(dist_center, self.hl, dist_center, dtype=object) - h = h[Row_Index_Submerged].to(u.m) - d = np.arange(0.5* D_LFOM_Orifices, self.hl, dist_center, dtype=object) - FLOW_new.append(N_LFOM_Orifices[i]*(pc.flow_orifice_vert(D_LFOM_Orifices, h - d[i], ratio_VC_orifice))) - return sum(FLOW_new) - - #Calculate number of orifices at each level given a diameter - def fric_n_orifices(self): - FLOW_ramp_local = self.__flow_ramp() - D_LFOM_Orifices = self.drillbit_diameter() - h = np.arange(self.__dist_center_rows(), self.hl, self.__dist_center_rows(), dtype=object) - d = np.arange(D_LFOM_Orifices * 0.5, self.hl, self.__dist_center_rows(), dtype=object) - n = [] - for i in range (len(d) - 1): - flow_actual = self.__flow_actual(i, n) - Height = h[i] - d[i] - rounded = np.round((FLOW_ramp_local[i] - flow_actual).to(u.m**3 / u.seconds) / pc.flow_orifice_vert(D_LFOM_Orifices, Height, ratio_VC_orifice)) - if self.nom_diam_pipe() <= 12 * u.inch: - n.append(min(max(0, rounded), self.__n_orifices_per_row_max())) - else: - n.append(max(0,rounded)) - return n - - #This function calculates the error of the design based on the differences between the predicted flow rate - #and the actual flow rate through the LFOM. - def __flow_error(self): - N_lfom_orifices = self.fric_n_orifices() - FLOW_lfom_error = [] - for j in range (len(N_lfom_orifices) - 1): - flow_actual = self.__flow_actual(j, N_lfom_orifices) - FLOW_lfom_error.append((flow_actual - self.__flow_ramp()[j]) / self.flow) - return FLOW_lfom_error - - def __flow_error_max(self): - x = max(self.__flow_error()) - y = x**2 - return y**1/2 - - def __flow_ideal(self, height): - __flow_ideal=(self.flow * height) / self.hl - return __flow_ideal - - def flow_lfom(self, height): - D_lfom_orifices = self.drillbit_diameter() - H_submerged = np.arange(height - 0.5 * D_lfom_orifices, self.hl, height - self.__dist_center_rows(), dtype=object) - N_lfom_orifices = self.fric_n_orifices() - flow = [] - for i in range (len(H_submerged)): - flow.append(pc.flow_orifice_vert(D_lfom_orifices, H_submerged[i], ratio_VC_orifice) * N_lfom_orifices[i]) - return sum(flow) - - -lfom = LFOM(FLOW, HL_LFOM, Pi_LFOM_safety, SDR_LFOM, uomeasure.english) - -print(lfom.nom_diam_pipe()) -print(lfom.drillbit_diameter()) -print(lfom.height_orifices()) -print(lfom.fric_n_orifices()) \ No newline at end of file diff --git a/aide_design/unit_process_design/prefab/lfom_prefab_functional.py b/aide_design/unit_process_design/prefab/lfom_prefab_functional.py deleted file mode 100644 index 470d5e65..00000000 --- a/aide_design/unit_process_design/prefab/lfom_prefab_functional.py +++ /dev/null @@ -1,199 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Edited on September 1, 2017 -@author: Monroe Weber-Shirk - -Created on Wed Jun 21 17:16:46 2017 - -@author: cc2467 -""" - -#Here we import packages that we will need for this notebook. You can find out about these packages in the Help menu. - -# although math is "built in" it needs to be imported so it's functions can be used. -import math - -#see numpy cheat sheet https://www.dataquest.io/blog/images/cheat-sheets/numpy-cheat-sheet.pdf -#The numpy import is needed because it is renamed here as np. -import numpy as np - -# add imports for AguaClara code that will be needed -# physchem has functions related to hydraulics, fractal flocs, flocculation, sedimentation, etc. -from aide_design import physchem as pc - -# pipedatabase has functions related to pipe diameters -from aide_design import pipedatabase as pipe - -# units allows us to include units in all of our calculations -from aide_design.units import unit_registry as u - -# utility has the significant digit display function -from aide_design import utility as ut - -# import export inputs and define the VC coefficient -from aide_design import expert_inputs as exp -ratio_VC_orifice= exp.RATIO_VC_ORIFICE - -# The following constants need to go into the constants file -Pi_LFOM_safety = 1.2 -# pipe schedule for LFOM -#SDR_LFOM = 26 -#FLOW = 10*u.L/u.s -#HL_LFOM = 20*u.cm - -#primary outputs from this file are -#Nominal diameter nom_diam_lfom_pipe(FLOW,HL_LFOM,Pi_LFOM_safety,SDR_LFOM) -#number of rows n_lfom_rows(FLOW,HL_LFOM) -#orifice diameter orifice_diameter(FLOW,HL_LFOM,drill_series_uom) -#number of orifices in each row n_lfom_orifices(FLOW,HL_LFOM,drill_series_uom,SDR_LFOM) -#height of the center of each row height_lfom_orifices(FLOW,HL_LFOM,drill_series_uom) - -# output is width per flow rate. -@u.wraps(u.s/(u.m**2), [u.m,u.m], False) -def width_stout(HL_LFOM,z): - return (2/((2*pc.gravity*z)**(1/2)*ratio_VC_orifice*np.pi*HL_LFOM)).magnitude - - -@u.wraps(None, [u.m**3/u.s,u.m], False) -def n_lfom_rows(FLOW,HL_LFOM): - """This equation states that the open area corresponding to one row can be - set equal to two orifices of diameter=row height. If there are more than - two orifices per row at the top of the LFOM then there are more orifices - than are convenient to drill and more than necessary for good accuracy. - Thus this relationship can be used to increase the spacing between the - rows and thus increase the diameter of the orifices. This spacing function - also sets the lower depth on the high flow rate LFOM with no accurate - flows below a depth equal to the first row height. - - But it might be better to always set then number of rows to 10. - The challenge is to figure out a reasonable system of constraints that - reliably returns a valid solution. - """ - N_estimated = (HL_LFOM*np.pi/(2*width_stout(HL_LFOM,HL_LFOM)*FLOW)) - #variablerow=min(10,max(4,math.trunc(N_estimated.magnitude))) - return 10 - - -def dist_center_lfom_rows(FLOW,HL_LFOM): - return HL_LFOM/n_lfom_rows(FLOW,HL_LFOM) - - -def vel_lfom_pipe_critical(HL_LFOM): - """The average vertical velocity of the water inside the LFOM pipe - at the very bottom of the bottom row of orifices - The speed of falling water is 0.841 m/s for all linear flow orifice meters - of height 20 cm, independent of total plant flow rate.""" - return (4/(3*math.pi)*(2*u.g_0*HL_LFOM)**(1/2)).to(u.m/u.s) - -def area_lfom_pipe_min(FLOW,HL_LFOM,Pi_LFOM_safety): - return (Pi_LFOM_safety*FLOW/vel_lfom_pipe_critical(HL_LFOM)).to(u.m**2) - -def nom_diam_lfom_pipe(FLOW,HL_LFOM,Pi_LFOM_safety,SDR_LFOM): - ID=pc.diam_circle(area_lfom_pipe_min(FLOW,HL_LFOM,Pi_LFOM_safety)) - return pipe.ND_SDR_available(ID,SDR_LFOM) - -def area_lfom_orifices_max(FLOW,HL_LFOM): - """Estimate the orifice area corresponding to the top row of orifices. - Another solution method is to use integration to solve this problem. - Here we use the width of the stout weir in the center of the top row - to estimate the area of the top orifice - """ - return ((FLOW*width_stout(HL_LFOM,HL_LFOM-0.5*dist_center_lfom_rows(FLOW,HL_LFOM))*dist_center_lfom_rows(FLOW,HL_LFOM))).to(u.m**2) - -def d_lfom_orifices_max(FLOW,HL_LFOM): - return (pc.diam_circle(area_lfom_orifices_max(FLOW,HL_LFOM))) - -def orifice_diameter(FLOW,HL_LFOM,drill_bits): - maxdrill = (min((dist_center_lfom_rows(FLOW,HL_LFOM)).to(u.m).magnitude,(d_lfom_orifices_max(FLOW,HL_LFOM)).to(u.m).magnitude))*u.m - return ut.floor_nearest(maxdrill,drill_bits) - - -def drillbit_area(FLOW,HL_LFOM,drill_bits): - return pc.area_circle(orifice_diameter(FLOW,HL_LFOM,drill_bits)) - - - -def n_lfom_orifices_per_row_max(FLOW,HL_LFOM,drill_bits,SDR_LFOM): - """A bound on the number of orifices allowed in each row. - The distance between consecutive orifices must be enough to retain - structural integrity of the pipe. - """ - S_lfom_orifices_Min= 3*u.mm - return math.floor(math.pi*(pipe.ID_SDR(nom_diam_lfom_pipe(FLOW,HL_LFOM,Pi_LFOM_safety,SDR_LFOM),SDR_LFOM))/(orifice_diameter(FLOW,HL_LFOM,drill_bits)+S_lfom_orifices_Min)) - -def flow_ramp(FLOW,HL_LFOM): - n_rows = n_lfom_rows(FLOW,HL_LFOM) - return np.linspace(FLOW.magnitude/n_rows,FLOW.magnitude,n_rows)*FLOW.units - -def height_lfom_orifices(FLOW,HL_LFOM,drill_bits): - """Calculates the height of the center of each row of orifices. - The bottom of the bottom row orifices is at the zero elevation - point of the LFOM so that the flow goes to zero when the water height - is at zero. - """ - - return (np.arange(((orifice_diameter(FLOW,HL_LFOM,drill_bits)*0.5).to(u.m)).magnitude, - (HL_LFOM.to(u.m)).magnitude, - ((dist_center_lfom_rows(FLOW,HL_LFOM)).to(u.m)).magnitude))*u.m - -#print(height_lfom_orifices(10*u.L/u.s,20*u.cm,[0.75]*u.inch)) - -def flow_lfom_actual(FLOW,HL_LFOM,drill_bits,Row_Index_Submerged,N_LFOM_Orifices): - """Calculates the flow for a given number of submerged rows of orifices - """ - D_LFOM_Orifices=orifice_diameter(FLOW,HL_LFOM,drill_bits) - row_height=dist_center_lfom_rows(FLOW,HL_LFOM) - #harray is the distance from the water level to the center of the orifices when the water is at the max level - harray = (np.linspace(row_height.to(u.mm).magnitude,HL_LFOM.to(u.mm).magnitude,n_lfom_rows(FLOW,HL_LFOM)))*u.mm -0.5* D_LFOM_Orifices - FLOW_new=0*u.m**3/u.s - for i in range(Row_Index_Submerged+1): - FLOW_new = FLOW_new + (N_LFOM_Orifices[i]*(pc.flow_orifice_vert(D_LFOM_Orifices,harray[Row_Index_Submerged-i],ratio_VC_orifice))) - return FLOW_new - - -#Calculate number of orifices at each level given a diameter -def n_lfom_orifices(FLOW,HL_LFOM,drill_bits,SDR_LFOM): - FLOW_ramp_local = flow_ramp(FLOW,HL_LFOM) - n_orifices_max =n_lfom_orifices_per_row_max(FLOW,HL_LFOM,drill_bits,SDR_LFOM) - n_rows = (n_lfom_rows(FLOW,HL_LFOM)) - D_LFOM_Orifices = orifice_diameter(FLOW,HL_LFOM,drill_bits) - # H is distance from the elevation between two rows of orifices down to the center of the orifices - H=dist_center_lfom_rows(FLOW,HL_LFOM)-D_LFOM_Orifices*0.5 - n=[] - for i in range(n_rows): - #place zero in the row that we are going to calculate the required number of orifices - n=np.append(n,0) - #calculate the ideal number of orifices at the current row without constraining to an integer - n_orifices_real=((FLOW_ramp_local[i]-flow_lfom_actual(FLOW,HL_LFOM,drill_bits,i,n))/ - pc.flow_orifice_vert(D_LFOM_Orifices,H,ratio_VC_orifice)).to(u.dimensionless).magnitude - #constrain number of orifices to be less than the max per row and greater or equal to 0 - n[i]=min((max(0,round(n_orifices_real))),n_orifices_max) - return n - - -#This function calculates the error of the design based on the differences between the predicted flow rate -#and the actual flow rate through the LFOM. -def flow_lfom_error(FLOW,HL_LFOM,drill_bits,SDR_LFOM): - N_lfom_orifices=n_lfom_orifices(FLOW,HL_LFOM,drill_bits,SDR_LFOM) - FLOW_lfom_error=[] - for j in range (len(N_lfom_orifices)-1): - FLOW_lfom_error.append((flow_lfom_actual(FLOW,HL_LFOM,drill_bits,j,N_lfom_orifices)-flow_ramp(FLOW,HL_LFOM)[j])/FLOW) - return FLOW_lfom_error - - - -def flow_lfom_ideal(FLOW,HL_LFOM,H): - flow_lfom_ideal=(FLOW*H)/HL_LFOM - return flow_lfom_ideal - - -def flow_lfom(FLOW,HL_LFOM,drill_bits,SDR_LFOM,H): - D_lfom_orifices=orifice_diameter(FLOW,HL_LFOM,drill_bits) - H_submerged=np.arange(H-0.5*D_lfom_orifices,HL_LFOM,H-dist_center_lfom_rows(FLOW,HL_LFOM),dtype=object) - N_lfom_orifices=n_lfom_orifices(FLOW,HL_LFOM,drill_bits,SDR_LFOM) - flow=[] - for i in range (len(H_submerged)): - flow.append(pc.flow_orifice_vert(D_lfom_orifices,H_submerged[i],ratio_VC_orifice)*N_lfom_orifices[i]) - return sum (flow) - - diff --git a/aide_design/unit_process_design/prefab/sed_prefab.py b/aide_design/unit_process_design/prefab/sed_prefab.py deleted file mode 100644 index 36cfdeb0..00000000 --- a/aide_design/unit_process_design/prefab/sed_prefab.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 10 15:29:47 2017 - -@author: cc2467 -""" - diff --git a/aide_design/unit_process_design/sed_channel.py b/aide_design/unit_process_design/sed_channel.py new file mode 100644 index 00000000..0bb45167 --- /dev/null +++ b/aide_design/unit_process_design/sed_channel.py @@ -0,0 +1,32 @@ +"""This file contains all the functions needed to design the sedimentation tank +channels for an AguaClara plant. + + +""" + +from aide_design.play import* +from aide_design.unit_process_design import sed_tank_dict_test as sed_tank + +# again we will change this to an import statment from the URL of aide_template repo +sed_chan_dict = { + 'thickness_wall': 0.15*u.m, + 'plate_settlers': { + 'angle': 60*u.deg, 'S': 2.5*u.cm, + 'thickness': 2*u.mm, 'L_cantilevered': 20*u.cm, + }, + 'tank': { + 'W': 42*u.inch, 'L': 5.8*u.m, 'vel_up': 1*u.mm/u.s + }, + 'manifold': { + 'ratio_Q_man_orifice': 0.8, + 'diffuser': { + 'thickness_wall': 1.17*u.inch, 'vel_max': 442.9*u.mm/u.s, + 'A': 0.419*u.inch**2 + }, + 'exit_man': { + 'hl_orifice': 4*u.cm, 'N_orifices': 58 + } + } +} + +x = n_sed_plates_max(sed_tank.sed_dict) diff --git a/aide_design/unit_process_design/sed_tank_dict_test.py b/aide_design/unit_process_design/sed_tank_dict_test.py new file mode 100644 index 00000000..54586f24 --- /dev/null +++ b/aide_design/unit_process_design/sed_tank_dict_test.py @@ -0,0 +1,525 @@ +"""This file contains all the functions needed to design a sedimentation tank +for an AguaClara plant. + +Attributes +---------- +thickness_wall : float + Thickness of walls in the sedimentation unit process + +plate_settlers : dict + A dictionary containing variables relating to the plate settlers + + Attributes + ---------- + angle : int + Angle of plate settlers (relative to being completely horizontal) + + S : float + Edge to edge distance between plates + + thickness : float + Thickness of PVC sheet used to make plate settlers + + L_cantilevered : float + Maximum length of sed plate sticking out past module pipes without any + additional support. The goal is to prevent floppy modules that don't + maintain constant distances between the plates + +tank : dict + A dictionary containing variables relating to the concrete portion of the + sedimentation tank + + Attributes + ---------- + W : float + Width of the sedimentation tank. Based off of the width of the PVC + sheet used to make plate settlers + + L : float + Length of the sedimentation tank. Based off of the length of a manifold + pipe + + vel_up : float + Upflow velocity through a sedimentation tank used as basis of design + +manifold : dict + A dictionary containg variables relating to the inlet manifold, + exit manifold, and diffusers + + Attributes + ---------- + ratio_Q_orifice : float + Acceptable ratio of min to max flow through the manifold orifices + + diffuser : dict + A dictionary containing variables relating to the diffuser + + Attributes + ---------- + thickness_wall : float + Wall thickness of a diffuser + + vel_max : float + Maximum velocity through a diffuser + + A : float + Area of a diffuser when viewed down the length of the manifold + + exit_man : dict + A dictionary containing variables relating to the exit manifold + + Attributes + ---------- + hl_orifice : float + Headloss through an orifice in the exit manifold + + N_orifices : int + Number of orifices in the exit manifold + +""" +from aide_design.play import* + +# again we will change this to an important statment from the URL of aide_template repo +sed_dict = { + 'thickness_wall': 0.15*u.m, + 'plate_settlers': { + 'angle': 60*u.deg, 'S': 2.5*u.cm, + 'thickness': 2*u.mm, 'L_cantilevered': 20*u.cm, + }, + 'tank': { + 'W': 42*u.inch, 'L': 5.8*u.m, 'vel_up': 1*u.mm/u.s + }, + 'manifold': { + 'ratio_Q_man_orifice': 0.8, + 'diffuser': { + 'thickness_wall': 1.17*u.inch, 'vel_max': 442.9*u.mm/u.s, + 'A': 0.419*u.inch**2 + }, + 'exit_man': { + 'hl_orifice': 4*u.cm, 'N_orifices': 58 + } + } +} + +@u.wraps(None, [None], False) +def n_sed_plates_max(sed_inputs=sed_dict): + """Return the maximum possible number of plate settlers in a module given + plate spacing, thickness, angle, and unsupported length of plate settler. + + Parameters + ---------- + S_plate : float + Edge to edge distance between plate settlers + + thickness_plate : float + Thickness of PVC sheet used to make plate settlers + + L_sed_plate_cantilevered : float + Maximum length of sed plate sticking out past module pipes without any + additional support. The goal is to prevent floppy modules that don't + maintain constant distances between the plates + + angle_plate : float + Angle of plate settlers + + Returns + ------- + int + Maximum number of plates + + Examples + -------- + >>> from aide_design.play import* + >>> + + """ + B_plate = sed_inputs['plate_settlers']['S'] + sed_inputs['plate_settlers']['thickness'] + return math.floor((sed_inputs['plate_settlers']['L_cantilevered'].magnitude / B_plate.magnitude + * np.tan(sed_inputs['plate_settlers']['angle'].to(u.rad).magnitude)) + 1) + +@u.wraps(u.inch, [None], False) +def w_diffuser_inner_min(sed_inputs=sed_dict): + """Return the minimum inner width of each diffuser in the sedimentation tank. + + Parameters + ---------- + sed_inputs : dict + A dictionary of all of the constant inputs needed for sedimentation tank + calculations. Can be found in sed.yaml + + Returns + ------- + float + Minimum inner width of each diffuser in the sedimentation tank + + Examples + -------- + >>> from aide_design.play import* + >>> + + """ + return ((sed_inputs['tank']['vel_up'].to(u.inch/u.s).magnitude / + sed_inputs['manifold']['diffuser']['vel_max'].to(u.inch/u.s).magnitude) + * sed_inputs['tank']['W']) + +@u.wraps(u.m, [None], False) +def w_diffuser_inner(sed_inputs=sed_dict): + """Return the inner width of each diffuser in the sedimentation tank. + + Parameters + ---------- + sed_inputs : dict + A dictionary of all of the constant inputs needed for sedimentation tank + calculations can be found in sed.yaml + + Returns + ------- + float + Inner width of each diffuser in the sedimentation tank + + Examples + -------- + >>> from aide_design.play import* + >>> + + """ + return ut.ceil_nearest(w_diffuser_inner_min(sed_inputs).magnitude, + (np.arange(1/16,1/4,1/16)*u.inch).magnitude) + +@u.wraps(u.m, [None], False) +def w_diffuser_outer(sed_inputs=sed_dict): + """Return the outer width of each diffuser in the sedimentation tank. + + Parameters + ---------- + sed_inputs : dict + A dictionary of all of the constant inputs needed for sedimentation tank + calculations can be found in sed.yaml + + Returns + ------- + float + Outer width of each diffuser in the sedimentation tank + + Examples + -------- + >>> from aide_design.play import* + >>> + + """ + return (w_diffuser_inner_min(sed_inputs['tank']['W']) + + (2 * sed_inputs['manifold']['diffuser']['thickness_wall'])).to(u.m).magnitude + +@u.wraps(u.m, [None], False) +def L_diffuser_outer(sed_inputs=sed_dict): + """Return the outer length of each diffuser in the sedimentation tank. + + Parameters + ---------- + sed_inputs : dict + A dictionary of all of the constant inputs needed for sedimentation tank + calculations can be found in sed.yaml + + Returns + ------- + float + Outer length of each diffuser in the sedimentation tank + + Examples + -------- + >>> from aide_design.play import* + >>> + + """ + return ((sed_inputs['manifold']['diffuser']['A'] / + (2 * sed_inputs['manifold']['diffuser']['thickness_wall'])) + - w_diffuser_inner(sed_inputs).to(u.inch)).to(u.m).magnitude + +@u.wraps(u.m, [None], False) +def L_diffuser_inner(sed_inputs=sed_dict): + """Return the inner length of each diffuser in the sedimentation tank. + + Parameters + ---------- + sed_inputs : dict + A dictionary of all of the constant inputs needed for sedimentation tank + calculations can be found in sed.yaml + + Returns + ------- + float + Inner length of each diffuser in the sedimentation tank + + Examples + -------- + >>> from aide_design.play import* + >>> + + """ + return L_diffuser_outer(sed_inputs['tank']['W']) - + (2 * (sed_inputs['manifold']['diffuser']['thickness_wall']).to(u.m)).magnitude) + +@u.wraps(u.m**3/u.s, [None], False) +def q_diffuser(sed_inputs=sed_dict): + """Return the flow through each diffuser. + + Parameters + ---------- + sed_inputs : dict + A dictionary of all of the constant inputs needed for sedimentation tank + calculations can be found in sed.yaml + + Returns + ------- + float + Flow through each diffuser in the sedimentation tank + + Examples + -------- + >>> from aide_design.play import* + >>> + + """ + return (sed_inputs['tank']['vel_up'].to(u.m/u.s) * + sed_inputs['tank']['W'].to(u.m) * + L_diffuser_outer(sed_inputs)).magnitude + +@u.wraps(u.m/u.s, [None], False) +def vel_sed_diffuser(sed_inputs=sed_dict): + """Return the velocity through each diffuser. + + Parameters + ---------- + sed_inputs : dict + A dictionary of all of the constant inputs needed for sedimentation tank + calculations can be found in sed.yaml + + Returns + ------- + float + Flow through each diffuser in the sedimentation tank + + Examples + -------- + >>> from aide_design.play import* + >>> + + """ + return (q_diffuser(sed_inputs).magnitude + / (w_diffuser_inner(w_tank) * L_diffuser_inner(w_tank)).magnitude) + +@u.wraps(u.m**3/u.s, [None], False) +def q_tank(sed_inputs=sed_dict): + """Return the maximum flow through one sedimentation tank. + + Parameters + ---------- + sed_inputs : dict + A dictionary of all of the constant inputs needed for sedimentation tank + calculations can be found in sed.yaml + + Returns + ------- + float + Maximum flow through one sedimentation tank + + Examples + -------- + >>> from aide_design.play import* + >>> + + """ + return (sed_inputs['tank']['L'] * sed_inputs['tank']['vel_up'].to(u.m/u.s) * + sed_inputs['tank']['W'].to(u.m)).magnitude + +@u.wraps(u.m/u.s, [None], False) +def vel_inlet_man_max(sed_inputs=sed_dict): + """Return the maximum velocity through the manifold. + + Parameters + ---------- + sed_inputs : dict + A dictionary of all of the constant inputs needed for sedimentation tank + calculations can be found in sed.yaml + + Returns + ------- + float + Maximum velocity through the manifold. + + Examples + -------- + >>> from aide_design.play import* + >>> + + """ + vel_manifold_max = (sed_inputs['diffuser']['vel_max'].to(u.m/u.s).magnitude * + sqrt(2*((1-(sed_inputs['manifold']['ratio_Q_man_orifice'])**2)) / + (((sed_inputs['manifold']['ratio_Q_man_orifice'])**2)+1))) + return vel_manifold_max + +@u.wraps(None, [u.m**3/u.s, None], False) +def n_tanks(Q_plant, sed_inputs=sed_dict): + """Return the number of sedimentation tanks required for a given flow rate. + + Parameters + ---------- + Q_plant : float + Total plant flow rate + + sed_inputs : dict + A dictionary of all of the constant inputs needed for sedimentation tank + calculations can be found in sed.yaml + + Returns + ------- + int + Number of sedimentation tanks required for a given flow rate. + + Examples + -------- + >>> from aide_design.play import* + >>> + + """ + q = q_tank(sed_inputs).magnitude + return (int(np.ceil(Q_plant / q))) + +@u.wraps(u.m, [u.m**3/u.s, None], False) +def L_channel(Q_plant, sed_inputs=sed_dict): + """Return the length of the inlet and exit channels for the sedimentation tank. + + Parameters + ---------- + Q_plant : float + Total plant flow rate + + sed_inputs : dict + A dictionary of all of the constant inputs needed for sedimentation tank + calculations can be found in sed.yaml + + Returns + ------- + float + Length of the inlet and exit channels for the sedimentation tank. + + Examples + -------- + >>> from aide_design.play import* + >>> + + """ + n_tanks = n_tanks(Q_plant, sed_inputs) + return ((n_tanks * sed_inputs['tank']['W']) + sed_inputs['thickness_wall'] + + ((n_tanks-1) * sed_inputs['thickness_wall'])) + +@u.wraps(u.m, [u.m**3/u.s, u.degK, None], False) +@ut.list_handler +def ID_exit_man(Q_plant, temp, sed_inputs=sed_dict): + """Return the inner diameter of the exit manifold by guessing an initial + diameter then iterating through pipe flow calculations until the answer + converges within 1%% error + + Parameters + ---------- + Q_plant : float + Total plant flow rate + + temp : float + Design temperature + + sed_inputs : dict + A dictionary of all of the constant inputs needed for sedimentation tank + calculations can be found in sed.yaml + + Returns + ------- + float + Inner diameter of the exit manifold + + Examples + -------- + >>> from aide_design.play import* + >>> + + """ + #Inputs do not need to be checked here because they are checked by + #functions this function calls. + nu = pc.viscosity_dynamic(temp) + hl = sed_input['manifold']['exit_man']['hl_orifice'].to(u.m) + L = sed_ipnut['manifold']['tank']['L'] + N_orifices = sed_inputs['manifold']['exit_man']['N_orifices'] + K_minor = con.K_MINOR_PIPE_EXIT + pipe_rough = mat.PIPE_ROUGH_PVC.to(u.m) + + D = max(diam_pipemajor(Q_plant, hl, L, nu, pipe_rough).magnitude, + diam_pipeminor(Q_plant, hl, K_minor).magnitude) + err = 1.00 + while err > 0.01: + D_prev = D + f = pc.fric(Q_plant, D_prev, nu, pipe_rough) + D = ((8*Q_plant**2 / pc.GRAVITY.magnitude * np.pi**2 * hl) * + (((f*L/D_prev + K_minor) * (1/3 * 1/) * + (1/3 + 1/(2 * N_orifices) + 1/(6 * N_orifices**2))) + / (1 - sed_inputs['manifold']['ratio_Q_orifice']**2)))**0.25 + err = abs(D_prev - D) / ((D + D_prev) / 2) + return D + +@u.wraps(u.m, [u.m**3/u.s, u.inch, None], False) +def D_exit_man_orifice(Q_plant, drill_bits, sed_inputs=sed_dict): + """Return the diameter of the orifices in the exit manifold for the sedimentation tank. + + Parameters + ---------- + Q_plant : float + Total plant flow rate + + drill_bits : list + List of possible drill bit sizes + + sed_inputs : dict + A dictionary of all of the constant inputs needed for sedimentation tank + calculations can be found in sed.yaml + + Returns + ------- + float + Diameter of the orifices in the exit manifold for the sedimentation tank. + + Examples + -------- + >>> from aide_design.play import* + >>> + """ + Q_orifice = Q_plant/sed_input['exit_man']['N_orifices'] + D_orifice = np.sqrt(Q_orifice**4)/(np.pi * con.RATIO_VC_ORIFICE * np.sqrt(2 * pc.GRAVITY.magnitude * sed_input['exit_man']['hl_orifice'].magnitude)) + return ut.ceil_nearest(D_orifice, drill_bits) + + +@u.wraps(u.m, [u.m**3/u.s, u.inch, None], False) +def L_sed_plate(sed_inputs=sed_dict): + """Return the length of a single plate in the plate settler module based on + achieving the desired capture velocity + + Parameters + ---------- + sed_inputs : dict + A dictionary of all of the constant inputs needed for sedimentation tank + calculations can be found in sed.yaml + + Returns + ------- + float + Length of a single plate + + Examples + -------- + >>> from aide_design.play import* + >>> + + """ + L_sed_plate = ((sed_input['plate_settlers']['S'] * ((sed_input['tank']['vel_up']/sed_input['plate_settlers']['vel_capture'])-1) + + sed_input['plate_settlers']['thickness'] * (sed_input['tank']['vel_up']/sed_input['plate_settlers']['vel_capture'])) + / (np.sin(sed_input['plate_settlers']['angle']) * np.cos(sed_input['plate_settlers']['angle'])) + ).to(u.m) + return L_sed_plate diff --git a/aide_design/units.py b/aide_design/units.py index 2aeb1173..755f13de 100644 --- a/aide_design/units.py +++ b/aide_design/units.py @@ -1,13 +1,4 @@ -""" -Created on Thu Jun 8 2017 - -@author: Monroe Weber-Shirk - - Last modified: Fri Aug 11 2017 -By: Ethan Keller - - -Module containing global `pint` unit registry. +"""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. @@ -18,6 +9,7 @@ registries raises an exception). This module contains a single global unit registry `unit_registry` that can be used by any number of other modules. + """ import os @@ -35,7 +27,7 @@ def set_sig_fig(n: int): """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 @@ -58,4 +50,4 @@ def set_sig_fig(n: int): unit_registry.default_format = '.' + str(n) + 'g' pd.options.display.float_format = ('{:,.' + str(n) + '}').format -unit_registry.load_definitions(os.path.join(os.path.dirname(__file__), "data/unit_definitions.txt")) \ No newline at end of file +unit_registry.load_definitions(os.path.join(os.path.dirname(__file__), "data/unit_definitions.txt")) diff --git a/aide_design/utility.py b/aide_design/utility.py index 85c612fd..b7f18170 100644 --- a/aide_design/utility.py +++ b/aide_design/utility.py @@ -1,11 +1,8 @@ -""" -Created on Sun Jun 11 - -@author: Monroe Weber-Shirk +"""This file provides basic utility functions such as significant figures which +can be used throughout the plant design. -Last modified: Fri Aug 5 2017 -By: Sage Weber-Shirk """ + # units allows us to include units in all of our calculations import math @@ -21,8 +18,8 @@ #that are not significant. def sig(x,n): """Return the 1st input reduced to a number of significant digits. - - x is a number that may include units. n is the number of significant + + x is a number that may include units. n is the number of significant digits to display. """ # Check to see if the quantity x includes units so we can strip the @@ -33,7 +30,7 @@ def sig(x,n): if n==1 and xmag>=1: req = round(xmag) return '{:~P}'.format(u.Quantity(req,xunit)) - + else: xmag = x if xmag == 0.: @@ -79,7 +76,7 @@ def sig(x,n): out.append("0.") out.extend(["0"] * -(e + 1)) out.append(m) - + if type(x) == type(1 * u.m): req = "".join(out) return '{:~P}'.format(u.Quantity(req,xunit)) @@ -89,7 +86,7 @@ def sig(x,n): def stepceil_with_units(param, step, unit): """This function returns the smallest multiple of 'step' greater than or - equal to 'param' and outputs the result in Pint units. + equal to 'param' and outputs the result in Pint units. This function is unit-aware and functions without requiring translation so long as 'param' and 'unit' are of the same dimensionality. """ @@ -116,19 +113,19 @@ def list_handler(func): @functools.wraps(func) def wrapper(*args, HandlerResult="nparray", **kwargs): """Run through the wrapped function once for each array element. - + :param HandlerResult: output type. Defaults to numpy arrays. """ sequences = [] enumsUnitCheck = enumerate(args) argsList = list(args) - #This for loop identifies pint unit objects and strips them + #This for loop identifies pint unit objects and strips them #of their units. for num, arg in enumsUnitCheck: if type(arg) == type(1 * u.m): argsList[num] = arg.to_base_units().magnitude enumsUnitless = enumerate(argsList) - #This for loop identifies arguments that are sequences and + #This for loop identifies arguments that are sequences and #adds their index location to the list 'sequences'. for num, arg in enumsUnitless: if isinstance(arg, (list, tuple, np.ndarray)): @@ -138,9 +135,9 @@ def wrapper(*args, HandlerResult="nparray", **kwargs): if len(sequences) == 0: result = func(*args, **kwargs) else: - #iterant keeps track of how many times we've iterated and + #iterant keeps track of how many times we've iterated and #limiter stops the loop once we've iterated as many times - #as there are list elements. Without this check, a few + #as there are list elements. Without this check, a few #erroneous runs will occur, appending the last couple values #to the end of the list multiple times. # @@ -156,18 +153,18 @@ def wrapper(*args, HandlerResult="nparray", **kwargs): break #We can safely replace the entire list argument #with a single element from it because of the looping - #we're doing. We redefine the object, but that + #we're doing. We redefine the object, but that #definition remains within this namespace and does #not penetrate further up the function. argsList[num] = arg #Here we dive down the rabbit hole. This ends up #creating a multi-dimensional array shaped by the #sizes and shapes of the lists passed. - result.append(wrapper(*argsList, + result.append(wrapper(*argsList, HandlerResult=HandlerResult, **kwargs)) iterant += 1 - #HandlerResult allows the user to specify what type to - #return the generated sequence as. It defaults to numpy + #HandlerResult allows the user to specify what type to + #return the generated sequence as. It defaults to numpy #arrays because functions tend to handle them better, but if #the user does not wish to import numpy the base Python options #are available to them. @@ -183,17 +180,17 @@ def wrapper(*args, HandlerResult="nparray", **kwargs): def check_range(*args): """Check whether passed paramters fall within approved ranges. - + Does not return anything, but will raise an error if a parameter falls outside of its defined range. - + Input should be passed as an array of sequences, with each sequence having three elements: [0] is the value being checked, [1] is the range parameter(s) within which the value should fall, and [2] is the name of the parameter, for better error messages. If [2] is not supplied, "Input" will be appended as a generic name. - + Range requests that this function understands are listed in the knownChecks sequence. """ @@ -202,8 +199,8 @@ def check_range(*args): #Converts arg to a mutable list arg = [*arg] if len(arg) == 1: - #arg[1] details what range the parameter should fall within; if - #len(arg) is 1 that means a validity was not specified and the + #arg[1] details what range the parameter should fall within; if + #len(arg) is 1 that means a validity was not specified and the #parameter should not have been passed in its current form raise TypeError("No range-validity parameter provided.") elif len(arg) == 2: @@ -213,7 +210,7 @@ def check_range(*args): #This ensures that all whitespace is removed before checking if the #request is understood arg[1] = "".join(arg[1].lower().split()) - #This block checks that each range request is understood. + #This block checks that each range request is understood. #If the request is a compound one, it must be separated into individual #requests for validity comprehension for i in arg[1].split(","): @@ -243,4 +240,4 @@ def check_range(*args): "integer.".format(i, arg[2])) if 'boolean' in arg[1] and type(i) != bool: raise TypeError("{1} is {0} but must be a " - "boolean.".format(i, arg[2])) \ No newline at end of file + "boolean.".format(i, arg[2])) diff --git a/tests/test_k_value_of_reductions_utility.py b/tests/test_k_value_of_reductions_utility.py index 69719685..53850327 100644 --- a/tests/test_k_value_of_reductions_utility.py +++ b/tests/test_k_value_of_reductions_utility.py @@ -10,42 +10,42 @@ class KValuesCalculationTest(unittest.TestCase): # Test Reductions def test_k_value_reduction_square_turbulent(self): - self.assertEqual(k.k_value_reduction(pipe.OD(4), pipe.OD(2), 4 * u.L/u.s), 5.6677039356929662) + self.assertAlmostEqual(k.k_value_reduction(pipe.OD(4), pipe.OD(2), 4 * u.L/u.s), 5.6677039356929662) def test_k_value_reduction_laminar(self): - self.assertEqual(k.k_value_reduction(pipe.OD(1), pipe.OD(0.5), 0.1 * u.L / u.s), 2.1802730749680945) + self.assertAlmostEqual(k.k_value_reduction(pipe.OD(1), pipe.OD(0.5), 0.1 * u.L / u.s), 2.1802730749680945) def test_k_value_reduction_from_very_large_pipe_turbulent(self): - self.assertEqual(k.k_value_reduction(pipe.OD(400), pipe.OD(4), 4 * u.L / u.s), 105560.31724275621) + self.assertAlmostEqual(k.k_value_reduction(pipe.OD(400), pipe.OD(4), 4 * u.L / u.s), 105560.31724275621) # Test Expansions def test_k_value_expansion_into_large_tank(self): - self.assertEqual(k.k_value_expansion(pipe.OD(4), pipe.OD(400), 4 * u.L / u.s), 1.0110511331493719) + self.assertAlmostEqual(k.k_value_expansion(pipe.OD(4), pipe.OD(400), 4 * u.L / u.s), 1.0110511331493719) def test_k_value_expansion_into_very_large_pipe_laminar(self): - self.assertEqual(k.k_value_expansion(pipe.OD(1), pipe.OD(400), 0.1 * u.L / u.s), 1.0216612503304363) + self.assertAlmostEqual(k.k_value_expansion(pipe.OD(1), pipe.OD(400), 0.1 * u.L / u.s), 1.0216612503304363) # Test Orifices # Test private functions def test_k_value_thick_orifice_high_headloss(self): - self.assertEqual(k._k_value_thick_orifice(0.02, 0.002, 0.000002, 2), 1594340.3320537778) + self.assertAlmostEqual(k._k_value_thick_orifice(0.02, 0.002, 0.000002, 2), 1594340.3320537778) def test_k_value_thin_orifice_high_headloss(self): - self.assertEqual(k._k_value_thin_sharp_orifice(0.02, 0.002, 2), 1594433.5406999998) + self.assertAlmostEqual(k._k_value_thin_sharp_orifice(0.02, 0.002, 2), 1594433.5406999998) # Test public function def test_k_value_thin_orifice_regular_high_headloss(self): - self.assertEqual(k.k_value_orifice(0.02 * u.m, 0.002 * u.m, 0 * u.m, 1*u.L/u.s), 1697.9866773221295) + self.assertAlmostEqual(k.k_value_orifice(0.02 * u.m, 0.002 * u.m, 0 * u.m, 1*u.L/u.s), 1697.9866773221295) def test_k_value_super_thick_orifice_high_headloss(self): - self.assertEqual(k.k_value_orifice(pipe.OD(6), pipe.OD(4), 60*u.inch, 1 * u.L / u.s), 1.8350488368427034) + self.assertAlmostEqual(k.k_value_orifice(pipe.OD(6), pipe.OD(4), 60*u.inch, 1 * u.L / u.s), 1.8350488368427034) def test_k_value_thin_orifice(self): - self.assertEqual(k.k_value_orifice(pipe.OD(6), pipe.OD(4), 0*u.inch, 1 * u.L / u.s), 3.3497584836648246) + self.assertAlmostEqual(k.k_value_orifice(pipe.OD(6), pipe.OD(4), 0*u.inch, 1 * u.L / u.s), 3.3497584836648246) def test_k_value_thick_orifice(self): - self.assertEqual(k.k_value_orifice(pipe.OD(6), pipe.OD(4), 1*u.inch, 1 * u.L / u.s), + self.assertAlmostEqual(k.k_value_orifice(pipe.OD(6), pipe.OD(4), 1*u.inch, 1 * u.L / u.s), 2.9070736824641181) diff --git a/tests/test_lfom.py b/tests/test_lfom.py index dd5ebca2..1bf27c9e 100644 --- a/tests/test_lfom.py +++ b/tests/test_lfom.py @@ -7,21 +7,167 @@ from aide_design import physchem as pc -from aide_design import expert_inputs as exp - from aide_design import utility as ut +from aide_design import optional_inputs as opt + +from aide_design import pipedatabase as pipe + +from aide_design import materials_database as mat + +from aide_design import constants as con + from aide_design.units import unit_registry as u -from aide_design.unit_process_design.prefab import lfom_prefab_functional as lfom +from aide_design.unit_process_design import lfom class LfomTest(unittest.TestCase): + def test_width_stout(self): + """"width_stout should give known result with known input. + Test cases were calculated using outputs from original Mathcad code. + + """ + checks = ((20 * u.cm, 1 * u.cm, 11.408649616179787 * u.s/u.m**2), + (40 * u.cm, 40 * u.cm, 0.9019329453483474 * u.s/u.m**2)) + for i in checks: + with self.subTest(i=i): + self.assertAlmostEqual(lfom.width_stout(i[0], i[1]), i[2], places=3) + + def test_n_lfom_rows(self): + """"n_lfom_rows should give known result with known input. + Test cases were calculated using outputs from original Mathcad code. + + """ + checks = ((60 * u.L/u.s, 20 * u.cm, 4), + (60 * u.L/u.s, 40 * u.cm, 8), + (20 * u.L/u.s, 20 * u.cm, 8), + (1 * u.L/u.s, 20 * u.cm, 8), + (1 * u.L/u.s, 40 * u.cm, 8)) + for i in checks: + with self.subTest(i=i): + self.assertAlmostEqual(lfom.n_lfom_rows(i[0], i[1]), i[2]) + + def test_dist_center_lfom_rows(self): + """dist_center_lfom_rows should give known result with known input. + Test cases were calculated using outputs from original Mathcad code. + + """ + checks = ((60 * u.L/u.s, 20 * u.cm, 0.05 * u.m), + (20 * u.L/u.s, 20 * u.cm, 0.025 * u.m), + (1 * u.L/u.s, 40 * u.cm, 0.05 * u.m)) + for i in checks: + with self.subTest(i=i): + self.assertAlmostEqual(lfom.dist_center_lfom_rows(i[0], i[1]), i[2]) + + def test_vel_lfom_pipe_critical(self): + """vel_lfom_pipe_critical should give known result with known input. + Test cases were calculated using outputs from original Mathcad code. + + """ + checks = ((20 * u.cm, 0.8405802802312778 * u.m/u.s), + (40 * u.cm, 1.18876003256645 * u.m/u.s)) + for i in checks: + with self.subTest(i=i): + self.assertAlmostEqual(lfom.vel_lfom_pipe_critical(i[0]), i[1]) + + def test_area_lfom_pipe_min(self): + """area_lfom_pipe_min should give known result with known input. + Test cases were calculated using outputs from original Mathcad code. + + """ + checks = ((60 * u.L/u.s, 20 * u.cm, 0.10706889290245702 * u.m**2), + (60 * u.L/u.s, 40 * u.cm, 0.07570914022546356 * u.m**2), + (20 * u.L/u.s, 20 * u.cm, 0.035689630967485675 * u.m**2), + (20 * u.L/u.s, 40 * u.cm, 0.02523638007515452 * u.m**2)) + for i in checks: + with self.subTest(i=i): + self.assertAlmostEqual(lfom.area_lfom_pipe_min(i[0], i[1]), i[2]) + + def test_nom_diam_lfom_pipe(self): + """nom_diam_lfom_pipe should give known result with known input. + Test cases were calculated using outputs from original Mathcad code and + the nominal diameter function written in pipedatabase which has already + been tested. + + """ + checks = ((60 * u.L/u.s, 20 * u.cm, 16.0 * u.inch), + (60 * u.L/u.s, 40 * u.cm, 16.0 * u.inch), + (20 * u.L/u.s, 20 * u.cm, 10.0 * u.inch), + (20 * u.L/u.s, 40 * u.cm, 8.0 * u.inch)) + for i in checks: + with self.subTest(i=i): + self.assertAlmostEqual(lfom.nom_diam_lfom_pipe(i[0], i[1]), i[2]) + +##### Below here is not finished + def test_area_lfom_orifices_top(self): + """area_lfom_orifices_top should give known result with known input. + Test cases were calculated using outputs from original Mathcad code and + the nominal diameter function written in pipedatabase which has already + been tested. + + """ + checks = ((60 * u.L/u.s, 20 * u.cm, 0.008181566649077958 * u.m**2), + (60 * u.L/u.s, 40 * u.cm, 0.0027945370213839633 * u.m**2), + (20 * u.L/u.s, 20 * u.cm, 0.0013173573853983045 * u.m**2), + (20 * u.L/u.s, 40 * u.cm, 0.0009315123404613211 * u.m**2)) + for i in checks: + with self.subTest(i=i): + self.assertAlmostEqual(lfom.area_lfom_orifices_top(i[0], i[1]), i[2]) + + def test_d_lfom_orifices_max(self): + """d_lfom_orifices_max should give known result with known input. + Test cases were calculated using outputs from original Mathcad code and + the nominal diameter function written in pipedatabase which has already + been tested. + + """ + checks = ((60 * u.L/u.s, 20 * u.cm, 0.10206416704942245* u.m), + (60 * u.L/u.s, 40 * u.cm, 0.05964993750920847* u.m), + (20 * u.L/u.s, 20 * u.cm, 0.04095499380586013* u.m), + (20 * u.L/u.s, 40 * u.cm, 0.03443890747808586* u.m)) + for i in checks: + with self.subTest(i=i): + self.assertAlmostEqual(lfom.d_lfom_orifices_max(i[0], i[1]), i[2]) + + def test_orifice_diameter(self): + """orifice_diameter should give known result with known input. + Test cases were calculated using outputs from original Mathcad code and + the nominal diameter function written in pipedatabase which has already + been tested. + + """ + checks = ((60 * u.L/u.s, 20 * u.cm, mat.DIAM_DRILL_ENG, 1* u.m), + (60 * u.L/u.s, 40 * u.cm, mat.DIAM_DRILL_ENG, 1* u.m), + (20 * u.L/u.s, 20 * u.cm, mat.DIAM_DRILL_ENG, 1* u.m), + (20 * u.L/u.s, 40 * u.cm, mat.DIAM_DRILL_ENG, 1* u.m)) + for i in checks: + with self.subTest(i=i): + self.assertAlmostEqual(lfom.orifice_diameter(i[0], i[1], i[2]), i[3]) + + def test_drillbit_area(self): + """drillbit_area should give known result with known input. + Test cases were calculated using outputs from original Mathcad code and + the nominal diameter function written in pipedatabase which has already + been tested. + + """ + checks = ((60 * u.L/u.s, 20 * u.cm, mat.DIAM_DRILL_ENG, 0.0007669903939428206 * u.m**2), + (60 * u.L/u.s, 40 * u.cm, mat.DIAM_DRILL_ENG, 0.0007669903939428206 * u.m**2), + (20 * u.L/u.s, 20 * u.cm, mat.DIAM_DRILL_ENG, 3.141592653589793 * u.m**2), + (20 * u.L/u.s, 40 * u.cm, mat.DIAM_DRILL_ENG, 0.0007669903939428206 * u.m**2)) + for i in checks: + with self.subTest(i=i): + self.assertAlmostEqual(lfom.drillbit_area(i[0], i[1], i[2]), i[3]) + + + + def test_orifice_diameter(self): FLOW = 31 * u.L / u.s HL_LFOM = 20 * u.cm drill_bits = np.arange(5, 25, 5) * u.mm - self.assertEqual(lfom.orifice_diameter(FLOW,HL_LFOM,drill_bits), 15* u.mm) + self.assertAlmostEqual(lfom.orifice_diameter(FLOW,HL_LFOM,drill_bits), 0.7874015748031495* u.m) if __name__ == '__main__': diff --git a/tests/test_physchem.py b/tests/test_physchem.py index 6acc85d6..20d3c4ac 100644 --- a/tests/test_physchem.py +++ b/tests/test_physchem.py @@ -23,7 +23,7 @@ def test_area_circle(self): (495.6, 192908.99423885669*u.m**2)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.area_circle(i[0]), i[1]) + self.assertAlmostEqual(pc.area_circle(i[0]), i[1]) def test_area_circle_units(self): """area_circle should should give known result with known input and correct units""" @@ -31,7 +31,7 @@ def test_area_circle_units(self): (495.6*u.cm, 19.290899423885669*u.m**2)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.area_circle(i[0]), i[1]) + self.assertAlmostEqual(pc.area_circle(i[0]), i[1]) def test_area_circle_range(self): """area_circle should return errors with inputs <= 0.""" @@ -48,7 +48,7 @@ def test_diam_circle(self): (10000 * u.cm**2, 1.1283791670955126)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.diam_circle(i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.diam_circle(i[0]).magnitude, i[1]) def test_diam_circle_range(self): """diam_circle should return errors with inputs <= 0.""" @@ -69,14 +69,14 @@ def test_water_table(self): (11, 373.15, 958.4)) for i in checks: with self.subTest(i=i): - self.assertEqual(table[0][i[0]], i[1]) - self.assertEqual(table[1][i[0]], i[2]) + self.assertAlmostEqual(table[0][i[0]], i[1]) + self.assertAlmostEqual(table[1][i[0]], i[2]) def test_water_table_units(self): """The water density table should handle units properly.""" table = pc.WATER_DENSITY_TABLE - self.assertEqual(table[0][0], (0 * u.degC).to_base_units().magnitude) - self.assertEqual(table[0][4], (30 * u.degC).to_base_units().magnitude) + self.assertAlmostEqual(table[0][0], (0 * u.degC).to_base_units().magnitude) + self.assertAlmostEqual(table[0][4], (30 * u.degC).to_base_units().magnitude) def test_density_water_true(self): """density_water should give known result with known input.""" @@ -85,7 +85,7 @@ def test_density_water_true(self): (343.15, 977.8)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.density_water(i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.density_water(i[0]).magnitude, i[1]) def test_viscosity_dynamic(self): """viscosity_dynamic should give known result with known input.""" @@ -94,7 +94,7 @@ def test_viscosity_dynamic(self): (274, 0.0017060470223965783)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.viscosity_dynamic(i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.viscosity_dynamic(i[0]).magnitude, i[1]) def test_viscosity_dynamic_units(self): """viscosity_dynamic should give known result with known input.""" @@ -102,7 +102,7 @@ def test_viscosity_dynamic_units(self): (26.85 * u.degC, 0.0008540578046518858)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.viscosity_dynamic(i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.viscosity_dynamic(i[0]).magnitude, i[1]) def test_viscosity_kinematic(self): """viscosity_kinematic should give known results with known input.""" @@ -112,7 +112,7 @@ def test_viscosity_kinematic(self): (373.15, 2.9108883329847625e-07)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.viscosity_kinematic(i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.viscosity_kinematic(i[0]).magnitude, i[1]) def test_viscosity_kinematic_units(self): """viscosity_kinematic should handle units correctly.""" @@ -122,8 +122,8 @@ def test_viscosity_kinematic_units(self): (100 * u.degC, 2.9108883329847625e-07)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.viscosity_kinematic(i[0]).magnitude, i[1]) - self.assertEqual(pc.viscosity_kinematic(i[0]), + self.assertAlmostEqual(pc.viscosity_kinematic(i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.viscosity_kinematic(i[0]), (pc.viscosity_dynamic(i[0]) / pc.density_water(i[0]))) @@ -136,7 +136,7 @@ def test_re_pipe(self): ((1, 12, .45), 0.23578510087688198)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.re_pipe(*i[0]), i[1]) + self.assertAlmostEqual(pc.re_pipe(*i[0]), i[1]) def test_re_pipe_range(self): """re_pipe should raise errors when inputs are out of bounds.""" @@ -153,11 +153,11 @@ def test_re_pipe_units(self): [12, 0.006 * u.km, 100 * u.cm**2/u.s]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.re_pipe(*i), base) + self.assertAlmostEqual(pc.re_pipe(*i), base) def test_re_rect(self): """re_rect should return known result with known input.""" - self.assertEqual(pc.re_rect(10, 4, 6, 1, True), 2.5) + self.assertAlmostEqual(pc.re_rect(10, 4, 6, 1, True), 2.5) def test_re_rect_range(self): """re_rect should raise errors when inputs are out of bounds.""" @@ -176,7 +176,7 @@ def test_re_rect_units(self): [10, 4, 6, 0.0001 * u.ha/u.s, True]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.re_rect(*i), base) + self.assertAlmostEqual(pc.re_rect(*i), base) def test_re_general(self): """re_general should return known values with known input.""" @@ -185,7 +185,7 @@ def test_re_general(self): ([0, 1, 2, 0.3], 0)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.re_general(*i[0]), i[1]) + self.assertAlmostEqual(pc.re_general(*i[0]), i[1]) def test_re_general_range(self): """re_general should raise errors when inputs are out of bounds.""" @@ -204,7 +204,7 @@ def test_re_general_units(self): [1, 2, 3, 4000 * u.cm**2/u.s]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.re_general(*i), base) + self.assertAlmostEqual(pc.re_general(*i), base) class RadiusFuncsTest(unittest.TestCase): @@ -215,7 +215,7 @@ def test_radius_hydraulic(self): ([10, 4, True], 2.2222222222222223)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.radius_hydraulic(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.radius_hydraulic(*i[0]).magnitude, i[1]) def test_radius_hydraulic_range(self): """radius_hydraulic should raise errors when inputs are out of bounds.""" @@ -233,14 +233,14 @@ def test_radius_hydraulic_units(self): [0.01 * u.km, 40 * u.dm, False]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.radius_hydraulic(*i), base) + self.assertAlmostEqual(pc.radius_hydraulic(*i), base) def test_radius_hydraulic_general(self): """radius_hydraulic_general should return known results with known input.""" checks = (([6, 12], 0.5), ([70, 0.4], 175)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.radius_hydraulic_general(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.radius_hydraulic_general(*i[0]).magnitude, i[1]) def test_radius_hydraulic_general_range(self): """radius_hydraulic_general should not accept inputs of 0 or less.""" @@ -256,7 +256,7 @@ def test_radius_hydraulic_general_units(self): [4, 0.007 * u.km]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.radius_hydraulic_general(*i), base) + self.assertAlmostEqual(pc.radius_hydraulic_general(*i), base) class FrictionFuncsTest(unittest.TestCase): @@ -270,7 +270,7 @@ def test_fric(self): ([55, 0.4, 0.5, 0.0001], 0.18278357257249706)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.fric(*i[0]), i[1]) + self.assertAlmostEqual(pc.fric(*i[0]), i[1]) def test_fric_range(self): """fric should raise an error if 0 <= PipeRough <= 1 is not true.""" @@ -290,7 +290,7 @@ def test_fric_units(self): [100, 2, 0.001, 1000 * u.mm]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.fric(*i), base) + self.assertAlmostEqual(pc.fric(*i), base) def test_fric_rect(self): """fric_rect should return known results with known inputs.""" @@ -302,7 +302,7 @@ def test_fric_rect(self): ([120, 1, 0.04, 0.125, 0, True], 0.042098136441473824)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.fric_rect(*i[0]), i[1]) + self.assertAlmostEqual(pc.fric_rect(*i[0]), i[1]) def test_fric_rect_range(self): """fric_rect should raise an error if 0 <= PipeRough <= 1 is not true.""" @@ -323,7 +323,7 @@ def test_fric_rect_units(self): [0.06, 0.1, 0.0625, 0.347, 6 * u.cm, True]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.fric_rect(*i), base) + self.assertAlmostEqual(pc.fric_rect(*i), base) def test_fric_general(self): """fric_general should return known results with known inputs.""" @@ -332,7 +332,7 @@ def test_fric_general(self): ([120, 0.6, 12, 0.3, 0.002], 0.023024557179148988)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.fric_general(*i[0]), i[1]) + self.assertAlmostEqual(pc.fric_general(*i[0]), i[1]) def test_fric_general_range(self): """fric_general should raise an error if 0 <= PipeRough <= 1 is not true.""" @@ -353,7 +353,7 @@ def test_fric_general_units(self): [46.2, 0.75, 1.23, 0.46, 2 * u.mm]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.fric_general(*i), base) + self.assertAlmostEqual(pc.fric_general(*i), base) class HeadlossFuncsTest(unittest.TestCase): @@ -367,7 +367,7 @@ def test_headloss_fric(self): ([55, 0.4, 2, 0.5, 0.0001], 8926.108171551185)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_fric(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.headloss_fric(*i[0]).magnitude, i[1]) def test_headloss_fric_range(self): """headloss_fric should raise an error if Length <= 0.""" @@ -388,11 +388,11 @@ def test_headloss_fric_units(self): [100, 2, 4, 0.001, 3 * u.cm]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_fric(*i), base) + self.assertAlmostEqual(pc.headloss_fric(*i), base) def test_headloss_exp(self): """headloss_exp should return known results with known input.""" - self.assertEqual(pc.headloss_exp(60, 0.9, 0.067).magnitude, + self.assertAlmostEqual(pc.headloss_exp(60, 0.9, 0.067).magnitude, 30.386230766265214) def test_headloss_exp_range(self): @@ -411,7 +411,7 @@ def test_headloss_exp_units(self): [60, 900 * u.mm, 0.067]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_exp(*i).magnitude, base) + self.assertAlmostEqual(pc.headloss_exp(*i).magnitude, base) def test_headloss(self): """headloss should return known results with known inputs.""" @@ -422,7 +422,7 @@ def test_headloss(self): ([55, 0.4, 2, 0.5, 0.0001, 0.12], 10098.131417963332)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.headloss(*i[0]).magnitude, i[1]) def test_headloss_units(self): """headloss should handle units correctly.""" @@ -436,7 +436,7 @@ def test_headloss_units(self): [100, 2, 4, 0.001, 100 * u.cm, 2]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss(*i), base) + self.assertAlmostEqual(pc.headloss(*i), base) def test_headloss_fric_rect(self): """headloss_fric_rect should return known result with known inputs.""" @@ -444,7 +444,7 @@ def test_headloss_fric_rect(self): ([0.06, 3, 0.2, 4, 0.5, 0.006, False], 4.640841787063992)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_fric_rect(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.headloss_fric_rect(*i[0]).magnitude, i[1]) def test_headloss_fric_rect_range(self): """headloss_fric_rect should raise an error when Length <=0.""" @@ -466,12 +466,12 @@ def test_headloss_fric_rect_units(self): [0.06, 2, 0.004, 3, 0.89, 7 * u.cm, True]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_fric_rect(*i), base) + self.assertAlmostEqual(pc.headloss_fric_rect(*i), base) def test_headloss_exp_rect(self): """headloss_exp_rect should return known result for known input.""" checks = ([0.06, 2, 0.004, 1], 2.8679518490004234) - self.assertEqual(pc.headloss_exp_rect(*checks[0]).magnitude, checks[1]) + self.assertAlmostEqual(pc.headloss_exp_rect(*checks[0]).magnitude, checks[1]) def test_headloss_exp_rect_range(self): """headloss_exp_rect should raise errors when inputs are out of bounds.""" @@ -490,7 +490,7 @@ def test_headloss_exp_rect_units(self): [0.06, 2, 900 * u.mm, 1]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_exp_rect(*i), base) + self.assertAlmostEqual(pc.headloss_exp_rect(*i), base) def test_headloss_rect(self): """headloss_rect should return known result for known inputs.""" @@ -498,7 +498,7 @@ def test_headloss_rect(self): ([0.06, 3, 0.2, 4, 1, 0.5, 0.006, False], 4.641351645170481)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_rect(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.headloss_rect(*i[0]).magnitude, i[1]) def test_headloss_rect_units(self): """headloss_rect should handle units properly.""" @@ -513,7 +513,7 @@ def test_headloss_rect_units(self): [0.06, 3, 0.2, 4, 1, 0.5, 6 * u.mm, True]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_rect(*i), base) + self.assertAlmostEqual(pc.headloss_rect(*i), base) self.assertRaises(ValueError, pc.headloss_rect, *[1, 1, 1, 1, 1 * u.m, 1, 1, True]) @@ -523,7 +523,7 @@ def test_headloss_fric_general(self): ([25, 4, 0.6, 2, 1, 1], 0.006265136412536391)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_fric_general(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.headloss_fric_general(*i[0]).magnitude, i[1]) def test_headloss_fric_general_range(self): """headloss_fric_general should raise an error when Length <= 0.""" @@ -545,11 +545,11 @@ def test_headloss_fric_general_units(self): [36, 5, 0.2, 6, 0.4, 2 * u.mm]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_fric_general(*i), base) + self.assertAlmostEqual(pc.headloss_fric_general(*i), base) def test_headloss_exp_general(self): """headloss_exp_general should return known result for known input.""" - self.assertEqual(pc.headloss_exp_general(0.06, 0.02).magnitude, + self.assertAlmostEqual(pc.headloss_exp_general(0.06, 0.02).magnitude, 3.670978366720542e-06) def test_headloss_exp_general_range(self): @@ -567,7 +567,7 @@ def test_headloss_exp_general_units(self): [6 * u.cm/u.s, 0.02]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_exp_general(*i).magnitude, base) + self.assertAlmostEqual(pc.headloss_exp_general(*i).magnitude, base) def test_headloss_gen(self): """headloss_gen should return known value for known inputs.""" @@ -575,7 +575,7 @@ def test_headloss_gen(self): ([49, 2.4, 12, 3, 2, 4, 0.6], 0.9396236839032805)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_gen(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.headloss_gen(*i[0]).magnitude, i[1]) def test_headloss_gen_units(self): """headloss_gen should handle units correctly.""" @@ -590,7 +590,7 @@ def test_headloss_gen_units(self): [49, 2.4, 12, 3, 2, 4, 60 * u.cm]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_gen(*i).magnitude, base) + self.assertAlmostEqual(pc.headloss_gen(*i).magnitude, base) def test_headloss_manifold(self): """headloss_manifold should return known value for known input.""" @@ -598,7 +598,7 @@ def test_headloss_manifold(self): ([2, 6, 40, 5, 1.1, 0.04, 6], 0.11938889890999548)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_manifold(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.headloss_manifold(*i[0]).magnitude, i[1]) def test_headloss_manifold_range(self): """headloss_manifold should object if NumOutlets is not a positive int.""" @@ -617,7 +617,7 @@ def test_headloss_manifold_units(self): base = pc.headloss_manifold(2, 6, 40, 5, 1.1, 0.04, 6).magnitude unitchecks = ([2 * u.m**3/u.s, 6 * u.m, 40 * u.m, 5* u.dimensionless, 1.1 * u.m**2/u.s, 0.04 * u.m, 6* u.dimensionless], - [2000000 * u.cm**3/u.s, 6, 40, 5, 1.1, 0.04, 6], + [2 * u.m**3/u.s, 6, 40, 5, 1.1, 0.04, 6], [2, 6000 * u.mm, 40, 5, 1.1, 0.04, 6], [2, 6, 0.04 * u.km, 5, 1.1, 0.04, 6], [2, 6, 40, 5, 11000 * u.cm**2/u.s, 0.04, 6], @@ -637,7 +637,7 @@ def test_flow_orifice(self): ([1.4, 0.1, 0], 0)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_orifice(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.flow_orifice(*i[0]).magnitude, i[1]) def test_flow_orifice_range(self): """flow_orifice should raise errors when inputs are out of bounds.""" @@ -655,7 +655,7 @@ def test_flow_orifice_units(self): (2, 0.003 * u.km, 0.5)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_orifice(*i).magnitude, base) + self.assertAlmostEqual(pc.flow_orifice(*i).magnitude, base) def test_flow_orifice_vert(self): """flow_orifice_vert should return known values for known inputs.""" @@ -663,7 +663,7 @@ def test_flow_orifice_vert(self): ([0.3, 4, 0.67], 0.41946278400781861), ([2, -4, 0.2], 0)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_orifice_vert(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.flow_orifice_vert(*i[0]).magnitude, i[1]) def test_flow_orifice_vert_range(self): """flow_orifice_vert should raise errors when inputs are out of bounds.""" @@ -684,7 +684,7 @@ def test_flow_orifice_vert_units(self): [1, 0.003 * u.km, 0.4]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_orifice_vert(*i).magnitude, base) + self.assertAlmostEqual(pc.flow_orifice_vert(*i).magnitude, base) def test_head_orifice(self): """head_orifice should return known value for known inputs.""" @@ -693,7 +693,7 @@ def test_head_orifice(self): ([2, 0.5, 0.04], 3.3062033177025895e-05)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.head_orifice(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.head_orifice(*i[0]).magnitude, i[1]) def test_head_orifice_range(self): """head_orifice should raise errors when passed invalid inputs.""" @@ -712,7 +712,7 @@ def test_head_orifice_units(self): [2, 0.5, 40 * u.L/u.s]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.head_orifice(*i).magnitude, base) + self.assertAlmostEqual(pc.head_orifice(*i).magnitude, base) def test_area_orifice(self): """area_orifice should return known value for known inputs.""" @@ -721,7 +721,7 @@ def test_area_orifice(self): ([0.5, 0.02, 3], 47.899493517158803)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.area_orifice(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.area_orifice(*i[0]).magnitude, i[1]) def test_area_orifice_range(self): """area_orifice should raise errors when inputs are out of bounds.""" @@ -739,7 +739,7 @@ def test_area_orifice_units(self): [3, 0.4, 60 * u.L/u.s]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.area_orifice(*i).magnitude, base) + self.assertAlmostEqual(pc.area_orifice(*i).magnitude, base) def test_num_orifices(self): """num_orifices should return known value for known inputs.""" @@ -747,7 +747,7 @@ def test_num_orifices(self): ([6, 0.8, 0.08, 1.2], 6)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.num_orifices(*i[0]), i[1]) + self.assertAlmostEqual(pc.num_orifices(*i[0]), i[1]) def test_num_orifices_units(self): """num_orifices should handle units correctly.""" @@ -758,7 +758,7 @@ def test_num_orifices_units(self): [6, 0.8, 0.08, 0.0012 * u.km]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.num_orifices(*i), base) + self.assertAlmostEqual(pc.num_orifices(*i), base) class FlowFuncsTest(unittest.TestCase): """Test the flow functions.""" @@ -768,7 +768,7 @@ def test_flow_transition(self): ([0.8, 1.1], 1451.4158059584847)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_transition(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.flow_transition(*i[0]).magnitude, i[1]) def test_flow_transition_range(self): """flow_transition should not accept inputs <= 0.""" @@ -785,7 +785,7 @@ def test_flow_transition_units(self): [2, 4000 * u.cm**2/u.s]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_transition(*i).magnitude, base) + self.assertAlmostEqual(pc.flow_transition(*i).magnitude, base) def test_flow_hagen(self): """flow_hagen should return known value for known inputs.""" @@ -793,7 +793,7 @@ def test_flow_hagen(self): ([0.05, 0.0006, 0.3, 1.1], 2.7351295806397676e-09)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_hagen(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.flow_hagen(*i[0]).magnitude, i[1]) def test_flow_hagen_range(self): """flow_hagen should raise errors when inputs are out of bounds.""" @@ -816,14 +816,14 @@ def test_flow_hagen_units(self): [0.05, 0.0006, 0.3, 11000 * u.cm**2/u.s]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_hagen(*i).magnitude, base) + self.assertAlmostEqual(pc.flow_hagen(*i).magnitude, base) def test_flow_swamee(self): """flow_swamee should return known value for known inputs.""" checks = (([2, 0.04, 3, 0.1, 0.37], 2.9565931732010045),) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_swamee(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.flow_swamee(*i[0]).magnitude, i[1]) def test_flow_swamee_range(self): """flow_swamee should raise errors when inputs are out of bounds.""" @@ -848,7 +848,7 @@ def test_flow_swamee_units(self): [2, 0.04, 3, 0.1, 37 * u.cm]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_swamee(*i).magnitude, base) + self.assertAlmostEqual(pc.flow_swamee(*i).magnitude, base) def test_flow_pipemajor(self): """flow_pipemajor should return known result for known inputs.""" @@ -856,7 +856,7 @@ def test_flow_pipemajor(self): ([2, 0.62, 0.5, 0.036, 0.23], 62.457206502701297)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_pipemajor(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.flow_pipemajor(*i[0]).magnitude, i[1]) def test_flow_pipemajor_units(self): """flow_pipemajor should handle units correctly.""" @@ -870,11 +870,11 @@ def test_flow_pipemajor_units(self): [2, 0.62, 0.5, 0.036, 23 * u.cm]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_pipemajor(*i).magnitude, base) + self.assertAlmostEqual(pc.flow_pipemajor(*i).magnitude, base) def test_flow_pipeminor(self): """flow_pipeminor should return known results for known input.""" - self.assertEqual(pc.flow_pipeminor(1, 0.125, 3).magnitude, + self.assertAlmostEqual(pc.flow_pipeminor(1, 0.125, 3).magnitude, 0.71000203931611083) def test_flow_pipeminor_range(self): @@ -896,7 +896,7 @@ def test_flow_pipeminor_units(self): (1, 125 * u.mm, 3)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_pipeminor(*i).magnitude, base) + self.assertAlmostEqual(pc.flow_pipeminor(*i).magnitude, base) def test_flow_pipe(self): """flow_pipe should return known value for known inputs.""" @@ -904,7 +904,7 @@ def test_flow_pipe(self): ([0.25, 0.4, 2, 0.58, 0.029, 0.35], 0.000324206539183988)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_pipe(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.flow_pipe(*i[0]).magnitude, i[1]) def test_flow_pipe_units(self): """flow_pipe should handle units correctly.""" @@ -918,14 +918,14 @@ def test_flow_pipe_units(self): [0.25, 0.4, 2, 0.58, 29 * u.mm, 0.35]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_pipe(*i).magnitude, base) + self.assertAlmostEqual(pc.flow_pipe(*i).magnitude, base) class DiamFuncsTest(unittest.TestCase): """Test the diameter functions.""" def test_diam_hagen(self): """diam_hagen should return known value for known inputs.""" - self.assertEqual(pc.diam_hagen(0.006, 0.00025, 0.75, 0.0004).magnitude, + self.assertAlmostEqual(pc.diam_hagen(0.006, 0.00025, 0.75, 0.0004).magnitude, 0.4158799465199102) def test_diam_hagen_range(self): @@ -947,11 +947,11 @@ def test_diam_hagen_units(self): [0.006, 0.00025, 0.75, 4 * u.cm**2/u.s]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.diam_hagen(*i).magnitude, base) + self.assertAlmostEqual(pc.diam_hagen(*i).magnitude, base) def test_diam_swamee(self): """diam_swamee should return known value for known input.""" - self.assertEqual(pc.diam_swamee(0.06, 1.2, 7, 0.2, 0.0004).magnitude, + self.assertAlmostEqual(pc.diam_swamee(0.06, 1.2, 7, 0.2, 0.0004).magnitude, 0.19286307314945772) def test_diam_swamee_range(self): @@ -978,7 +978,7 @@ def test_diam_swamee_units(self): [0.06, 1.2, 7, 0.2, 0.4 * u.mm]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.diam_swamee(*i).magnitude, base) + self.assertAlmostEqual(pc.diam_swamee(*i).magnitude, base) def test_diam_pipemajor(self): """diam_pipemajor should return known value for known inputs.""" @@ -986,7 +986,7 @@ def test_diam_pipemajor(self): ([1, 2, 0.03, 0.004, 0.005], 0.14865504303291951)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.diam_pipemajor(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.diam_pipemajor(*i[0]).magnitude, i[1]) def test_diam_pipemajor_units(self): """diam_pipemajor should handle units correctly.""" @@ -1000,7 +1000,7 @@ def test_diam_pipemajor_units(self): [1, 2, 0.03, 0.004, 5 * u.mm]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.diam_pipemajor(*i).magnitude, base) + self.assertAlmostEqual(pc.diam_pipemajor(*i).magnitude, base) def test_diam_pipeminor(self): """diam_pipeminor should return known value for known inputs.""" @@ -1008,7 +1008,7 @@ def test_diam_pipeminor(self): ([0.015, 0.3, 0.472], 0.073547549463488848)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.diam_pipeminor(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.diam_pipeminor(*i[0]).magnitude, i[1]) def test_diam_pipeminor_range(self): """diam_pipeminor should raise errors when inputs are out of bounds.""" @@ -1029,7 +1029,7 @@ def test_diam_pipeminor_units(self): [0.008, 1.2 * u.cm, 0.93]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.diam_pipeminor(*i).magnitude, base) + self.assertAlmostEqual(pc.diam_pipeminor(*i).magnitude, base) def test_diam_pipe(self): """diam_pipe should return known value for known inputs.""" @@ -1037,7 +1037,7 @@ def test_diam_pipe(self): ([0.007, 0.04, 0.75, 0.16, 0.0079, 0.8], 0.5436137491479152)) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.diam_pipe(*i[0]).magnitude, i[1]) + self.assertAlmostEqual(pc.diam_pipe(*i[0]).magnitude, i[1]) def test_diam_pipe_units(self): """diam_pipe should handle units correctly.""" @@ -1051,14 +1051,14 @@ def test_diam_pipe_units(self): [0.007, 0.04, 0.75, 0.16, 7.9 * u.mm, 0.8]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.diam_pipe(*i).magnitude, base) + self.assertAlmostEqual(pc.diam_pipe(*i).magnitude, base) class WeirFuncsTest(unittest.TestCase): """Test the weir functions.""" def test_width_rect_weir(self): """width_rect_weir should return known value for known inputs.""" - self.assertEqual(pc.width_rect_weir(0.005, 0.2).magnitude, - 0.030538608524736166) + self.assertAlmostEqual(pc.width_rect_weir(0.005, 0.2).magnitude, + 0.030, places=3) def test_width_rect_weir_range(self): """width_rect_weird should raise errors when inputs are out of bounds.""" @@ -1076,12 +1076,12 @@ def test_width_rect_weir_units(self): [0.005, 20 * u.cm]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.width_rect_weir(*i).magnitude, base) + self.assertAlmostEqual(pc.width_rect_weir(*i).magnitude, base) def test_headloss_weir(self): """headloss_weir should return known value for known inputs.""" - self.assertEqual(pc.headloss_weir(0.005, 1).magnitude, - 0.019540221940287855) + self.assertAlmostEqual(pc.headloss_weir(0.005, 1).magnitude, + 0.019, places=3) def test_headloss_weir_range(self): """headloss_weir should raise errors when inputs are out of bounds.""" @@ -1099,11 +1099,11 @@ def test_headloss_weir_units(self): [0.005, 100 * u.cm]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_weir(*i).magnitude, base) + self.assertAlmostEqual(pc.headloss_weir(*i).magnitude, base) def test_flow_rect_weir(self): """flow_rect_weir should return known value for known inputs.""" - self.assertEqual(pc.flow_rect_weir(2, 1).magnitude, 5.1775077728360559) + self.assertAlmostEqual(pc.flow_rect_weir(2, 1).magnitude, 5.261, places=3) def test_flow_rect_weir_range(self): """flow_rect_weir should raise errors when inputs are out of bounds.""" @@ -1121,14 +1121,14 @@ def test_flow_rect_weir_units(self): [2, 100 * u.cm]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.flow_rect_weir(*i).magnitude, base) + self.assertAlmostEqual(pc.flow_rect_weir(*i).magnitude, base) class MiscPhysFuncsTest(unittest.TestCase): """Test the miscellaneous physchem functions.""" def test_height_water_critical(self): """height_water_critical should return known value for known inputs.""" - self.assertEqual(pc.height_water_critical(0.006, 1.2).magnitude, + self.assertAlmostEqual(pc.height_water_critical(0.006, 1.2).magnitude, 0.013660704939951886) def test_height_water_critical_range(self): @@ -1147,11 +1147,11 @@ def test_height_water_critical_units(self): [0.006, 120 * u.cm]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.height_water_critical(*i).magnitude, base) + self.assertAlmostEqual(pc.height_water_critical(*i).magnitude, base) def test_vel_horizontal(self): """vel_horizontal should return known value for known inputs.""" - self.assertEqual(pc.vel_horizontal(0.03).magnitude, 0.5424016039799292) + self.assertAlmostEqual(pc.vel_horizontal(0.03).magnitude, 0.5424016039799292) def test_vel_horizontal_range(self): """vel_horizontzal should raise an errors when input is <= 0.""" @@ -1163,11 +1163,11 @@ def test_vel_horizontal_units(self): checks = (0.03 * u.m, 3 * u.cm) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.vel_horizontal(i).magnitude, base) + self.assertAlmostEqual(pc.vel_horizontal(i).magnitude, base) def test_headloss_kozeny(self): """headloss_kozeny should return known value for known input.""" - self.assertEqual(pc.headloss_kozeny(1, 1.4, 0.5, 0.625, 0.8).magnitude, + self.assertAlmostEqual(pc.headloss_kozeny(1, 1.4, 0.5, 0.625, 0.8).magnitude, 2.1576362645214617) def test_headloss_kozeny_range(self): @@ -1191,7 +1191,7 @@ def test_headloss_kozeny_units(self): [1, 1.4, 0.5, 0.625, 8000 * u.cm**2/u.s]) for i in checks: with self.subTest(i=i): - self.assertEqual(pc.headloss_kozeny(*i).magnitude, base) + self.assertAlmostEqual(pc.headloss_kozeny(*i).magnitude, base) if __name__ == "__main__": diff --git a/tests/test_pipedatabase.py b/tests/test_pipedatabase.py index 3f573bbd..ae58e6ec 100644 --- a/tests/test_pipedatabase.py +++ b/tests/test_pipedatabase.py @@ -7,7 +7,7 @@ def test_OD(self): checks = [[1.0 * u.inch, 1.315 * u.inch]] for i in checks: with self.subTest(i=i): - self.assertEqual(pipe.OD(i[0]), i[1]) + self.assertAlmostEqual(pipe.OD(i[0]), i[1]) if __name__ == '__main__': unittest.main() diff --git a/tests/test_pipeline_utility.py b/tests/test_pipeline_utility.py index bbaef00b..f85ecdd5 100644 --- a/tests/test_pipeline_utility.py +++ b/tests/test_pipeline_utility.py @@ -8,7 +8,7 @@ class PipelineUtilityTest(unittest.TestCase): def test_pipeline_flow(self): flow = pipeline.flow_pipeline(np.array([3,4,5])*u.inch, np.array([3,4,5])*u.m, np.array([3,4,5]), 5 *u.m) - self.assertEqual(flow.magnitude, 0.018149097841279497 * u.m**3 / u.s) + self.assertAlmostEqual(flow.magnitude, 0.018149097841279497 * u.m**3 / u.s) if __name__ == '__main__':