Created
December 26, 2022 05:49
-
-
Save OrangeChannel/f9aec0f0e0bd6371d20a68f7afe2479f to your computer and use it in GitHub Desktop.
v8.1
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# region --- Imports --------------------------------------------------------------------------------------------------- | |
from sympy import * # symbolical algebra library | |
from numbers import Real | |
import math | |
from fractions import Fraction | |
from functools import wraps | |
from typing import Any, TypeVar, Callable, Optional, Final, TypeAlias | |
from enum import Enum, auto | |
from matplotlib import pyplot as plt | |
from pint import UnitRegistry # import the unit handler | |
import numpy as np # fast mathematical calculations library | |
import pandas as pd # two-dimensional array (spreadsheets) library | |
from scipy.optimize import minimize_scalar | |
import re | |
# endregion | |
# region --- Initializing Imports -------------------------------------------------------------------------------------- | |
pd.options.display.max_columns = 20 | |
F = TypeVar("F", bound=Callable) | |
ureg = UnitRegistry() # initialize a registry to hold the unit conversions | |
ureg.setup_matplotlib(True) | |
Q = ureg.Quantity # alias `Q` to the quantity constructor | |
QUANTITYLIKE: TypeAlias = Q | str | Real | |
width = Q(8.5, "inch") | |
margins = Q(1.75, "cm") | |
w = (width - 2 * margins) * 0.85 | |
FIGSIZE = (w.to("inch").m, w.to("inch").m * 4 / 7) | |
# endregion | |
# region --- Riser Tracking Class -------------------------------------------------------------------------------------- | |
diversity = lambda x: np.interp( | |
x, | |
[0, 2, 4, 8, 10, 12, 16, 20, 24, 28, 32, 36, 40], | |
[1, 1, 0.88, 0.78, 0.7, 0.64, 0.57, 0.53, 0.47, 0.44, 0.41, 0.40, 0.40], | |
) | |
class SimpleRiser: | |
def __init__(self, riser_dict: dict[int, int], size_dict: Optional[dict[int, str]] = None): | |
""" | |
Given a dict {-1: 200, 0: 0, 1: 250, 2: 450, ...}, it assumes the airflow is given in CFM and the floor number is an int. | |
""" | |
self.raw_riser_dict = riser_dict | |
self.raw_size_dict = size_dict or dict() | |
self._calc_cfms() | |
self._init_sizes() | |
def run(self, dims: Optional[list[int]] = None): | |
return run(self.cfms.m, dims) | |
def iupdate(self, riser_dict: dict[int, int], size_dict: Optional[dict[int, str]] = None): | |
self.raw_riser_dict |= riser_dict | |
if size_dict is None: | |
self.raw_size_dict = None | |
else: | |
self.raw_size_dict |= size_dict | |
self._calc_cfms() | |
self._init_sizes() | |
def update(self, riser_dict: dict[int, int], size_dict: Optional[dict[int, str]] = None): | |
self.iupdate(riser_dict, size_dict) | |
return self | |
def __add__(self, other: "SimpleRiser"): | |
if not isinstance(other, SimpleRiser): | |
raise NotImplementedError | |
new_riser_dict = dict() | |
for floor_number in self.raw_riser_dict: | |
if floor_number in other.raw_riser_dict: | |
new_riser_dict |= { | |
floor_number: self.raw_riser_dict[floor_number] | |
+ other.raw_riser_dict[floor_number] | |
} | |
return SimpleRiser(new_riser_dict) | |
def __getitem__(self, item): | |
return Q(self.cfms_dict[item], "cfm") | |
def _calc_cfms(self): | |
self.cfms_dict = dict() | |
for i, (floor, cfm) in enumerate(self.raw_riser_dict.items()): | |
self.cfms_dict[floor] = sum(list(self.raw_riser_dict.values())[:i + 1]) | |
self.cfms = Q(list(self.cfms_dict.values()), "cfm") | |
def _init_sizes(self): | |
if self.raw_size_dict is None: | |
self.duct_dims = None | |
else: | |
self.duct_dims = dict() | |
for floor, dim in self.raw_size_dict.items(): | |
self.duct_dims[floor] = DuctDimension(dim) | |
def verify(self, max_vel: str | Q = "1200 fpm"): | |
verification = dict() | |
for floor in self.raw_size_dict.keys(): | |
if find_second_dimension(self[floor], self.duct_dims[floor][0], max_vel) <= self.duct_dims[floor][1]: | |
verification[floor] = True | |
else: | |
verification[floor] = False | |
return verification | |
def get_all_possible_dims(self, max_vel: str | Q = "1200 fpm"): | |
for (floor, cfm) in self.cfms_dict.items(): | |
print(f"Floor {floor}:") | |
get_all_possible_dims(cfm, max_vel) | |
print(f"----------------") | |
def __repr__(self): | |
return f"<SimpleRiser({self.raw_riser_dict, self.raw_size_dict})>" | |
class Diversity_Riser: | |
def __init__( | |
self, | |
connections_dict: dict[int, int], | |
cfm_per_connection: int, | |
diversity_factor: Optional[float] = None, | |
): | |
""" | |
Given a dict {-1: 2, 0: 0, 1: 3, 2: 1, ...}, it assumes the numbers given in values are number of connections and the floor number is an int. | |
TBD, quantify the cfms, make the cfms respect the floor numbering (dict), allow for use of a size_dict like Simple Riser | |
""" | |
self.connections_dict = connections_dict | |
self.branches = list(connections_dict.values()) | |
self.total_connections = sum(self.branches) | |
self.cfm_per_connection = cfm_per_connection | |
if diversity_factor is None: | |
self.diversity_factor = diversity(sum(self.branches)) | |
else: | |
self.diversity_factor = diversity_factor | |
self.worst_case_connections = math.ceil( | |
self.diversity_factor * self.total_connections | |
) | |
self.cfms: list[int] = [] | |
for i, _ in enumerate(self.branches): | |
if sum(self.branches[: i + 1]) <= self.worst_case_connections: | |
self.cfms.append(sum(self.branches[: i + 1]) * cfm_per_connection) | |
else: | |
self.cfms.append(self.worst_case_connections * cfm_per_connection) | |
self.riser_dict = { | |
k: v * cfm_per_connection | |
for k, v in zip(connections_dict.keys(), connections_dict.values()) | |
} | |
def run(self, dims: Optional[list[int]] = None): | |
return run(self.cfms, dims) | |
def __getitem__(self, item): | |
return self.cfms[item] | |
# endregion | |
# region --- Custom Unit Definitions ----------------------------------------------------------------------------------- | |
# fmt: off | |
ureg.define("@alias sq_ft = sf = sqft") | |
ureg.define("@alias cu_ft = cf") | |
ureg.define("@alias Btu = btu") | |
ureg.define("cfm = cu_ft / min") | |
ureg.define("cfs = cu_ft / second") | |
ureg.define("fpm = ft / min") | |
ureg.define("@alias inch_H2O_39F = in_WC = inch_WC") | |
ureg.define("inch_WC_per_100_ft = inch_H2O_39F per (100 * ft)") | |
ureg.define("@alias inch_WC_per_100_ft = in_WC_per_100_ft") | |
ureg.define("person = [people]") | |
ureg.define("[occupant_density] = [people] / [area]") | |
ureg.define("ppl_per_1000_sqft = person per (1000 * sqft)") | |
ureg.define("btuh = Btu / hr") | |
ureg.define("mbh = 1000 * btuh") | |
ureg.define("percent = 0.01") | |
ureg.define("noise_criteria = [NC] = NC") | |
ureg.define("mcf = 1000 * cf") | |
ureg.define("brake_horsepower = hp = bhp") | |
# fmt: on | |
# endregion ------------------------------------------------------------------------------------------------------------ | |
# region --- Add String Support for Future TUI/GUI --------------------------------------------------------------------- | |
def accept_strings(func: F) -> F: | |
@wraps(func) | |
def inner(*args: Any): | |
return func(*[(Q(x) if isinstance(x, str) else x) for x in args]) | |
return inner | |
# endregion | |
# region --- Enums ----------------------------------------------------------------------------------------------------- | |
class LinearDiffuserMounting(Enum): | |
SIDEWALL = auto() | |
CEILING = auto() | |
SIDEWALL = LinearDiffuserMounting.SIDEWALL | |
CEILING = LinearDiffuserMounting.CEILING | |
class PlenumBoxConnection(Enum): | |
CENTER = auto() | |
END = auto() | |
CENTER = PlenumBoxConnection.CENTER | |
END = PlenumBoxConnection.END | |
class AirFlowDirection(Enum): | |
SUPPLY = auto() | |
RETURN = auto() | |
SUPPLY = AirFlowDirection.SUPPLY | |
RETURN = AirFlowDirection.RETURN | |
# endregion | |
# region --- Architectual Dimension ------------------------------------------------------------------------------------ | |
class ArchitectualLength: | |
def __init__(self, value1: int, value2: Optional[int] = None, value3: Optional[Real] = None, subprecision: Fraction = Fraction(1, 32)): | |
""" | |
Python representation of an architectural length quantity. Helpful for pretty-printing and quick inputting from | |
a REPL. Supports | |
Examples:: | |
>>> q = Q(10.8188125, 'meter') | |
>>> s = r"16'-3 1/4\\"" # fix for dumb docstring bug in pycharm, string needs to be input correctly | |
>>> x, y, z = ArchitectualLength(3, 0, 0), ArchitectualLength(s), ArchitectualLength(q) | |
>>> x, y, z | |
(<ArchitectualLength(3, 0, 0)>, <ArchitectualLength(16, 3, 1/4)>, <ArchitectualLength(35, 5, 8444249301319921/9007199254740992)>) | |
>>> print(x, "\t\t", y, "\t\t", z) | |
3'-0 1/4" 16'-3 1/4" 35'-5 15/16" | |
>>> x.Q() | |
<Quantity(3.02083333, 'foot')> | |
""" | |
self.subprecision = subprecision | |
if (value2 is None) and (value3 is None): | |
if isinstance(value1, Q): | |
self._from_quantity(quant=value1) | |
elif isinstance(value1, str): | |
self._from_string(string=value1) | |
else: | |
self.feet = value1 | |
self.inch = value2 | |
self.subinch = Fraction(value3) | |
def as_quantity(self): | |
return Q(self.feet, "feet") + Q(self.inch, "inch") + Q(float(self.subinch), "inch") | |
Q = as_quantity | |
def __str__(self): | |
if (self.feet == 0) and (self.subinch == 0): | |
return f"{self.inch}\"" # 0" / 3" | |
elif (self.feet == 0) and (self.inch == 0): | |
return f"{self.subinch.limit_denominator(self.subprecision.denominator)}\"" # 1/4" | |
elif self.feet == 0: | |
return f"{self.inch} {self.subinch.limit_denominator(self.subprecision.denominator)}\"" | |
elif (self.inch == 0) and (self.subinch == 0): | |
return f"{self.feet}'" | |
elif self.subinch == 0: | |
return f"{self.feet}'-{self.inch}\"" | |
else: | |
return f"{self.feet}'-{self.inch} {self.subinch.limit_denominator(self.subprecision.denominator)}\"" | |
def __repr__(self): | |
return f"<ArchitectualLength({self.feet}, {self.inch}, {self.subinch})>" | |
def _from_string(self, string: str): | |
p = re.compile(r"(?P<feet>\d+\')?-?(?P<inches>\d+[ \d/]*\")?") | |
m = p.search(string) | |
if m.group('feet'): | |
feet = int(m.group('feet')[:-1]) | |
else: | |
feet = 0 | |
if m.group('inches'): | |
_inches = m.group('inches').split(" ") | |
inches = int(_inches[0].replace('"', '')) | |
if len(_inches) == 2: | |
partial_inches = Fraction(_inches[1].replace('"', '')) | |
else: | |
partial_inches = 0 | |
else: | |
inches = 0 | |
partial_inches = 0 | |
self.feet = feet | |
self.inch = inches | |
self.subinch = partial_inches | |
def _from_quantity(self, quant: Q): | |
feet = Q(math.floor(quant.to("ft").m), "ft") | |
self.feet = feet.m | |
quant -= feet | |
inches = Q(math.floor(quant.to("inch").m), "inch") | |
self.inch = inches.m | |
quant -= inches | |
self.subinch = Fraction(quant.to("inch").m) | |
# endregion | |
# region --- Physical Constants ---------------------------------------------------------------------------------------- | |
air_density: Final[Q] = Q("0.075 lb / ft^3") | |
_air_viscosity: Final[Q] = Q("0.0432 lb / ft / hr") | |
air_kin_viscosity: Final[Q] = _air_viscosity / air_density | |
specific_heat: Final[Q] = Q("0.24 Btu / lb / delta_degF") | |
energy_factor: Final[Q] = Q("1.08 Btu / hr / delta_degF / cfm") | |
absolute_roughness: Final[Q] = Q("0.0003 ft") | |
# endregion | |
# region --- Defined Constants ----------------------------------------------------------------------------------------- | |
max_vel: Final[Q] = Q("1200 fpm") | |
max_head_loss: Final[Q] = Q("0.1 in_WC_per_100_ft") | |
max_AR: Final[float] = 3.5 | |
# endregion | |
# region --- Helper Functions ------------------------------------------------------------------------------------------ | |
def _round_up_to_nearest_int(x: Real | Q, multiple: Real, abs_tol: float = 0.1) -> int: | |
""" | |
Rounds a numerical value, ``x``, up to the nearest ``multiple`` (integer). Will round down if ``x`` is only | |
``abs_tol`` greater than the closest multiple of ``multiple``. | |
:param x: Numerical value to be rounded up. | |
:param multiple: Integer multiple to round up to. | |
:param abs_tol: Absolute difference acceptable between ``x`` and the nearest multiple of ``multiple`` such that | |
``x`` is greater than this multiple and will be rounded down. | |
""" | |
if isinstance(x, Q): | |
if x.m < multiple: | |
return Q(multiple, x.u) | |
elif (x.m % multiple) < abs_tol: | |
return np.floor(x) | |
else: | |
return np.ceil(x / multiple) * multiple | |
else: | |
if x < multiple: | |
return multiple | |
elif (x % multiple) < abs_tol: | |
return math.floor(x) | |
else: | |
return math.ceil(x / multiple) * multiple | |
class DuctDimension: | |
def __init__(self, string: str): | |
string = string.lower() | |
L, W = string.split("x") | |
self.L = Q(f"{L} inch") | |
self.W = Q(f"{W} inch") | |
self.D_H = equiv_diameter_rect_duct(self.L, self.W) | |
def __repr__(self): | |
if self.L >= self.W: | |
return f"{self.L.m}x{self.W.m}" | |
else: | |
return f"{self.W.m}x{self.L.m}" | |
__str__ = __repr__ | |
def __getitem__(self, item): | |
"""Makes it so you can use in functions that require both dimensions of the duct. | |
Example: | |
x = DuctDimension('8x6') | |
f(*x[:]) to call f with 8 inches and 6 inches as the first two arguments. | |
""" | |
return [self.L, self.W][item] | |
def stats(self, airflow: str | Q): | |
get_airflow_stats(self.L, self.W, airflow) | |
# endregion | |
# region --- Standalone Math Functions --------------------------------------------------------------------------------- | |
@accept_strings | |
@ureg.wraps("", ("m", "", "m")) | |
def Niazkar_friction_factor( | |
hydraulic_diameter: Q, | |
Reynolds_number: Q, | |
roughness_factor: Q = absolute_roughness, | |
) -> Q: # [doi:10.1007/s12205-019-2217-1] | |
""" | |
Calculate friction factor based on pipe diameter, reynolds number, and surface conditions. | |
:param hydraulic_diameter: [m] | |
:param Reynolds_number: [-] | |
:param roughness_factor: [m] (default set to ``absolute_roughness``) | |
:return: Niazkar friction factor [-] | |
""" | |
if math.isclose(hydraulic_diameter, 0, abs_tol=1e-9): | |
return 0 | |
D = hydraulic_diameter | |
Re = Reynolds_number | |
ε = roughness_factor | |
p_1 = 4.5547 | |
p_2 = 0.8784 | |
A = -2 * math.log10(ε / (3.7 * D) + p_1 / pow(Re, p_2)) | |
B = -2 * math.log10(ε / (3.7 * D) + 2.51 * A / Re) | |
C = -2 * math.log10(ε / (3.7 * D) + 2.51 * B / Re) | |
return pow(A - pow(B - A, 2) / (C - 2 * B + A), -2) | |
@accept_strings | |
@ureg.wraps("m", ("m^3 / s", "", "Pa / m", "kg / m^3")) | |
def hydraulic_diameter( | |
volumetric_flow_rate: Q, | |
friction_factor: Q, | |
head_loss: Q = max_head_loss, | |
fluid_density: Q = air_density, | |
) -> Q: | |
""" | |
Calculate necesary pipe diameter to achieve a volumetric flow rate based on friction factor, head loss, and | |
fluid density. | |
:param volumetric_flow_rate: [m^3/s] | |
:param friction_factor: [-] | |
:param head_loss: [Pa/m] (default set to ``max_head_loss``) | |
:param fluid_density: [kg/m^3] (default set to ``air_density``) | |
:return: Minimum pipe diameter [m] | |
""" | |
if math.isclose(volumetric_flow_rate, 0, abs_tol=1e-9): | |
return 0 | |
f = friction_factor | |
ρ = fluid_density | |
H_L = head_loss | |
Q_dot = volumetric_flow_rate | |
return pow( | |
8 * pow(Q_dot, 2) * f * ρ / pow(np.pi, 2) / H_L, | |
1 / 5, | |
) | |
@accept_strings | |
@ureg.wraps("", ("m", "m/s", "m^2 / s")) | |
def Reynolds_number( | |
hydraulic_diameter: Q, | |
velocity: Q, | |
kinematic_viscosity: Q = air_kin_viscosity, | |
) -> Real: | |
""" | |
Calculate Reynold's number based on pipe diameter, fluid velocity, and fluid kinematic viscosity. | |
:param hydraulic_diameter: [m] | |
:param velocity: [m/s] | |
:param kinematic_viscosity: [m^2/s] | |
:return: Reynold's number [-] | |
""" | |
if math.isclose(hydraulic_diameter, 0, abs_tol=1e-9): | |
return 0 | |
D_h = hydraulic_diameter | |
V = velocity | |
ν = kinematic_viscosity | |
return D_h * V / ν | |
@accept_strings | |
@ureg.wraps("m/s", ("m^3 / s", "m^2")) | |
def fluid_velocity(volumetric_flow_rate: Q, orifice_area: Q) -> Q: | |
""" | |
Calculate fluid velocity through a given area based on volumetric flow rate. | |
:param volumetric_flow_rate: [m^3/s] | |
:param orifice_area: [m^2] | |
:return: Fluid velocity [m/s] | |
""" | |
if math.isclose(volumetric_flow_rate, 0, abs_tol=1e-9): | |
return 0 | |
if math.isclose(orifice_area, 0, abs_tol=1e-9): | |
return 0 | |
Q_dot = volumetric_flow_rate | |
A = orifice_area | |
return Q_dot / A | |
@accept_strings | |
@ureg.wraps("m^2", "m") | |
def pipe_area(internal_diameter: Q) -> Q: | |
""" | |
Calculate pipe area based on pipe diameter. | |
:param internal_diameter: [m] | |
:return: Pipe area [m^2] | |
""" | |
if math.isclose(internal_diameter, 0, abs_tol=1e-9): | |
return 0 | |
D = internal_diameter | |
return math.pi / 4 * pow(D, 2) | |
@accept_strings | |
@ureg.wraps("inch", ("inch", "inch")) | |
def equiv_diameter_rect_duct(a: Q, b: Q) -> Q: | |
""" | |
Finds equivalent round duct given length and width of a rectangular duct (all in inches). | |
:param a: [inch] | |
:param b: [inch] | |
:return: [inch] | |
""" | |
if math.isclose(a, 0, abs_tol=1e-9): | |
return 0 | |
if math.isclose(b, 0, abs_tol=1e-9): | |
return 0 | |
return 1.30 * pow(a * b, 0.625) / pow(a + b, 0.250) | |
# endregion | |
# region --- Convenience Backend Wrappers ------------------------------------------------------------------------------ | |
@accept_strings | |
def fluid_velocity_thru_pipe(volumetric_flow_rate: Q, internal_diameter: Q) -> Q: | |
""" | |
Wrapper for :py:func:fluid_velocity(). | |
Calculate fluid velocity through a pipe given volumetric flow rate and pipe diameter. | |
""" | |
Q_dot = volumetric_flow_rate | |
ID = internal_diameter | |
A = pipe_area(ID) | |
return fluid_velocity(Q_dot, A) | |
@accept_strings | |
def fluid_velocity_thru_rect_duct_dims(volumetric_flow_rate: Q, a: Q, b: Q) -> Q: | |
""" | |
Wrapper for :py:func:fluid_velocity(). | |
Calculate fluid velocity through a rectangular cross section. | |
""" | |
Q_dot = volumetric_flow_rate | |
D_h = equiv_diameter_rect_duct(a, b) | |
A = pipe_area(D_h) | |
return fluid_velocity(Q_dot, A) | |
@accept_strings | |
def find_hydraulic_diameter( | |
volumetric_flow_rate: Q, | |
head_loss: Q = max_head_loss, | |
fluid_density: Q = air_density, | |
roughness_factor: Q = absolute_roughness, | |
fluid_kinematic_viscosity: Q = air_kin_viscosity, | |
min_bound: Q = Q(1e-4, "m"), | |
max_bound: Q = Q(5, "m"), | |
) -> Q: | |
""" | |
Finds hydraulic diameter based only on flow rate and headloss. | |
Numerical solution of Darcy eq (verify) to determine a hydraulic diameter without a known friction factor. | |
:param volumetric_flow_rate: [m^3/s] | |
:param head_loss: [Pa/m] | |
:param fluid_density: [kg/m^3] | |
:param roughness_factor: [m] | |
:param fluid_kinematic_viscosity: [m^2/s] | |
:param min_bound: [m] | |
:param max_bound: [m] | |
:return: Hydraulic diameter [m] | |
""" | |
if math.isclose(volumetric_flow_rate.m, 0, abs_tol=1e-9): | |
return 0 | |
Q_dot = volumetric_flow_rate | |
H_L = head_loss | |
ρ = fluid_density | |
ε = roughness_factor | |
ν = fluid_kinematic_viscosity | |
min_bound = min_bound.to("m") | |
max_bound = max_bound.to("m") | |
@ureg.wraps(None, ("m^3/s", "kg/m^3", "Pa / m")) | |
def rhs_eq(volumetric_flow_rate, density, head_loss): | |
return 8 * pow(volumetric_flow_rate, 2) * density / pow(np.pi, 2) / head_loss | |
@ureg.wraps(None, ("m", "")) | |
def lhs_eq(hydraulic_diameter, friction_factor): | |
return pow(hydraulic_diameter, 5) / friction_factor | |
rhs = rhs_eq(Q_dot, ρ, H_L) | |
def f(x): | |
"""Make sure `x` is in meters""" | |
x = Q(x, "m") | |
A = pipe_area(x) | |
V = fluid_velocity(Q_dot, A) | |
Re = Reynolds_number(x, V, ν) | |
N_f = Niazkar_friction_factor(x, Re, ε) | |
lhs = lhs_eq(x, N_f) | |
return abs(rhs - lhs) | |
return Q(minimize_scalar(f, method="bounded", bounds=(min_bound.m, max_bound.m)).x, "m") | |
@accept_strings | |
def find_second_dimension( | |
volumetric_flow_rate: Q, | |
first_dimension: Q, | |
max_vel: Q = max_vel, | |
head_loss: Q = max_head_loss, | |
fluid_density: Q = air_density, | |
roughness_factor: Q = absolute_roughness, | |
fluid_kinematic_viscosity: Q = air_kin_viscosity, | |
min_bound: Q = Q(1, "inch"), | |
max_bound: Q = Q(200, "inch"), | |
) -> Q: | |
""" | |
Find second rectangular dimension of a duct based on first and flowrate/headloss. | |
:param volumetric_flow_rate: [m^3/s] | |
:param first_dimension: [inch] | |
:param max_vel: [m/s] | |
:param head_loss: [Pa/m] | |
:param fluid_density: [kg/m^3] | |
:param roughness_factor: [m] | |
:param fluid_kinematic_viscosity: [m^2/s] | |
:param: min_bound: [inch] | |
:param: max_bound: [inch] | |
:return: Other dimension in a rectangular duct [inch] | |
""" | |
if math.isclose(volumetric_flow_rate.m, 0, abs_tol=1e-9): | |
return 0 | |
if math.isclose(first_dimension.m, 0, abs_tol=1e-9): | |
return 0 | |
Q_dot = volumetric_flow_rate | |
a = first_dimension | |
V_max = max_vel | |
H_L = head_loss | |
ρ = fluid_density | |
ε = roughness_factor | |
ν = fluid_kinematic_viscosity | |
min_bound = min_bound.to("inch") | |
max_bound = max_bound.to("inch") | |
D_h = find_hydraulic_diameter(Q_dot, H_L, ρ, ε, ν).to("inch") | |
def f1(x): # minimizes for hydraulic dia | |
"""Make sure `x` is in inches""" | |
x = Q(x, "inch") | |
return abs(D_h.m - equiv_diameter_rect_duct(a, x).m) | |
def f2(x): # minimizes for velocity | |
"""Make sure `x` is inches""" | |
x = Q(x, "inch") | |
V = fluid_velocity_thru_rect_duct_dims(Q_dot, a, x) | |
return abs(V.m - max_vel.to("m/s").m) | |
second_dimension = Q(minimize_scalar(f1, method="bounded", bounds=(min_bound.m, max_bound.m)).x, "inch") | |
f1_air_vel = fluid_velocity_thru_rect_duct_dims(Q_dot, a, second_dimension) | |
if f1_air_vel <= V_max: | |
return second_dimension | |
else: | |
return Q(minimize_scalar(f2, method="bounded", bounds=(min_bound.m, max_bound.m)).x, "inch") | |
# endregion | |
# region --- User Functions -------------------------------------------------------------------------------------------- | |
@accept_strings | |
@ureg.wraps(None, ("cfm", "fpm", "in_WC_per_100_ft", "inch", ""), strict=False) | |
def get_all_possible_dims( | |
volumetric_flow_rate: QUANTITYLIKE, | |
max_vel: QUANTITYLIKE = max_vel, | |
head_loss: QUANTITYLIKE = max_head_loss, | |
max_dim: QUANTITYLIKE = Q("48 inch"), | |
max_aspect_ratio: Real = max_AR, | |
): | |
if math.isclose(volumetric_flow_rate, 0, abs_tol=1e-9): | |
print("0 x 0") | |
return | |
Q_dot = Q(volumetric_flow_rate, "cfm") | |
V_max = Q(max_vel, "fpm") | |
H_L = Q(head_loss, "in_WC_per_100_ft") | |
used = [] | |
for i in ([5] + list(range(6, max_dim + 1))[::2]) * ureg.inch: | |
second_dimension = find_second_dimension(Q_dot, i, V_max, H_L) | |
second_dimension = _round_up_to_nearest_int(second_dimension, 2) | |
if second_dimension >= i: | |
AR = (second_dimension / i).to("") | |
else: | |
AR = (i / second_dimension).to("") | |
if AR <= max_aspect_ratio: | |
if (int(second_dimension.m), int(i.m)) in used: | |
pass | |
elif int(second_dimension.m) in [u[0] for u in used]: | |
pass | |
else: | |
used.append((int(i.m), int(second_dimension.m))) | |
if AR == 1: | |
print(int(i.m), "x", int(second_dimension.m), " *") | |
else: | |
print(int(i.m), "x", int(second_dimension.m)) | |
@accept_strings | |
@ureg.wraps(None, ("cfm", "in_WC_per_100_ft", None), strict=False) | |
def get_round_duct_size( | |
volumetric_flow_rate: QUANTITYLIKE, | |
head_loss: QUANTITYLIKE = max_head_loss, | |
print_: bool = True, | |
): | |
Q_dot = Q(volumetric_flow_rate, "cfm") | |
H_L = Q(head_loss, "in_WC_per_100_ft") | |
D_h = find_hydraulic_diameter(Q_dot, H_L).to("inch") | |
D_h = _round_up_to_nearest_int(D_h, 1) | |
if print_: | |
print(D_h) | |
else: | |
return D_h | |
@accept_strings | |
@ureg.wraps(None, ("cfm", "inch", "fpm", "in_WC_per_100_ft", None), strict=False) | |
def get_second_dimension( | |
volumetric_flow_rate: QUANTITYLIKE, | |
first_dimension: QUANTITYLIKE, | |
max_vel: QUANTITYLIKE = max_vel, | |
head_loss: QUANTITYLIKE = max_head_loss, | |
print_: bool = True, | |
): | |
Q_dot = Q(volumetric_flow_rate, "cfm") | |
a = Q(first_dimension, "inch") | |
V_max = Q(max_vel, "fpm") | |
H_L = Q(head_loss, "in_WC_per_100_ft") | |
b = find_second_dimension(Q_dot, a, V_max, H_L) | |
b = _round_up_to_nearest_int(b, 2) | |
if print_: | |
print(b) | |
else: | |
return b | |
@ureg.wraps( | |
None, | |
( | |
"sqft", | |
None, | |
"person", | |
"sqft / person", | |
None, | |
None, | |
None, | |
None, | |
"cfm" | |
), | |
strict=False, | |
) | |
def minimum_ventilation_rates( | |
zone_floor_area: Q, | |
occupancy_classification: str = 'Multipurpose assembly', | |
exact_occupancy: Q = Q("0 person"), | |
exact_area_per_person: Q = Q("0 sqft / person"), | |
zone_air_distribution_effectiveness: float = 0.8, | |
bedrooms: int = 0, | |
use_max_of_default: bool = False, | |
code_year: int = 2014, | |
minimum_flow_per_space: Q = Q("20 cfm") | |
): | |
# re-assign units | |
A_z = Q(zone_floor_area, "sqft") | |
P_z = Q(exact_occupancy, "person") | |
exact_density = Q(exact_area_per_person, "sqft / person") | |
min_flow = Q(minimum_flow_per_space, "cfm") | |
# import the 2014 data | |
table_403 = pd.read_csv(r'C:\Users\Dave\PycharmProjects\Ventrop\table_403_3_minimum_ventilation_rates.csv') | |
table_403 = table_403.set_index('Occupancy Classification') | |
# get the row based on room classifications | |
classification_row = table_403.loc[[occupancy_classification]] | |
R_p = Q(float(classification_row['R_p [cfm/person]']), "cfm/person") | |
R_a = Q(float(classification_row['R_a [cfm/sqft]']), "cfm/sqft") | |
default_occupant_density = Q(float(classification_row['Occupant Density [ppl_per_1000_sqft]']), "ppl_per_1000_sqft") | |
exhaust_airflow_rate = Q(float(classification_row['Exhaust Airflow [cfm/sqft]']), "cfm/sqft") | |
# occupancy calcs | |
if exact_density > Q(0, "sqft / person"): | |
P_z = np.ceil((A_z / exact_density).to("person")) | |
if use_max_of_default: | |
if occupancy_classification == 'Living areas': | |
P_z = max(P_z, Q(bedrooms + 1, "person")) | |
else: | |
P_z = max(P_z, np.ceil((default_occupant_density * A_z).to("person"))) | |
V_bz = _round_up_to_nearest_int((R_p * P_z + R_a * A_z).to("cfm") / zone_air_distribution_effectiveness, 5) | |
V_bz = max(min_flow, V_bz) | |
exhaust_airflow = (exhaust_airflow_rate * A_z).to("cfm") | |
if exhaust_airflow > Q(0, "cfm"): | |
exhaust_airflow = max(min_flow, exhaust_airflow) | |
print(A_z, occupancy_classification) | |
if exhaust_airflow == Q(0, "cfm"): | |
print(f"{V_bz} OA/RA, {P_z}") | |
else: | |
exhaust_airflow = _round_up_to_nearest_int(exhaust_airflow, 5) | |
print(f"{V_bz} OA, {exhaust_airflow} EX, {P_z}") | |
# endregion | |
def run(cfms: Optional[list[int]] = None, first_dims: Optional[list[int]] = None): | |
if cfms is None: | |
cfms = [int(x) for x in input("cfms: ").split()] | |
df = pd.DataFrame({"Flow [cfm]": cfms}) | |
df["D_h [inch]"] = df["Flow [cfm]"].apply( | |
lambda x: find_hydraulic_diameter(Q(x, "cfm")).to("inch").m | |
) | |
if first_dims is None: | |
print(df) | |
first_dims = [int(x) for x in input("first dims: ").split()] | |
def calc(df, first_dims): | |
df["Width [inch]"] = first_dims | |
df["Height [inch]"] = df.apply( | |
lambda x: find_second_dimension( | |
Q(x["Flow [cfm]"], "cfm"), Q(x["Width [inch]"], "inch") | |
).m, | |
axis=1, | |
) | |
df["D_e [inch]"] = df.apply( | |
lambda x: equiv_diameter_rect_duct( | |
Q(x["Width [inch]"], "inch"), Q(x["Height [inch]"], "inch") | |
).m, | |
axis=1, | |
) | |
df["Flow area [ft^2]"] = df.apply( | |
lambda x: (Q(x["Width [inch]"], "inch") * Q(x["Height [inch]"], "inch")) | |
.to("ft^2") | |
.m, | |
axis=1, | |
) | |
df["Fluid velocity [fpm]"] = df.apply( | |
lambda x: fluid_velocity_thru_pipe( | |
Q(x["Flow [cfm]"], "cfm"), Q(x["D_e [inch]"], "inch") | |
) | |
.to("fpm") | |
.m, | |
axis=1, | |
) | |
df["Reynolds Number"] = df.apply( | |
lambda x: Reynolds_number( | |
Q(x["D_e [inch]"], "inch"), Q(x["Fluid velocity [fpm]"], "fpm") | |
), | |
axis=1, | |
) | |
df["Friction Factor"] = df.apply( | |
lambda x: Niazkar_friction_factor( | |
Q(x["D_e [inch]"], "inch"), x["Reynolds Number"] | |
), | |
axis=1, | |
) | |
df["Velocity pressure [in WC]"] = df.apply( | |
lambda x: (air_density * pow(Q(x["Fluid velocity [fpm]"], "fpm"), 2) / 2) | |
.to("in_WC") | |
.m, | |
axis=1, | |
) | |
df["Head loss [in WC / 100 ft]"] = df.apply( | |
lambda x: ( | |
pow(Q(x["Fluid velocity [fpm]"], "fpm"), 2) | |
* x["Friction Factor"] | |
* air_density | |
/ 2 | |
/ Q(x["D_e [inch]"], "inch") | |
) | |
.to("in_WC_per_100_ft") | |
.m, | |
axis=1, | |
) | |
df["D_e [inch]"] = round(df["D_e [inch]"], 3) | |
df["Flow area [ft^2]"] = round(df["Flow area [ft^2]"], 4) | |
df["Fluid velocity [fpm]"] = df["Fluid velocity [fpm]"].apply(math.ceil) | |
df["Reynolds Number"] = df["Reynolds Number"].apply(math.ceil) | |
df["Friction Factor"] = round(df["Friction Factor"], 5) | |
df["Velocity pressure [in WC]"] = round(df["Velocity pressure [in WC]"], 4) | |
df["Head loss [in WC / 100 ft]"] = round(df["Head loss [in WC / 100 ft]"], 4) | |
calc(df, first_dims) | |
return df | |
@accept_strings | |
@ureg.wraps(("btuh", "ton_of_refrigeration"), ("person", "sqft", None), strict=False) | |
def cooling_load_estimation(ppl: int, sqft: float, external: bool = False): | |
P_z = Q(ppl, "person") | |
A_z = Q(sqft, "sqft") | |
EL = Q("25 btuh / sqft") if external else Q("5 btuh / sqft") | |
load = Q("450 btuh / person") * P_z + Q("1 W / sqft") * A_z + EL * A_z | |
return load, load | |
@ureg.wraps("NC", ("feet", None), strict=False) | |
def _NC_correction( | |
plenum_box_length: QUANTITYLIKE, airflow_direction: AirFlowDirection = SUPPLY | |
) -> Q: | |
L = Q(plenum_box_length, "ft") | |
df_NC_correction = pd.DataFrame() | |
df_NC_correction["Length [ft]"] = [2, 4, 6, 8, 10] | |
df_NC_correction["Supply [NC]"] = [-2, 0, 2, 3, 5] | |
df_NC_correction["Return [NC]"] = [0, 3, 5, 6, 8] | |
match airflow_direction: | |
case AirFlowDirection.SUPPLY: | |
return np.interp( | |
L, | |
np.asarray(df_NC_correction["Length [ft]"]) * ureg.ft, | |
np.asarray(df_NC_correction["Supply [NC]"]) * ureg.NC, | |
) | |
case AirFlowDirection.RETURN: | |
return np.interp( | |
L, | |
np.asarray(df_NC_correction["Length [ft]"]) * ureg.ft, | |
np.asarray(df_NC_correction["Return [NC]"]) * ureg.NC, | |
) | |
@ureg.wraps(None, "feet", strict=False) | |
def _throw_correction_multiplier(plenum_box_length: QUANTITYLIKE) -> float: | |
L = Q(plenum_box_length, "ft") | |
df_throw_correction = pd.DataFrame() | |
df_throw_correction["Length [ft]"] = [2, 4, 8, 10, 12] | |
df_throw_correction["Throw Correction"] = [0.72, 1, 1.5, 1.7, 1.8] | |
return np.interp( | |
L, | |
np.asarray(df_throw_correction["Length [ft]"]) * ureg.ft, | |
df_throw_correction["Throw Correction"], | |
) | |
@accept_strings | |
@ureg.wraps(None, ("cfm", "inch", None, None, "NC", None), strict=False) | |
def linear_air_sizer( | |
airflow: QUANTITYLIKE, | |
incoming_duct_height: QUANTITYLIKE, | |
connection_type: PlenumBoxConnection, | |
mounting_type: LinearDiffuserMounting, | |
max_acceptable_noise: QUANTITYLIKE = Q(18, "NC"), | |
slots: int = 1, | |
): | |
Q_dot = Q(airflow, "cfm") | |
DH = Q(incoming_duct_height, "inch") | |
match mounting_type: | |
case LinearDiffuserMounting.SIDEWALL: | |
match slots: | |
case 1: | |
plenum_box_height = max(DH + Q("2 inch"), Q("5.75 inch")) | |
case 2: | |
plenum_box_height = max( | |
DH + Q("2 inch"), Q("11.25 inch") | |
) # see FBP submittal | |
match connection_type: | |
case PlenumBoxConnection.CENTER: | |
plenum_box_width = get_second_dimension( | |
Q_dot / 2, | |
plenum_box_height, | |
Q("1000 fpm"), | |
"0.1 in_WC_per_100_ft", | |
False, | |
) | |
case PlenumBoxConnection.END: | |
plenum_box_width = get_second_dimension( | |
Q_dot, | |
plenum_box_height, | |
Q("1000 fpm"), | |
"0.1 in_WC_per_100_ft", | |
False, | |
) | |
case LinearDiffuserMounting.CEILING: | |
plenum_box_height = DH + Q("2 inch") | |
match slots: | |
case 1: | |
min_width = Q("5.75 inch") | |
case 2: | |
min_width = Q("11.25 inch") | |
match connection_type: | |
case PlenumBoxConnection.CENTER: | |
plenum_box_width = max( | |
min_width, | |
get_second_dimension( | |
Q_dot / 2, | |
plenum_box_height, | |
Q("1000 fpm"), | |
"0.1 in_WC_per_100_ft", | |
False, | |
), | |
) | |
case PlenumBoxConnection.END: | |
plenum_box_width = max( | |
min_width, | |
get_second_dimension( | |
Q_dot, | |
plenum_box_height, | |
Q("1000 fpm"), | |
"0.1 in_WC_per_100_ft", | |
False, | |
), | |
) | |
max_NC = Q(max_acceptable_noise, "NC") | |
FL_25_specs = dict() | |
FL_25_specs[CEILING] = dict() | |
FL_25_specs[SIDEWALL] = dict() | |
FL_25_specs[CEILING][1] = dict() | |
FL_25_specs[CEILING][2] = dict() | |
FL_25_specs[SIDEWALL][1] = dict() | |
FL_25_specs[SIDEWALL][2] = dict() | |
FL_25_specs[CEILING][1]["Airflow [cfm/ft]"] = Q([30, 55, 80, 105, 130, 155, 180], "cfm/ft") | |
FL_25_specs[CEILING][1]["Noise Criteria [NC]"] = Q([10, 11, 23, 31, 38, 43, 48], "NC") | |
FL_25_specs[CEILING][1]["Throw (L) [ft]"] = Q([4, 10, 14, 17, 19, 21, 22], "ft") | |
FL_25_specs[CEILING][1]["Throw (M) [ft]"] = Q([8, 15, 18, 21, 23, 25, 27], "ft") | |
FL_25_specs[CEILING][1]["Throw (H) [ft]"] = Q([16, 21, 26, 29, 33, 36, 39], "ft") | |
FL_25_specs[CEILING][2]["Airflow [cfm/ft]"] = Q([60, 105, 150, 195, 240, 285, 330], "cfm/ft") | |
FL_25_specs[CEILING][2]["Noise Criteria [NC]"] = Q([10, 13, 24, 32, 38, 44, 48], "NC") | |
FL_25_specs[CEILING][2]["Throw (L) [ft]"] = Q([6, 13, 19, 23, 26, 28, 30], "ft") | |
FL_25_specs[CEILING][2]["Throw (M) [ft]"] = Q([11, 20, 25, 28, 32, 34, 37], "ft") | |
FL_25_specs[CEILING][2]["Throw (H) [ft]"] = Q([22, 29, 35, 40, 45, 49, 52], "ft") | |
FL_25_specs[SIDEWALL][1]["Airflow [cfm/ft]"] = Q([95, 150, 205, 260, 315, 370], "cfm/ft") | |
FL_25_specs[SIDEWALL][1]["Noise Criteria [NC]"] = Q([10, 18, 25, 30, 35, 39], "NC") | |
FL_25_specs[SIDEWALL][1]["Throw (L) [ft]"] = Q([6, 9, 12, 15, 18, 22], "ft") | |
FL_25_specs[SIDEWALL][1]["Throw (M) [ft]"] = Q([8, 13, 18, 23, 28, 33], "ft") | |
FL_25_specs[SIDEWALL][1]["Throw (H) [ft]"] = Q([17, 26, 35, 39, 43, 46], "ft") | |
FL_25_specs[SIDEWALL][2]["Airflow [cfm/ft]"] = Q([80, 190, 300, 410, 520, 630, 740], "cfm/ft") | |
FL_25_specs[SIDEWALL][2]["Noise Criteria [NC]"] = Q([10, 11, 22, 28, 34, 38, 42], "NC") | |
FL_25_specs[SIDEWALL][2]["Throw (L) [ft]"] = Q([2, 8, 12, 17, 22, 26, 31], "ft") | |
FL_25_specs[SIDEWALL][2]["Throw (M) [ft]"] = Q([4, 12, 19, 26, 32, 39, 46], "ft") | |
FL_25_specs[SIDEWALL][2]["Throw (H) [ft]"] = Q([10, 24, 37, 49, 55, 60, 66], "ft") | |
flow_per_ft_adj = lambda x: np.interp(max_NC - x, | |
FL_25_specs[mounting_type][slots]["Noise Criteria [NC]"], | |
FL_25_specs[mounting_type][slots]["Airflow [cfm/ft]"], | |
) | |
max_airflow_per_length = flow_per_ft_adj(Q("0 NC")) | |
prev_length = Q_dot / max_airflow_per_length | |
NC_adjustment = _NC_correction(prev_length) | |
new_max_airflow_per_length = flow_per_ft_adj(NC_adjustment) | |
new_length = Q_dot / new_max_airflow_per_length | |
while abs(new_length - prev_length) > Q("1/2 inch"): | |
prev_length = new_length | |
NC_adjustment = _NC_correction(prev_length) | |
new_max_airflow_per_length = flow_per_ft_adj(NC_adjustment) | |
new_length = Q_dot / new_max_airflow_per_length | |
plenum_box_length = Q(_round_up_to_nearest_int(new_length.to("inch").m, 4), "inch") | |
print( | |
f"Plenum box dimensions (LxWxH): {plenum_box_length} x {plenum_box_width} x {plenum_box_height}" | |
) | |
LD_supply_length = Q( | |
_round_up_to_nearest_int(plenum_box_length.m + 2, 6), "inch" | |
).to("ft") | |
max_airflow_per_length = flow_per_ft_adj(Q("0 NC")) | |
prev_length = Q_dot / max_airflow_per_length | |
NC_adjustment = _NC_correction(prev_length, airflow_direction=RETURN) | |
new_max_airflow_per_length = flow_per_ft_adj(NC_adjustment) | |
new_length = Q_dot / new_max_airflow_per_length | |
while abs(new_length - prev_length) > Q("1/2 inch"): | |
prev_length = new_length | |
NC_adjustment = _NC_correction(prev_length, airflow_direction=RETURN) | |
new_max_airflow_per_length = flow_per_ft_adj(NC_adjustment) | |
new_length = Q_dot / new_max_airflow_per_length | |
LD_return_length = Q( | |
_round_up_to_nearest_int(new_length.to("inch").m, 6), "inch" | |
).to("ft") | |
total_LD_length = LD_supply_length + LD_return_length | |
print( | |
f"Linear diffuser length: ({LD_supply_length} SUPPLY + {LD_return_length} RETURN) = {total_LD_length}" | |
) | |
print(f"If combined LD: LD-{slots} // {ArchitectualLength(total_LD_length)}") | |
print( | |
f"If separate LD: Supply = LD-{slots} // {ArchitectualLength(LD_supply_length)}\tReturn = LD-{slots} // {ArchitectualLength(LD_return_length)}" | |
) | |
throw_dist = dict() | |
for k in "LMH": | |
throw_dist[k] = np.interp( | |
Q_dot / plenum_box_length.to("ft"), | |
FL_25_specs[mounting_type][slots]["Airflow [cfm/ft]"], | |
FL_25_specs[mounting_type][slots][f"Throw ({k}) [ft]"] * _throw_correction_multiplier(plenum_box_length=plenum_box_length), | |
) | |
print( | |
f"Throw distance (L, M, H): {ArchitectualLength(throw_dist['L'])}, {ArchitectualLength(throw_dist['M'])}, {ArchitectualLength(throw_dist['H'])}" | |
) | |
@accept_strings | |
@ureg.wraps(None, ("cfm", "inch", "", "fpm"), strict=False) | |
def louver_sizing(airflow: Q, first_dimension: Q, free_area: Q = Q("50 percent"), airspeed: Q = Q("600 fpm")): | |
Q_dot = Q(airflow, "cfm") | |
a = Q(first_dimension, "inch") | |
v = Q(airspeed, "fpm") | |
f = free_area | |
b = (Q_dot / f / a / v).to("inch") | |
return np.ceil(b), np.round((a*b).to("sqft"), 1) | |
@accept_strings | |
@ureg.wraps(None, ("sqft", "ft", "lbs"), strict=False) | |
def refrigerant_leak_check_R410A(floor_area: Q, floor_to_ceiling: Q, total_charge: Q): | |
""" | |
>>> refrigerant_leak_check_R410A("315 sqft", ArchitectualLength("10'-2\\"").Q(), "76 lbs") | |
# SAFE | |
""" | |
max_conc = Q("26 lb / mcf") | |
room_vol = (floor_area * floor_to_ceiling).to("mcf") | |
charge_per_vol = (total_charge / room_vol).to("lb / mcf") | |
if charge_per_vol <= max_conc: | |
print("SAFE") | |
else: | |
print("NOT SAFE") | |
@accept_strings | |
@ureg.wraps(None, ("inch", "inch", "cfm"), strict=False) | |
def get_airflow_stats(a: Q, b: Q, airflow: Q): | |
a = Q(a, "inch") | |
b = Q(b, "inch") | |
airflow = Q(airflow, "cfm") | |
D_e = equiv_diameter_rect_duct(a, b).to("inch") | |
flow_area = (a * b).to("ft^2") | |
fluid_vel = fluid_velocity_thru_rect_duct_dims(airflow, a, b).to("fpm") | |
Re = Reynolds_number(D_e, fluid_vel) | |
f = Niazkar_friction_factor(D_e, Re) | |
vel_pressure = (air_density * pow(fluid_vel, 2) / 2).to("in_WC") | |
head_loss = (pow(fluid_vel, 2) * f * air_density / 2 / D_e).to("in_WC_per_100_ft") | |
print(f"For {airflow:.0f~P} thru a {a} x {b} duct:" | |
f"\n\tEquivalent diameter = {D_e:.3f~P}" | |
f"\n\tFlow area = {flow_area:.4f~P}" | |
f"\n\tFluid velocity = {fluid_vel:,.0f~P}" | |
f"\n\tReynold's number = {Re:,.0f~P}" | |
f"\n\tFriction factor = {f:.5f~P}" | |
f"\n\tVelocity pressure = {vel_pressure:.4f~P}" | |
f"\n\tHead loss = {head_loss:.4f~P}") | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi Dave, I'm wondering what is the equation you use for calculating the pipe diameter in line 450-453. I cannot find a formula that is similar to this one. Thanks!