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]) ] 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( mode: .knownAirflow, 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( mode: .measuredPressure, 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.") } } }