feat: Adds duct sizing calculations.

This commit is contained in:
2025-12-21 14:40:36 -05:00
parent b3502fc79b
commit 861ff3bfd6
4 changed files with 120 additions and 0 deletions

View File

@@ -10,3 +10,60 @@ extension Array where Element == EffectiveLengthGroup {
reduce(0) { $0 + $1.effectiveLength }
}
}
func roundSize(_ size: Double) throws -> Int {
guard size > 0 else {
throw ManualDError(message: "Size should be greater than 0.")
}
guard size <= 24 else {
throw ManualDError(message: "Size should be less than 24.")
}
switch size {
case 0..<4:
return 4
case 4..<5:
return 5
case 5..<6:
return 6
case 6..<7:
return 7
case 7..<8:
return 8
case 8..<9:
return 9
case 9..<10:
return 10
case 10..<12:
return 12
case 12..<14:
return 14
case 14..<16:
return 16
case 16..<18:
return 18
case 18..<20:
return 20
case 20..<22:
return 2
case 22..<24:
return 24
default:
throw ManualDError(message: "Size '\(size)' not in range.")
}
}
func velocity(cfm: Int, roundSize: Int) -> Int {
let cfm = Double(cfm)
let roundSize = Double(roundSize)
let velocity = cfm / (pow(roundSize / 24, 2) * 3.14)
return Int(round(velocity))
}
func flexSize(_ request: ManualDClient.DuctSizeRequest) throws -> Int {
let cfm = pow(Double(request.designCFM), 0.4)
let fr = pow(request.frictionRate / 1.76, 0.2)
let size = 0.55 * (cfm / fr)
return try roundSize(size)
}

View File

@@ -4,6 +4,21 @@ import ManualDCore
extension ManualDClient: DependencyKey {
public static let liveValue: Self = .init(
ductSize: { request in
guard request.designCFM > 0 else {
throw ManualDError(message: "Design CFM should be greater than 0.")
}
let fr = pow(request.frictionRate, 0.5)
let ductulatorSize = pow(Double(request.designCFM) / (3.12 * fr), 0.38)
let finalSize = try roundSize(ductulatorSize)
let flexSize = try flexSize(request)
return .init(
ductulatorSize: ductulatorSize,
finalSize: finalSize,
flexSize: flexSize,
velocity: velocity(cfm: request.designCFM, roundSize: finalSize)
)
},
frictionRate: { request in
// Ensure the total effective length is greater than 0.
guard request.totalEffectiveLength > 0 else {

View File

@@ -4,6 +4,7 @@ import ManualDCore
@DependencyClient
public struct ManualDClient: Sendable {
public var ductSize: @Sendable (DuctSizeRequest) async throws -> DuctSizeResponse
public var frictionRate: @Sendable (FrictionRateRequest) async throws -> FrictionRateResponse
public var totalEffectiveLength: @Sendable (TotalEffectiveLengthRequest) async throws -> Int
public var equivalentRectangularDuct:
@@ -21,6 +22,42 @@ extension DependencyValues {
}
}
// MARK: Duct Size
extension ManualDClient {
public struct DuctSizeRequest: Codable, Equatable, Sendable {
public let designCFM: Int
public let frictionRate: Double
public init(
designCFM: Int,
frictionRate: Double
) {
self.designCFM = designCFM
self.frictionRate = frictionRate
}
}
public struct DuctSizeResponse: Codable, Equatable, Sendable {
public let ductulatorSize: Double
public let finalSize: Int
public let flexSize: Int
public let velocity: Int
public init(
ductulatorSize: Double,
finalSize: Int,
flexSize: Int,
velocity: Int
) {
self.ductulatorSize = ductulatorSize
self.finalSize = finalSize
self.flexSize = flexSize
self.velocity = velocity
}
}
}
// MARK: - Friction Rate
extension ManualDClient {
public struct FrictionRateRequest: Codable, Equatable, Sendable {

View File

@@ -22,6 +22,17 @@ struct ManualDClientTests {
return formatter
}
@Test
func ductSize() async throws {
let response = try await manualD.ductSize(
.init(designCFM: 88, frictionRate: 0.06)
)
#expect(numberFormatter.string(for: response.ductulatorSize) == "6.07")
#expect(response.finalSize == 7)
#expect(response.flexSize == 7)
#expect(response.velocity == 329)
}
@Test
func frictionRate() async throws {
let response = try await manualD.frictionRate(