First, we will define a base Pump class, much like we did for valves, as shown in the following code:
# pump.py (part 1)
1 #!/usr/bin/env python3 2 """ 3 VirtualPLC pump.py 4 5 Purpose: Creates a generic Pump class for PLC-controlled SCADA systems. 6 7 Classes: 8 Pump: Generic superclass 9 CentrifPump: Pump subclass; provides for a variable displacement pump 10 PositiveDisplacement: Pump subclass; provides for a positive displacement pump 11 12 Author: Cody Jackson 13 14 Date: 4/12/18 15 ################################# 16 Version 0.2 17 Added path extension to alleviate errors 18 Version 0.1 19 Initial build 20 """
Lines 1-20 are similar to what we've seen before, with the basic program information:
# pump.py (part 2)
1 import sys 2 sys.path.extend(["/home/cody/PycharmProjects/VirtualPLC"]) 3 import math 4 import numbers 5 from Utilities import utility_formulas 6 7 GRAVITY = 9.81 # m/s^2
In lines 1 and 2, notice that, like the Tank program, we have to extend the system's path to include the root program directory to prevent errors when importing the utility formulas.
Line 7 defines the gravitational constant, which is used when calculating the power used by the pump. Unlike our other constants, this one is in metric values, as it is necessary for the pump power calculation:
# pump.py (part 3)
1 class Pump: 2 """Generic class for pumps. 3 4 Displacement is the amount of fluid pushed through the pump per second. 5 Horsepower coefficient is the slope of the equivalent pump curve. 6 """ 7 def __init__(self, name="", flow_rate_out=0.0, pump_head_in=0.0, press_out=0.0, pump_speed=0): 8 """Set initial parameters.""" 9 self.name = name 10 self.__flow_rate_out = float(flow_rate_out) 11 self.head_in = float(pump_head_in) 12 self.__outlet_pressure = float(press_out) 13 self.__speed = pump_speed 14 self.__wattage = self.pump_power(self.__flow_rate_out, self.diff_press_psi(self.head_in, self.outlet_pressure))
Line 1 starts the generic Pump class. The docstring (lines 2-6) provides some basic information about the class, while lines 7-14 initialize the pump instance parameters. For a pump, we care about the following:
- The pump instance's name
- The flow rate coming out of the pump
- The net positive suction head at the pump's inlet—this value has to be higher than the pump's minimum requirement to prevent cavitation
- The pump's outlet pressure
- The speed of the pump, typically measured in revolutions per minute (rpm)
- The power used by the pump, measured in watts
# pump.py (part 4)
1 @property 2 def speed(self): 3 """Get the current speed of the pump.""" 4 return self.__speed 5 6 @speed.setter 7 def speed(self, new_speed): 8 """Change the pump speed.""" 9 try: 10 if not isinstance(new_speed, numbers.Number): 11 raise TypeError("Numeric values only.") 12 elif new_speed < 0: 13 raise ValueError("Speed must be 0 or greater.") 14 else: 15 self.__speed = new_speed 16 except TypeError: 17 raise # Re-raise error for testing 18 except ValueError: 19 raise # Re-raise error for testing
The preceding code block defines the pump speed property and setter methods. Like the previous examples, name mangling is used for property variables to ensure there is no confusion with other names in the program. Notice that we are re-raising the exceptions in lines 17 and 19 for use when we write our unit tests:
# pump.py (part 5)
1 @property 2 def outlet_pressure(self): 3 """Get the current outlet pressure of the pump.""" 4 return self.__outlet_pressure 5 6 @outlet_pressure.setter 7 def outlet_pressure(self, press): 8 """Set the pump outlet pressure.""" 9 self.__outlet_pressure = press 10 11 @property 12 def flow(self): 13 """Get the current outlet flow rate of the pump.""" 14 return self.__flow_rate_out
Lines 1-9 are the property and setter methods for the pump's outlet pressure, while lines 11-14 are for the output flow rate property method:
# pump.py (part 6)
1 @flow.setter 2 def flow(self, flow_rate): 3 """Set the pump outlet flow.""" 4 self.__flow_rate_out = flow_rate 5 6 @property 7 def power(self): 8 """Get the current power draw of the pump.""" 9 return self.__wattage 10 11 @power.setter 12 def power(self, power): 13 """Set the pump power.""" 14 self.__wattage = power
Lines 1-4 are the outlet flow rate setter method and lines 6-14 are for the pump's power usage:
# pump.py (part 7)
1 def pump_power(self, flow_rate, diff_head, fluid_spec_weight=62.4): 2 """Calculate pump power in kW. 3 4 Formula from https://www.engineeringtoolbox.com/pumps-power-d_505.html. 5 Because imperial values are converted to metric, the calculation isn't exactly the formula listed on the site; view the site's source code to see the formula used. 6 """ 7 flow_rate = flow_rate / 15852 8 density = fluid_spec_weight / 0.0624 9 head = diff_head / 3.2808 10 hyd_power = (100 * (flow_rate * density * GRAVITY * head) / 1000) / 100 11 self.power = hyd_power 12 return self.power
The actual power calculation is handled by the function defined in the preceding code listing. As this formula was taken from a third-party site, testing the output was compared to the website's output to ensure correct values were used. This is important to note when using any external tools; the Python program's results have to be compared to the same tool, as rounding errors or other problems may skew the results:
# pump.py (part 8)
1 @staticmethod 2 def diff_press_ft(in_press_ft, out_press_ft): 3 """Calculate differential head across pump, converted from feet.""" 4 in_press = utility_formulas.head_to_press(in_press_ft) 5 out_press = utility_formulas.head_to_press(out_press_ft) 6 delta_p = out_press - in_press 7 return delta_p 8 9 @staticmethod 10 def diff_press_psi(press_in, press_out): 11 """Calculate differential pump head.""" 12 delta_p = abs(press_out - press_in) # Account for Pout < Pin 13 return delta_p
The preceding code listing creates two static methods that calculate the pressure difference from the pump inlet to outlet; essentially, this is the increase in pressure from the pump. This method can be used by all instances of the class, as well as the class itself. Lines 1-7 return the pressure in feet of head pressure, while the static method in lines 9-13 returns PSI.