Air Flow through a Duct
Contents
Air Flow through a Duct#
In this notebook it is shown how to size a single air duct using the package fluid_flow
which is part of the main package hvac
.
Let’s Import What We’ll Need#
from hvac import Quantity
from hvac.fluid_flow import Duct, Rectangular
from hvac.fluids import Fluid
We define a shortcut for instantiating Quantity
objects:
Q_ = Quantity
Define Standard Dry Air#
We use standard dry air for sizing the duct. The code below shows how standard air is defined.
Air = Fluid('Air')
STANDARD_AIR = Air(T=Q_(20, 'degC'), P=Q_(101_325, 'Pa'))
Sizing a Rectangular Duct#
A duct has a cross-section. Three types of cross-section are available:
circular
rectangular
flat-oval
Here we will size a rectangular duct. The height of the rectangular cross-section must be known beforehand. The width will then be calculated. To create a rectangular cross-section with a height of 100 mm, we write:
cross_section = Rectangular.create(height=Q_(100, 'mm'))
The required width of the duct will depend on the design volume flow rate through the duct and the selected specific pressure drop (i.e. per unit of duct length). To size a duct we need to specify:
the length of the duct
wall roughness (for a galvanized steel duct with medium roughness this is 0.09 mm)
the fluid that flows through the duct (in this case standard air as defined above)
the cross-section (of which the required width needs to be determined)
the volume flow rate of the fluid through the duct
either the specific pressure drop or the total pressure drop across the straight length of duct; in this example a specific pressure drop will be specified.
duct = Duct.create(
length=Q_(18.2, 'm'),
wall_roughness=Q_(0.09, 'mm'),
fluid=STANDARD_AIR,
cross_section=cross_section,
volume_flow_rate=Q_(500, 'm ** 3 / hr'),
specific_pressure_drop=Q_(1.28, 'Pa / m')
)
Once the duct is created with the parameters above, we can retrieve the required width of the duct’s cross-section through the cross_section
property of the Duct
object:
ja.display_list([
f"cross-section width of duct: <b>{duct.cross_section.width.to('mm'):~P.3f}</b>"
])
- cross-section width of duct: 370.567 mm
The equivalent diameter, hydraulic diameter, and the cross-section area of the duct can also be requested:
ja.display_list([
f"equivalent diameter: <b>{duct.cross_section.equivalent_diameter.to('mm'):~P.3f}</b>",
f"hydraulic diameter: <b>{duct.cross_section.hydraulic_diameter.to('mm'):~P.3f}</b>",
f"cross-section area: <b>{duct.cross_section.area.to('cm ** 2'):~P.3f}</b>"
])
- equivalent diameter: 200.138 mm
- hydraulic diameter: 157.498 mm
- cross-section area: 370.567 cm²
The total (friction) pressure drop over the whole length of duct can be attained by:
ja.display_list([
f"pressure drop: <b>{duct.pressure_drop.to('Pa'):~P.3f}</b>"
])
- pressure drop: 23.296 Pa
Sizing a Rectangular Duct with a Schedule#
In order to get a cross-section of the duct which is commercially available, we will couple a “schedule” (an instance of the DuctSchedule
class) with the cross-section. For rectangular ducts a default schedule is available:
from hvac.fluid_flow import rectangular_duct_schedule
To indicate that we want the nearest commercially available width, we add the parameter schedule
when creating the cross-section:
cross_section = Rectangular.create(height=Q_(100, 'mm'), schedule=rectangular_duct_schedule)
Now we create the duct like before:
duct = Duct.create(
length=Q_(18.2, 'm'),
wall_roughness=Q_(0.09, 'mm'),
fluid=STANDARD_AIR,
cross_section=cross_section,
volume_flow_rate=Q_(500, 'm ** 3 / hr'),
specific_pressure_drop=Q_(1.28, 'Pa / m')
)
ja.display_list([
f"cross-section width of duct: <b>{duct.cross_section.width.to('mm'):~P.1f}</b>"
])
- cross-section width of duct: 350.0 mm
ja.display_list([
f"equivalent diameter: <b>{duct.cross_section.equivalent_diameter.to('mm'):~P.3f}</b>",
f"hydraulic diameter: <b>{duct.cross_section.hydraulic_diameter.to('mm'):~P.3f}</b>",
f"cross-section area: <b>{duct.cross_section.area.to('cm ** 2'):~P.3f}</b>"
])
- equivalent diameter: 195.291 mm
- hydraulic diameter: 155.556 mm
- cross-section area: 350.000 cm²
As the commercially available width is different from the calculated value, the specific pressure drop that we’ve initially specified won’t be valid anymore:
ja.display_list([
f"specific pressure drop across duct: <b>{duct.specific_pressure_drop.to('Pa / m'):~P.3f}</b>",
f"pressure drop across duct: <b>{duct.pressure_drop.to('Pa'):~P.3f}</b>"
])
- specific pressure drop across duct: 1.439 Pa/m
- pressure drop across duct: 26.190 Pa
Other flow quantities that can be retrieved after the duct has been created, are:
ja.display_list([
f"the air velocity in the duct: <b>{duct.velocity.to('m / s'):~P.3f}</b>",
f"the reynolds number: <b>{duct.reynolds_number:.3f}</b>",
f"the velocity pressure: <b>{duct.velocity_pressure:~P.3f}</b>"
])
- the air velocity in the duct: 3.968 m/s
- the reynolds number: 40842.480
- the velocity pressure: 9.484 Pa
Air Volume Flow Rate through a Rectangular Duct#
Let’s keep the duct from the example above. Suppose that a pressure drop of 50 Pa exists along the duct. The question is now what air volume flow rate is causing this pressure drop.
duct.pressure_drop = Q_(50, 'Pa')
ja.display_list([
f"volume flow rate through duct = <b>{duct.volume_flow_rate.to('m ** 3 / hr'):~P.1f}</b>"
])
- volume flow rate through duct = 711.3 m³/h
Sizing a Circular Duct#
When sizing a circular duct for a given design volume flow rate and pressure drop, the only thing we know is that the duct has a circular cross-section. A circular cross-section is represented by the Circular
class.
from hvac.fluid_flow import Circular
Simply call create
on the Circular
class to create the cross-section:
cross_section = Circular.create()
Using the same design values as for the rectangular duct above, we create the circular duct in exactly the same way:
duct = Duct.create(
length=Q_(18.2, 'm'),
wall_roughness=Q_(0.09, 'mm'),
fluid=STANDARD_AIR,
cross_section=cross_section,
volume_flow_rate=Q_(500, 'm ** 3 / hr'),
specific_pressure_drop=Q_(1.28, 'Pa / m')
)
To get at the required internal diameter to establish the requested specific pressure drop at the design volume flow rate, we write:
ja.display_list([
f"cross-section internal diameter: <b>{duct.cross_section.internal_diameter.to('mm'):~P.3f}</b>"
])
- cross-section internal diameter: 200.138 mm
A circular cross-section also has the equivalent and hydraulic diameter being defined; obviously they are equal to the internal diameter.
ja.display_list([
f"equivalent diameter: <b>{duct.cross_section.equivalent_diameter.to('mm'):~P.3f}</b>",
f"hydraulic diameter: <b>{duct.cross_section.hydraulic_diameter.to('mm'):~P.3f}</b>",
f"cross-section area: <b>{duct.cross_section.area.to('cm ** 2'):~P.3f}</b>"
])
- equivalent diameter: 200.138 mm
- hydraulic diameter: 200.138 mm
- cross-section area: 314.593 cm²
As the length of the circular duct is the same as that of the rectangular duct, and as we have used the same specific pressure drop, the total pressure drop along the circular duct should be equal to the total pressure drop along the rectangular duct:
ja.display_list([
f"pressure drop: <b>{duct.pressure_drop.to('Pa'):~P.3f}</b>"
])
- pressure drop: 23.296 Pa
Notice that the equivalent diameter of the rectangular duct in the previous example matches with the internal diameter of the circular duct.
Sizing a Circular Duct with a Schedule#
In the same way as for the rectangular duct, we can assign a “schedule” to the cross-section of a circular duct in order to retrieve an internal diameter of the duct that is commercially available.
from hvac.fluid_flow import circular_duct_schedule
cross_section = Circular.create(schedule=circular_duct_schedule)
duct = Duct.create(
length=Q_(18.2, 'm'),
wall_roughness=Q_(0.09, 'mm'),
fluid=STANDARD_AIR,
cross_section=cross_section,
volume_flow_rate=Q_(500, 'm ** 3 / hr'),
specific_pressure_drop=Q_(1.28, 'Pa / m')
)
ja.display_list([
f"cross-section internal diameter: <b>{duct.cross_section.internal_diameter.to('mm'):~P.3f}</b>"
])
- cross-section internal diameter: 200.000 mm
ja.display_list([
f"specific pressure drop across duct: <b>{duct.specific_pressure_drop.to('Pa / m'):~P.3f}</b>",
f"pressure drop across duct: <b>{duct.pressure_drop.to('Pa'):~P.3f}</b>"
])
- specific pressure drop across duct: 1.284 Pa/m
- pressure drop across duct: 23.372 Pa
Replace a Circular Duct by an Equivalent Rectangular Duct#
Assume that we need to replace this circular duct by a rectangular duct while keeping the pressure drop the same. To accomplish this, we can create a rectangular cross-section of which the equivalent diameter is equal to the internal diameter of the circular duct. The height of the rectangular duct we can choose ourselves, but the width still needs to be calculated.
rect_cross_sect = Rectangular.create(height=Q_(100, 'mm'), equivalent_diameter=cross_section.internal_diameter)
ja.display_list([
f"width of the equivalent rectangular duct: <b>{rect_cross_sect.width.to('mm'):~P.3f}</b>"
])
- width of the equivalent rectangular duct: 369.970 mm
Let’s check if we still have the same pressure drop. Therefore, we create a rectangular duct with the same length and with the same air volume flow rate as the circular duct. When we create the rectangular duct, we don’t know the specific pressure drop nor the total pressure drop, as this is just what we are looking for. So we omit these parameters when instantiating the duct. The program will notice that the cross-section of the duct is fully determined and that the volume flow rate is given, so it deduces that it is pressure drop that needs to be calculated.
rect_duct = Duct.create(
length=Q_(18.2, 'm'),
wall_roughness=Q_(0.09, 'mm'),
fluid=STANDARD_AIR,
cross_section=rect_cross_sect,
volume_flow_rate=Q_(500, 'm ** 3 / hr')
)
ja.display_list([
f"specific pressure drop across duct: <b>{rect_duct.specific_pressure_drop.to('Pa / m'):~P.3f}</b>",
f"pressure drop across duct: <b>{rect_duct.pressure_drop.to('Pa'):~P.3f}</b>"
])
- specific pressure drop across duct: 1.281 Pa/m
- pressure drop across duct: 23.317 Pa
Sizing a Flat-Oval Duct#
Sizing a flat-oval duct is similar to sizing a rectangular duct. Instead of Rectangular
instantiate the cross-section from class FlatOval
. The corresponding default schedule is now flat_oval_duct_schedule
.
Adding a Fitting to a Rectangular Duct#
We will add a rectangular, mitered elbow to our last created rectangular duct. The loss coefficients of duct fittings are calculated based on the fitting loss coefficient tables in SMACNA’s handbook “HVAC SYSTEMS - DUCT DESIGN”, Fourth Edition (2006). The designation of the fittings available in the program corresponds with the designation of the fitting loss tables in the handbook. The loss coefficients of rectangular mitered elbows can be found in table A-7,D of SMACNA’s handbook. For a complete overview of all the available duct fittings see the file smacna.py
in the subpackage fluid_flow.fittings.duct
.
Adding a fitting to a duct is a two-step process. First create the fitting object, and then add it to the duct.
Create the Fitting
from hvac.fluid_flow.fittings.duct import ElbowA7D
elbow = ElbowA7D(ID='elbow1', duct=rect_duct, theta=Q_(90, 'deg'))
Notice that we need to pass the Duct
object, in this case rect_duct
, to the constructor of the fitting. Fittings can be assigned an ID and may need additional parameters, such as the angle of the elbow in this example.
The loss or resistance coefficient of a fitting can be retrieved through its property zeta
. The Greek letter zeta (ζ) is often used in literature to designate the loss coefficient of a fitting.
ja.display_list([
f"loss coefficient of elbow: <b>{elbow.zeta:.3f}</b>"
])
- loss coefficient of elbow: 1.488
Add the Fitting to the Duct
To distinguish between the different fittings in a duct, a name or ID can be assigned to a fitting. To add the fitting to the duct, we need to pass its loss coefficient and ID to the duct.
rect_duct.add_fitting(zeta=elbow.zeta, ID=elbow.ID)
Adding fittings to a duct, will mostly increase the pressure drop across the duct caused by the air flow. After the fitting has been added to the duct, we can check its effect on the pressure drop:
ja.display_list([
f"total pressure drop along the duct: <b>{rect_duct.pressure_drop.to('Pa'):~P.3f}</b>"
])
- total pressure drop along the duct: 35.945 Pa