diff --git a/Sources/ManualS/Extensions/Capacity+validator.swift b/Sources/ManualS/Extensions/Capacity+validator.swift index 84945cb..d7b1258 100644 --- a/Sources/ManualS/Extensions/Capacity+validator.swift +++ b/Sources/ManualS/Extensions/Capacity+validator.swift @@ -1,6 +1,16 @@ import Models import Validations +extension Capacity.Cooling: AsyncValidatable { + public var body: some AsyncValidation { + AsyncValidator.accumulating { + AsyncValidator.greaterThan(\.total, 0) + AsyncValidator.greaterThan(\.sensible, 0) + AsyncValidator.greaterThanOrEquals(\.total, \.sensible) + } + } +} + extension Capacity.ManufacturersCooling: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { diff --git a/Sources/ManualS/Internal/Interpolate.swift b/Sources/ManualS/Internal/CoolingInterpolation.swift similarity index 68% rename from Sources/ManualS/Internal/Interpolate.swift rename to Sources/ManualS/Internal/CoolingInterpolation.swift index 1be4fff..78116de 100644 --- a/Sources/ManualS/Internal/Interpolate.swift +++ b/Sources/ManualS/Internal/CoolingInterpolation.swift @@ -1,32 +1,26 @@ import Models import Validations -extension Interpolate.Request { - func respond() async throws -> Interpolate.Response { +extension CoolingInterpolation.Request { + + func respond() async throws -> CoolingInterpolation.Response { try await validate() - let interpolatedCapacity = await interpolation.interpolatedCapacity( - outdoorDesignTemperature: designInfo.summer.outdoorTemperature + let interpolatedCapacity = await interpolatedCapacity() + let excessLatent = excessLatent(interpolatedCapacity.latent) + let elevationDeratings = try await elevationDeratings() + let sizingLimits = try await sizingLimits() + + let finalCapacity = calculateFinalCapacity( + interpolated: interpolatedCapacity, + excessLatent: excessLatent, + adjustmentMultipliers: interpolation.adjustmentMultipliers, + elevationDeratings: elevationDeratings ) - let excessLatent = self.excessLatent(interpolatedLatent: interpolatedCapacity.latent) - let elevationDeratings = try await Derating.Request( - elevation: designInfo.elevation, - systemType: systemType - ).respond() - - let sizingLimits = try await SizingLimits.Request( - systemType: systemType, - houseLoad: houseLoad - ).respond() - - let finalCapacity = interpolatedCapacity - .applying(excessLatent: excessLatent) - .applying(adjustmentMultipliers: interpolation.adjustmentMultipliers) - .applying(adjustmentMultipliers: elevationDeratings) let capacityAsPercentOfLoad = Capacity.Cooling( - total: normalizePercentage(finalCapacity.total, houseLoad.coolingTotal), - sensible: normalizePercentage(finalCapacity.sensible, houseLoad.coolingSensible), - latent: normalizePercentage(finalCapacity.latent, houseLoad.coolingLatent) + total: normalizePercentage(finalCapacity.total, coolingLoad.total), + sensible: normalizePercentage(finalCapacity.sensible, coolingLoad.sensible), + latent: normalizePercentage(finalCapacity.latent, coolingLoad.latent) ) return .init( @@ -39,6 +33,57 @@ extension Interpolate.Request { sizingLimits: sizingLimits ) } + + private func interpolatedCapacity() async -> Capacity.Cooling { + await interpolation.interpolatedCapacity( + outdoorDesignTemperature: summerDesignInfo.outdoorTemperature + ) + } + + private func elevationDeratings() async throws -> AdjustmentMultiplier? { + guard let elevation else { return nil } + + return try await Derating.Request(elevation: elevation, systemType: systemType) + .respond() + } + + private func sizingLimits() async throws -> SizingLimits.Response { + try await SizingLimits.Request(systemType: systemType, coolingLoad: coolingLoad) + .respond() + } + + private func excessLatent(_ interpolatedLatent: Int) -> Int { + (interpolatedLatent - coolingLoad.latent) / 2 + } + +} + +private func calculateFinalCapacity( + interpolated: Capacity.Cooling, + excessLatent: Int, + adjustmentMultipliers: AdjustmentMultiplier?, + elevationDeratings: AdjustmentMultiplier? +) -> Capacity.Cooling { + var total = interpolated.total + var sensible = interpolated.sensible + excessLatent + if let adjustmentMultipliers { + adjustmentMultipliers.apply(total: &total, sensible: &sensible) + } + if let elevationDeratings { + elevationDeratings.apply(total: &total, sensible: &sensible) + } + return .init(total: total, sensible: sensible) +} + +private extension AdjustmentMultiplier { + func apply(total: inout Int, sensible: inout Int) { + if let coolingTotal { + total = Int(Double(total) * coolingTotal) + } + if let coolingSensible { + sensible = Int(Double(sensible) * coolingSensible) + } + } } private extension SizingLimits.Response { @@ -92,29 +137,7 @@ private func normalizePercentage( return Int((value * 1000).rounded() / 10.0) } -private extension Capacity.Cooling { - func applying(excessLatent: Int) -> Self { - .init(total: total, sensible: sensible + excessLatent) - } - - func applying(adjustmentMultipliers: AdjustmentMultiplier?) -> Self { - guard let adjustmentMultipliers else { return self } - - return .init( - total: Int(Double(total) * (adjustmentMultipliers.coolingTotal ?? 1)), - sensible: Int(Double(sensible) * (adjustmentMultipliers.coolingSensible ?? 1)) - ) - } -} - -private extension Interpolate.Request { - - func excessLatent(interpolatedLatent: Int) -> Int { - (interpolatedLatent - houseLoad.coolingLatent) / 2 - } -} - -private extension Interpolate.InterpolationRequest { +private extension CoolingInterpolation.InterpolationRequest { func interpolatedCapacity(outdoorDesignTemperature: Int) async -> Capacity.Cooling { switch self { @@ -147,20 +170,16 @@ private func interpolateIndoorCapacity( above: Capacity.ManufacturersContainer, below: Capacity.ManufacturersContainer ) async -> Capacity.Cooling { - let total = Double(below.totalCapacity) - + ( - Double(above.totalCapacity - below.totalCapacity) - / Double(above.wetBulb - below.wetBulb) - ) - * Double(63 - below.wetBulb) + let total = below.totalCapacity + + ((above.totalCapacity - below.totalCapacity) / (above.wetBulb - below.wetBulb)) + * (63 - below.wetBulb) let sensible = Double(below.sensibleCapacity) - + Double(above.sensibleCapacity - below.sensibleCapacity) - / Double(below.totalCapacity - above.totalCapacity) - * Double(below.totalCapacity) - - total + + (Double(above.sensibleCapacity) - Double(below.sensibleCapacity)) + / (Double(below.totalCapacity) - Double(above.totalCapacity)) + * (Double(below.totalCapacity) - Double(total)) - return .init(total: Int(total), sensible: Int(sensible)) + return .init(total: total, sensible: Int(sensible)) } private func interpolateOutdoorCapacity( @@ -175,7 +194,7 @@ private func interpolateOutdoorCapacity( * ((belowCapacity - aboveCapacity) / (aboveOutdoorTemperature - belowOutdoorTemperature)) } -private extension Interpolate.OneWayIndoor { +private extension CoolingInterpolation.OneWayIndoor { func interpolatedCapacity() async -> Capacity.Cooling { return await interpolateIndoorCapacity( @@ -185,7 +204,7 @@ private extension Interpolate.OneWayIndoor { } } -private extension Interpolate.OneWayOutdoor { +private extension CoolingInterpolation.OneWayOutdoor { func interpolatedCapacity(outdoorDesignTemperature: Int) async -> Capacity.Cooling { let total = await interpolate( outdoorDesignTemperature: outdoorDesignTemperature, @@ -203,8 +222,8 @@ private extension Interpolate.OneWayOutdoor { private func interpolate( outdoorDesignTemperature: Int, - aboveCapacity: KeyPath, - belowCapacity: KeyPath, + aboveCapacity: KeyPath, + belowCapacity: KeyPath, ) async -> Int { return await interpolateOutdoorCapacity( outdoorDesignTemperature: outdoorDesignTemperature, @@ -216,7 +235,7 @@ private extension Interpolate.OneWayOutdoor { } } -private extension Interpolate.TwoWay { +private extension CoolingInterpolation.TwoWay { func interpolatedCapacity(outdoorDesignTemperature: Int) async -> Capacity.Cooling { let aboveIndoorInterpolation = await self.aboveIndoorInterpolation() @@ -233,7 +252,7 @@ private extension Interpolate.TwoWay { aboveIndoor: Capacity.Cooling, belowIndoor: Capacity.Cooling ) async -> Capacity.Cooling { - let request = Interpolate.OneWayOutdoor( + let request = CoolingInterpolation.OneWayOutdoor( airflow: 0, wetBulb: 63, capacities: .init( @@ -271,19 +290,19 @@ private extension Interpolate.TwoWay { // MARK: - Validations // Basic validations of the request. -extension Interpolate.Request: AsyncValidatable { +extension CoolingInterpolation.Request: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { - AsyncValidator.validate(\.designInfo) - AsyncValidator.validate(\.houseLoad) + AsyncValidator.validate(\.summerDesignInfo) + AsyncValidator.validate(\.coolingLoad) AsyncValidator.validate(\.interpolation) } } } -extension Interpolate.InterpolationRequest: AsyncValidatable { +extension CoolingInterpolation.InterpolationRequest: AsyncValidatable { - public typealias Value = Interpolate.InterpolationRequest + public typealias Value = CoolingInterpolation.InterpolationRequest public func validate(_ value: Self) async throws { switch value { @@ -299,7 +318,7 @@ extension Interpolate.InterpolationRequest: AsyncValidatable { } } -extension Interpolate.OneWayIndoor: AsyncValidatable { +extension CoolingInterpolation.OneWayIndoor: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { AsyncValidator.greaterThan(\.airflow, 0) @@ -309,7 +328,7 @@ extension Interpolate.OneWayIndoor: AsyncValidatable { } } -extension Interpolate.OneWayIndoor.Capacities: AsyncValidatable { +extension CoolingInterpolation.OneWayIndoor.Capacities: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { AsyncValidator.validate(\.aboveDewpoint) @@ -318,7 +337,7 @@ extension Interpolate.OneWayIndoor.Capacities: AsyncValidatable { } } -extension Interpolate.OneWayOutdoor: AsyncValidatable { +extension CoolingInterpolation.OneWayOutdoor: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { AsyncValidator.greaterThan(\.airflow, 0) @@ -328,7 +347,7 @@ extension Interpolate.OneWayOutdoor: AsyncValidatable { } } -extension Interpolate.OneWayOutdoor.Capacities: AsyncValidatable { +extension CoolingInterpolation.OneWayOutdoor.Capacities: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { @@ -338,7 +357,7 @@ extension Interpolate.OneWayOutdoor.Capacities: AsyncValidatable { } } -extension Interpolate.OneWayOutdoor.Capacities.Capacity: AsyncValidatable { +extension CoolingInterpolation.OneWayOutdoor.Capacities.Capacity: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { AsyncValidator.greaterThan(\.outdoorTemperature, 0) @@ -349,7 +368,7 @@ extension Interpolate.OneWayOutdoor.Capacities.Capacity: AsyncValidatable { } } -extension Interpolate.TwoWay: AsyncValidatable { +extension CoolingInterpolation.TwoWay: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { AsyncValidator.greaterThan(\.airflow, 0) @@ -358,7 +377,7 @@ extension Interpolate.TwoWay: AsyncValidatable { } } -extension Interpolate.TwoWay.Capacities: AsyncValidatable { +extension CoolingInterpolation.TwoWay.Capacities: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { AsyncValidator.validate(\.above) @@ -367,7 +386,7 @@ extension Interpolate.TwoWay.Capacities: AsyncValidatable { } } -extension Interpolate.TwoWay.Capacities.CapacityContainer: AsyncValidatable { +extension CoolingInterpolation.TwoWay.Capacities.CapacityContainer: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { AsyncValidator.validate(\.aboveDewPoint) diff --git a/Sources/ManualS/Internal/SizingLimits.swift b/Sources/ManualS/Internal/SizingLimits.swift index c4cb328..71a4a98 100644 --- a/Sources/ManualS/Internal/SizingLimits.swift +++ b/Sources/ManualS/Internal/SizingLimits.swift @@ -4,7 +4,7 @@ import Models extension SizingLimits.Request { func respond() async throws -> SizingLimits.Response { return try .init( - oversizing: .oversizingLimit(systemType: systemType, houseLoad: houseLoad), + oversizing: .oversizingLimit(systemType: systemType, houseLoad: coolingLoad), undersizing: .undersizingLimits ) } @@ -20,7 +20,7 @@ private extension SizingLimits.Limits { static func oversizingLimit( systemType: SystemType, - houseLoad: HouseLoad? + houseLoad: Capacity.Cooling? ) throws -> Self { switch systemType { case let .heatingOnly(type: type): @@ -51,7 +51,7 @@ private extension SystemType.HeatingOnlyType { } private func coolingTotalOversizingLimit( - houseLoad: HouseLoad?, + houseLoad: Capacity.Cooling?, compressorType: SystemType.CompressorType, climateType: SystemType.ClimateType ) throws -> Int { @@ -67,7 +67,7 @@ private func coolingTotalOversizingLimit( guard let houseLoad else { throw HouseLoadError() } - let decimal = Double(houseLoad.coolingTotal + 15000) / Double(houseLoad.coolingTotal) + let decimal = Double(houseLoad.total + 15000) / Double(houseLoad.total) return Int(round(decimal * 100)) } } diff --git a/Sources/ManualS/ManualS.swift b/Sources/ManualS/ManualS.swift index ef7832c..88eaa4c 100644 --- a/Sources/ManualS/ManualS.swift +++ b/Sources/ManualS/ManualS.swift @@ -13,7 +13,7 @@ public extension DependencyValues { public struct ManualS: Sendable { public var balancePoint: @Sendable (BalancePoint.Request) async throws -> BalancePoint.Response public var derating: @Sendable (Derating.Request) async throws -> Derating.Response - public var interpolate: @Sendable (Interpolate.Request) async throws -> Interpolate.Response + public var coolingInterpolation: @Sendable (CoolingInterpolation.Request) async throws -> CoolingInterpolation.Response public var requiredKW: @Sendable (RequiredKW.Request) async throws -> RequiredKW.Response public var sizingLimits: @Sendable (SizingLimits.Request) async throws -> SizingLimits.Response } @@ -22,7 +22,7 @@ extension ManualS: DependencyKey { public static let liveValue = Self( balancePoint: { try await $0.respond() }, derating: { try await $0.respond() }, - interpolate: { try await $0.respond() }, + coolingInterpolation: { try await $0.respond() }, requiredKW: { try await $0.respond() }, sizingLimits: { try await $0.respond() } ) diff --git a/Sources/Models/Interpolate.swift b/Sources/Models/CoolingInterpolation.swift similarity index 70% rename from Sources/Models/Interpolate.swift rename to Sources/Models/CoolingInterpolation.swift index 6ffb6b6..0224d2d 100644 --- a/Sources/Models/Interpolate.swift +++ b/Sources/Models/CoolingInterpolation.swift @@ -1,20 +1,23 @@ -public enum Interpolate { +public enum CoolingInterpolation { public struct Request: Codable, Equatable, Sendable { - public let designInfo: DesignInfo - public let houseLoad: HouseLoad + public let elevation: Int? + public let summerDesignInfo: DesignInfo.Summer + public let coolingLoad: Models.Capacity.Cooling public let systemType: SystemType public let interpolation: InterpolationRequest public init( - designInfo: DesignInfo, - houseLoad: HouseLoad, + elevation: Int? = nil, + summerDesignInfo: DesignInfo.Summer, + coolingLoad: Capacity.Cooling, systemType: SystemType, interpolation: InterpolationRequest ) { - self.designInfo = designInfo - self.houseLoad = houseLoad + self.elevation = elevation + self.summerDesignInfo = summerDesignInfo + self.coolingLoad = coolingLoad self.systemType = systemType self.interpolation = interpolation } @@ -24,18 +27,18 @@ public enum Interpolate { public let failed: Bool public let failures: [String]? - public let interpolatedCapacity: Capacity.Cooling + public let interpolatedCapacity: Models.Capacity.Cooling public let excessLatent: Int - public let finalCapacityAtDesign: Capacity.Cooling + public let finalCapacityAtDesign: Models.Capacity.Cooling public let altitudeDerating: AdjustmentMultiplier? - public let capacityAsPercentOfLoad: Capacity.Cooling + public let capacityAsPercentOfLoad: Models.Capacity.Cooling public let sizingLimits: SizingLimits.Response public init( failures: [String]? = nil, - interpolatedCapacity: Capacity.Cooling, + interpolatedCapacity: Models.Capacity.Cooling, excessLatent: Int, - finalCapacityAtDesign: Capacity.Cooling, + finalCapacityAtDesign: Models.Capacity.Cooling, altitudeDerating: AdjustmentMultiplier? = nil, capacityAsPercentOfLoad: Capacity.Cooling, sizingLimits: SizingLimits.Response @@ -52,7 +55,7 @@ public enum Interpolate { } public enum InterpolationRequest: Codable, Equatable, Sendable { - case noInterpolation(Capacity.ManufacturersCooling, AdjustmentMultiplier? = nil) + case noInterpolation(Models.Capacity.ManufacturersCooling, AdjustmentMultiplier? = nil) case oneWayIndoor(OneWayIndoor) case oneWayOutdoor(OneWayOutdoor) case twoWay(TwoWay) @@ -68,7 +71,7 @@ public enum Interpolate { public init( airflow: Int, outdoorTemperature: Int, - capacities: Interpolate.OneWayIndoor.Capacities, + capacities: CoolingInterpolation.OneWayIndoor.Capacities, adjustmentMultipliers: AdjustmentMultiplier? = nil ) { self.airflow = airflow @@ -79,10 +82,13 @@ public enum Interpolate { public struct Capacities: Codable, Equatable, Sendable { - public let aboveDewpoint: Capacity.ManufacturersContainer - public let belowDewpoint: Capacity.ManufacturersContainer + public let aboveDewpoint: Models.Capacity.ManufacturersContainer + public let belowDewpoint: Models.Capacity.ManufacturersContainer - public init(aboveDewpoint: Capacity.ManufacturersContainer, belowDewpoint: Capacity.ManufacturersContainer) { + public init( + aboveDewpoint: Models.Capacity.ManufacturersContainer, + belowDewpoint: Models.Capacity.ManufacturersContainer + ) { self.aboveDewpoint = aboveDewpoint self.belowDewpoint = belowDewpoint } @@ -99,7 +105,7 @@ public enum Interpolate { public init( airflow: Int, wetBulb: Int, - capacities: Interpolate.OneWayOutdoor.Capacities, + capacities: CoolingInterpolation.OneWayOutdoor.Capacities, adjustmentMultipliers: AdjustmentMultiplier? = nil ) { self.airflow = airflow @@ -114,8 +120,8 @@ public enum Interpolate { public let belowOutdoor: Capacity public init( - aboveOutdoor: Interpolate.OneWayOutdoor.Capacities.Capacity, - belowOutdoor: Interpolate.OneWayOutdoor.Capacities.Capacity + aboveOutdoor: CoolingInterpolation.OneWayOutdoor.Capacities.Capacity, + belowOutdoor: CoolingInterpolation.OneWayOutdoor.Capacities.Capacity ) { self.aboveOutdoor = aboveOutdoor self.belowOutdoor = belowOutdoor @@ -144,7 +150,7 @@ public enum Interpolate { public init( airflow: Int, - capacities: Interpolate.TwoWay.Capacities, + capacities: CoolingInterpolation.TwoWay.Capacities, adjustmentMultipliers: AdjustmentMultiplier? = nil ) { self.airflow = airflow @@ -158,8 +164,8 @@ public enum Interpolate { public let below: CapacityContainer public init( - above: Interpolate.TwoWay.Capacities.CapacityContainer, - below: Interpolate.TwoWay.Capacities.CapacityContainer + above: CoolingInterpolation.TwoWay.Capacities.CapacityContainer, + below: CoolingInterpolation.TwoWay.Capacities.CapacityContainer ) { self.above = above self.below = below @@ -168,13 +174,13 @@ public enum Interpolate { public struct CapacityContainer: Codable, Equatable, Sendable { public let outdoorTemperature: Int - public let aboveDewPoint: Capacity.ManufacturersContainer - public let belowDewPoint: Capacity.ManufacturersContainer + public let aboveDewPoint: Models.Capacity.ManufacturersContainer + public let belowDewPoint: Models.Capacity.ManufacturersContainer public init( outdoorTemperature: Int, - aboveDewPoint: Capacity.ManufacturersContainer, - belowDewPoint: Capacity.ManufacturersContainer + aboveDewPoint: Models.Capacity.ManufacturersContainer, + belowDewPoint: Models.Capacity.ManufacturersContainer ) { self.outdoorTemperature = outdoorTemperature self.aboveDewPoint = aboveDewPoint diff --git a/Sources/Models/SizingLimits.swift b/Sources/Models/SizingLimits.swift index b4a0f41..9c7dffe 100644 --- a/Sources/Models/SizingLimits.swift +++ b/Sources/Models/SizingLimits.swift @@ -3,11 +3,11 @@ public enum SizingLimits { public struct Request: Codable, Equatable, Sendable { public let systemType: SystemType - public let houseLoad: HouseLoad? + public let coolingLoad: Capacity.Cooling? - public init(systemType: SystemType, houseLoad: HouseLoad? = nil) { + public init(systemType: SystemType, coolingLoad: Capacity.Cooling? = nil) { self.systemType = systemType - self.houseLoad = houseLoad + self.coolingLoad = coolingLoad } } diff --git a/Tests/ManualSTests/ManualSTests.swift b/Tests/ManualSTests/ManualSTests.swift index 5b8ca9e..32f4bd7 100644 --- a/Tests/ManualSTests/ManualSTests.swift +++ b/Tests/ManualSTests/ManualSTests.swift @@ -6,6 +6,138 @@ import Testing @Suite("ManualSTests") struct ManualSTests { + @Test + func coolingInterpolation_noInterpolation() async throws { + try await withDependencies { + $0.manualS = .liveValue + } operation: { + @Dependency(\.manualS) var manualS + + let response = try await manualS.coolingInterpolation(.init( + summerDesignInfo: .mock, + coolingLoad: .mockCoolingLoad, + systemType: .airToAir(type: .airConditioner, compressor: .singleSpeed, climate: .mildWinterOrLatentLoad), + interpolation: .noInterpolation( + .init( + airflow: 800, + capacity: .init( + dryBulbTemperature: 75, + wetBulbTemperature: 63, + outdoorTemperature: 90, + totalCapacity: 22600, + sensibleCapacity: 16850 + ) + ), + nil + ) + )) + + #expect(response.failed) + #expect(response.failures == ["Oversizing total failure."]) + #expect(response.interpolatedCapacity == .init(total: 22600, sensible: 16850)) + #expect(response.excessLatent == 886) + #expect(response.finalCapacityAtDesign == .init(total: 22600, sensible: 17736)) + #expect(response.capacityAsPercentOfLoad == .init(total: 126, sensible: 127, latent: 122)) + } + } + + @Test + func coolingInterpolation_oneWayIndoor() async throws { + try await withDependencies { + $0.manualS = .liveValue + } operation: { + @Dependency(\.manualS) var manualS + + let response = try await manualS.coolingInterpolation(.init( + summerDesignInfo: .mock, + coolingLoad: .mockCoolingLoad, + systemType: .airToAir(type: .airConditioner, compressor: .singleSpeed, climate: .mildWinterOrLatentLoad), + interpolation: .oneWayIndoor(.init( + airflow: 800, + outdoorTemperature: 95, + capacities: .init( + aboveDewpoint: .init(wetBulb: 67, totalCapacity: 24828, sensibleCapacity: 15937), + belowDewpoint: .init(wetBulb: 62, totalCapacity: 23046, sensibleCapacity: 19078) + ) + )) + )) + + #expect(response.failed) + #expect(response.failures == ["Oversizing total failure."]) + #expect(response.interpolatedCapacity == .init(total: 23402, sensible: 18450)) + #expect(response.excessLatent == 487) + #expect(response.finalCapacityAtDesign == .init(total: 23402, sensible: 18937)) + #expect(response.capacityAsPercentOfLoad == .init(total: 130, sensible: 136, latent: 112)) + } + } + + @Test + func coolingInterpolation_oneWayOutdoor() async throws { + try await withDependencies { + $0.manualS = .liveValue + } operation: { + @Dependency(\.manualS) var manualS + + let response = try await manualS.coolingInterpolation(.init( + summerDesignInfo: .mock, + coolingLoad: .mockCoolingLoad, + systemType: .airToAir(type: .airConditioner, compressor: .singleSpeed, climate: .mildWinterOrLatentLoad), + interpolation: .oneWayOutdoor(.init( + airflow: 800, + wetBulb: 63, + capacities: .init( + aboveOutdoor: .init(outdoorTemperature: 95, totalCapacity: 22000, sensibleCapacity: 16600), + belowOutdoor: .init(outdoorTemperature: 85, totalCapacity: 23200, sensibleCapacity: 17100) + ) + )) + )) + + #expect(response.failed) + #expect(response.failures == ["Oversizing total failure."]) + #expect(response.interpolatedCapacity == .init(total: 22600, sensible: 16850)) + #expect(response.excessLatent == 886) + #expect(response.finalCapacityAtDesign == .init(total: 22600, sensible: 17736)) + #expect(response.capacityAsPercentOfLoad == .init(total: 126, sensible: 127, latent: 122)) + } + } + + @Test + func coolingInterpolation_twoWay() async throws { + try await withDependencies { + $0.manualS = .liveValue + } operation: { + @Dependency(\.manualS) var manualS + + let response = try await manualS.coolingInterpolation(.init( + summerDesignInfo: .mock, + coolingLoad: .mockCoolingLoad, + systemType: .airToAir(type: .airConditioner, compressor: .singleSpeed, climate: .mildWinterOrLatentLoad), + interpolation: .twoWay(.init( + airflow: 800, + capacities: .init( + above: .init( + outdoorTemperature: 95, + aboveDewPoint: .init(wetBulb: 67, totalCapacity: 24828, sensibleCapacity: 15937), + belowDewPoint: .init(wetBulb: 62, totalCapacity: 23046, sensibleCapacity: 19078) + ), + below: .init( + outdoorTemperature: 85, + aboveDewPoint: .init(wetBulb: 67, totalCapacity: 25986, sensibleCapacity: 16330), + belowDewPoint: .init(wetBulb: 62, totalCapacity: 24029, sensibleCapacity: 19605) + ) + ) + )) + )) + + #expect(response.failed) + #expect(response.failures == ["Oversizing total failure."]) + #expect(response.interpolatedCapacity == .init(total: 23915, sensible: 18700)) + #expect(response.excessLatent == 618) + #expect(response.finalCapacityAtDesign == .init(total: 23915, sensible: 19318)) + #expect(response.capacityAsPercentOfLoad == .init(total: 133, sensible: 139, latent: 115)) + } + } + @Test func balancePoint() async throws { try await withDependencies { @@ -52,7 +184,7 @@ struct ManualSTests { let limits = try await manualS.sizingLimits(.init( systemType: system, - houseLoad: .init(coolingTotal: 17872, coolingSensible: 13894, heating: 49667) + coolingLoad: .init(total: 17872, sensible: 13894) )) #expect(limits.oversizing.coolingLatent == 150) @@ -211,3 +343,11 @@ extension SystemType.HeatingOnlyType { } } } + +extension Capacity.Cooling { + static let mockCoolingLoad = Self(total: 17872, sensible: 13894) +} + +extension DesignInfo.Summer { + static let mock = Self(outdoorTemperature: 90, indoorTemperature: 75, indoorHumidity: 50) +}