223 lines
7.4 KiB
Swift
223 lines
7.4 KiB
Swift
import Foundation
|
|
import Logging
|
|
import OrderedCollections
|
|
import Routes
|
|
|
|
public extension RoomPressure.Request {
|
|
|
|
static let pascalToIWCMultiplier = 0.004014
|
|
static let targetVelocity = 400.0
|
|
static let maxVelocity = 600.0
|
|
static let minGrilleFreeArea = 0.7
|
|
static let standardDuctSizes = OrderedSet([4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20])
|
|
static let standardGrilleHeights = OrderedSet([4, 6, 8, 10, 12, 14, 20])
|
|
static let standardGrilleSizes = [
|
|
4: OrderedSet([8, 10, 12, 14]),
|
|
6: OrderedSet([8, 10, 12, 14]),
|
|
8: OrderedSet([10, 12, 14]),
|
|
10: OrderedSet([10]),
|
|
12: OrderedSet([12]),
|
|
14: OrderedSet([14])
|
|
]
|
|
|
|
// OrderedSet([
|
|
// (width: 8, height: 4),
|
|
// (width: 10, height: 4),
|
|
// (width: 12, height: 4),
|
|
// (width: 14, height: 4),
|
|
// (width: 8, height: 6),
|
|
// (width: 10, height: 6),
|
|
// (width: 12, height: 6),
|
|
// (width: 14, height: 6),
|
|
// (width: 10, height: 8),
|
|
// (width: 12, height: 8),
|
|
// (width: 14, height: 8),
|
|
// (width: 10, height: 10),
|
|
// (width: 12, height: 12),
|
|
// (width: 14, height: 14)
|
|
// ])
|
|
|
|
func respond(logger: Logger) async throws -> RoomPressure.Response {
|
|
switch self {
|
|
case let .knownAirflow(request):
|
|
return try await calculateKnownAirflow(request, logger)
|
|
case let .measuredPressure(request):
|
|
return try await calculateMeasuredPressure(request, logger)
|
|
}
|
|
}
|
|
|
|
private func calculateKnownAirflow(
|
|
_ request: RoomPressure.Request.KnownAirflow,
|
|
_ logger: Logger
|
|
) async throws -> RoomPressure.Response {
|
|
try request.validate()
|
|
|
|
let totalLeakageArea = calculateDoorLeakageArea(
|
|
doorWidth: request.doorWidth,
|
|
doorHeight: request.doorHeight,
|
|
doorUndercut: request.doorUndercut
|
|
)
|
|
|
|
// Net free area (in^2)
|
|
let netFreeArea = (request.supplyAirflow / Self.targetVelocity) * 144.0
|
|
let grilleDimensions = try getStandardGrilleSize(
|
|
requiredArea: netFreeArea,
|
|
selectedHeight: request.preferredGrilleHeight.rawValue
|
|
)
|
|
let (standardDuctSize, actualVelocity) = calculateDuctMetrics(for: request.supplyAirflow)
|
|
|
|
return .init(
|
|
grilleSize: .init(width: grilleDimensions.width, height: grilleDimensions.height, area: netFreeArea / 144),
|
|
ductSize: .init(diameter: standardDuctSize, velocity: actualVelocity),
|
|
warnings: generateWarnings(
|
|
roomPressure: request.targetRoomPressure,
|
|
actualVelocity: actualVelocity,
|
|
totalLeakageArea: totalLeakageArea,
|
|
netFreeArea: netFreeArea
|
|
)
|
|
)
|
|
}
|
|
|
|
private func calculateMeasuredPressure(
|
|
_ request: RoomPressure.Request.MeasuredPressure,
|
|
_ logger: Logger
|
|
) async throws -> RoomPressure.Response {
|
|
try request.validate()
|
|
|
|
let totalLeakageArea = calculateDoorLeakageArea(
|
|
doorWidth: request.doorWidth,
|
|
doorHeight: request.doorHeight,
|
|
doorUndercut: request.doorUndercut
|
|
)
|
|
|
|
let pressureInchesWaterColumn = request.measuredRoomPressure * Self.pascalToIWCMultiplier
|
|
let calculatedAirflow = totalLeakageArea * 4005 * sqrt(pressureInchesWaterColumn)
|
|
// in^2
|
|
let netFreeArea = (calculatedAirflow / Self.targetVelocity) * 144
|
|
let grilleDimensions = try getStandardGrilleSize(
|
|
requiredArea: netFreeArea,
|
|
selectedHeight: request.preferredGrilleHeight.rawValue
|
|
)
|
|
let (standardDuctSize, actualVelocity) = calculateDuctMetrics(for: calculatedAirflow)
|
|
|
|
return .init(
|
|
grilleSize: .init(width: grilleDimensions.width, height: grilleDimensions.height, area: netFreeArea / 144),
|
|
ductSize: .init(diameter: standardDuctSize, velocity: actualVelocity),
|
|
calculatedAirflow: calculatedAirflow,
|
|
warnings: generateWarnings(
|
|
roomPressure: request.measuredRoomPressure,
|
|
actualVelocity: actualVelocity,
|
|
totalLeakageArea: totalLeakageArea,
|
|
netFreeArea: netFreeArea
|
|
)
|
|
)
|
|
}
|
|
|
|
private func calculateDuctMetrics(for airflow: Double) -> (diameter: Int, velocity: Double) {
|
|
let ductArea = airflow / Self.targetVelocity
|
|
let ductDiameter = sqrt((4 * ductArea) / Double.pi) * 12
|
|
let standardDuctSize = getStandardDuctSize(for: Int(ductDiameter))
|
|
let actualVelocity = airflow / (Double.pi * pow(Double(standardDuctSize) / 24, 2))
|
|
|
|
return (standardDuctSize, actualVelocity)
|
|
}
|
|
|
|
private func generateWarnings(
|
|
roomPressure: Double,
|
|
actualVelocity: Double,
|
|
totalLeakageArea: Double,
|
|
netFreeArea: Double
|
|
) -> [String] {
|
|
var warnings = [String]()
|
|
|
|
if roomPressure > 3 {
|
|
warnings.append(
|
|
"Room pressure exceeds 3 Pascals - consider reducing pressure differntial."
|
|
)
|
|
}
|
|
|
|
if totalLeakageArea > netFreeArea / 144 * 0.5 {
|
|
warnings.append(
|
|
"Door leakage area is significant - consider reducing gaps or increasing grille size."
|
|
)
|
|
}
|
|
|
|
if actualVelocity > Self.maxVelocity {
|
|
warnings.append(
|
|
"Return air velocity exceeds maximum recommended velocity - consider next size up duct."
|
|
)
|
|
}
|
|
return []
|
|
}
|
|
|
|
private func getStandardDuctSize(for calculatedSize: Int) -> Int {
|
|
Self.standardDuctSizes.sorted()
|
|
.first { $0 >= calculatedSize }
|
|
?? 20
|
|
}
|
|
|
|
private func getStandardGrilleSize(requiredArea: Double, selectedHeight: Int) throws -> (width: Int, height: Int) {
|
|
guard let availableSizes = Self.standardGrilleSizes[selectedHeight] else {
|
|
throw RoomPressureError.invalidPreferredHeight
|
|
}
|
|
|
|
if let width = availableSizes.first(where: {
|
|
Double($0) * Double(selectedHeight) * Self.minGrilleFreeArea >= (requiredArea / Self.minGrilleFreeArea)
|
|
}) {
|
|
return (width, selectedHeight)
|
|
}
|
|
|
|
// If no width matches return largest for the selectedHeight.
|
|
if let largestSize = availableSizes.last { return (largestSize, selectedHeight) }
|
|
return (14, selectedHeight)
|
|
}
|
|
|
|
// Calculate door leakage area in sq. ft.
|
|
private func calculateDoorLeakageArea(doorWidth: Double, doorHeight: Double, doorUndercut: Double) -> Double {
|
|
let doorLeakageArea = (doorWidth * doorUndercut) / 144.0
|
|
let doorPerimiterLeakage = ((2 * doorHeight + doorWidth) * 0.125) / 144.0
|
|
return doorLeakageArea + doorPerimiterLeakage
|
|
}
|
|
|
|
enum RoomPressureError: Error {
|
|
case invalidPreferredHeight
|
|
}
|
|
}
|
|
|
|
extension RoomPressure.Request.KnownAirflow {
|
|
func validate() throws {
|
|
guard targetRoomPressure > 0 else {
|
|
throw ValidationError(message: "Target room pressure should be greater than 0.")
|
|
}
|
|
guard doorWidth > 0 else {
|
|
throw ValidationError(message: "Door width should be greater than 0.")
|
|
}
|
|
guard doorHeight > 0 else {
|
|
throw ValidationError(message: "Door height should be greater than 0.")
|
|
}
|
|
guard doorUndercut > 0 else {
|
|
throw ValidationError(message: "Door undercut should be greater than 0.")
|
|
}
|
|
guard supplyAirflow > 0 else {
|
|
throw ValidationError(message: "Supply airflow should be greater than 0.")
|
|
}
|
|
}
|
|
}
|
|
|
|
extension RoomPressure.Request.MeasuredPressure {
|
|
func validate() throws {
|
|
guard measuredRoomPressure > 0 else {
|
|
throw ValidationError(message: "MeasuredPressure room pressure should be greater than 0.")
|
|
}
|
|
guard doorWidth > 0 else {
|
|
throw ValidationError(message: "Door width should be greater than 0.")
|
|
}
|
|
guard doorHeight > 0 else {
|
|
throw ValidationError(message: "Door height should be greater than 0.")
|
|
}
|
|
guard doorUndercut > 0 else {
|
|
throw ValidationError(message: "Door undercut should be greater than 0.")
|
|
}
|
|
}
|
|
}
|