From bd33827e53c6afae207d0b4fb3ac11097bed6a7b Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Thu, 13 Mar 2025 10:55:15 -0400 Subject: [PATCH] feat: Adds heat pump heating interpolation. --- .swiftlint.yml | 1 + .../Extensions/HouseLoad+validator.swift | 1 - .../Internal/CoolingInterpolation.swift | 6 +- .../HeatPumpHeatingInterpolation.swift | 80 +++++++++++++++++++ Sources/ManualS/ManualS.swift | 7 +- Sources/Models/CoolingInterpolation.swift | 40 +++++----- Sources/Models/Core/Capacity.swift | 1 - .../Models/HeatPumpHeatingInterpolation.swift | 47 +++++++++++ Tests/ManualSTests/ManualSTests.swift | 25 ++++++ 9 files changed, 182 insertions(+), 26 deletions(-) create mode 100644 Sources/ManualS/Internal/HeatPumpHeatingInterpolation.swift create mode 100644 Sources/Models/HeatPumpHeatingInterpolation.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 213129c..dc0e5e1 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -3,6 +3,7 @@ disabled_rules: - fuction_body_length - opening_brace - nesting + - type_body_length included: - Sources diff --git a/Sources/ManualS/Extensions/HouseLoad+validator.swift b/Sources/ManualS/Extensions/HouseLoad+validator.swift index 4bd21b4..5cc5885 100644 --- a/Sources/ManualS/Extensions/HouseLoad+validator.swift +++ b/Sources/ManualS/Extensions/HouseLoad+validator.swift @@ -2,7 +2,6 @@ import Models import Validations extension HouseLoad: AsyncValidatable { - public var body: some AsyncValidation { AsyncValidator.accumulating { AsyncValidator.greaterThan(\.coolingTotal, 0) diff --git a/Sources/ManualS/Internal/CoolingInterpolation.swift b/Sources/ManualS/Internal/CoolingInterpolation.swift index ba82f26..ae0ca2b 100644 --- a/Sources/ManualS/Internal/CoolingInterpolation.swift +++ b/Sources/ManualS/Internal/CoolingInterpolation.swift @@ -222,8 +222,8 @@ private extension CoolingInterpolation.OneWayOutdoor { private func interpolate( outdoorDesignTemperature: Int, - aboveCapacity: KeyPath, - belowCapacity: KeyPath + aboveCapacity: KeyPath, + belowCapacity: KeyPath ) async -> Int { return await interpolateOutdoorCapacity( outdoorDesignTemperature: outdoorDesignTemperature, @@ -357,7 +357,7 @@ extension CoolingInterpolation.OneWayOutdoor.Capacities: AsyncValidatable { } } -extension CoolingInterpolation.OneWayOutdoor.Capacities.Capacity: AsyncValidatable { +extension CoolingInterpolation.OneWayOutdoor.Capacities.CapacityContainer: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { AsyncValidator.greaterThan(\.outdoorTemperature, 0) diff --git a/Sources/ManualS/Internal/HeatPumpHeatingInterpolation.swift b/Sources/ManualS/Internal/HeatPumpHeatingInterpolation.swift new file mode 100644 index 0000000..9d1c5de --- /dev/null +++ b/Sources/ManualS/Internal/HeatPumpHeatingInterpolation.swift @@ -0,0 +1,80 @@ +import CoreFoundation +import Models +import Validations + +extension HeatPumpHeatingInterpolation.Request { + + func respond() async throws -> HeatPumpHeatingInterpolation.Response { + try await validate() + let altitudeAdjustmentMultiplier = try await altitudeAdjustmentMultiplier() + let finalCapacity = finalCapacity(altitudeAdjustmentMultiplier) + let balancePointTemperature = try await balancePoint(finalCapacity) + let capacityAtDesign = capacityAtDesign(finalCapacity) + let requiredKW = try await requiredKW(capacityAtDesign) + return .init( + altitudeAdjustmentMultiplier: altitudeAdjustmentMultiplier, + balancePointTemperature: balancePointTemperature, + capacityAtDesign: capacityAtDesign, + finalCapacity: finalCapacity, + supplementalHeatRequired: requiredKW + ) + } + + private func balancePoint(_ finalCapacity: Capacity.HeatPumpHeating) async throws -> Int { + let response = try await BalancePoint.Request( + winterDesignTemperature: winterDesignTemperature, + heatLoss: heatLoss, + heatPumpCapacity: finalCapacity + ).respond() + return Int(response.balancePointTemperature) + } + + private func altitudeAdjustmentMultiplier() async throws -> Double? { + guard let elevation else { return nil } + + let response = try await Derating.Request( + elevation: elevation, + systemType: .airToAir(type: .heatPump, compressor: .singleSpeed, climate: climateType) + ).respond() + + return response.heating + } + + private func finalCapacity( + _ adjustmentMultiplier: Double? + ) -> Capacity.HeatPumpHeating { + guard let adjustmentMultiplier else { return capacity } + return .init( + at47: Int(Double(capacity.at47) * adjustmentMultiplier), + at17: Int(Double(capacity.at17) * adjustmentMultiplier) + ) + } + + private func capacityAtDesign( + _ finalCapacity: Capacity.HeatPumpHeating + ) -> Int { + let outdoorTemperature = Double(winterDesignTemperature) + let derating = Double((finalCapacity.at47 - finalCapacity.at17) / 30) + * (17 - outdoorTemperature) + return Int(Double(finalCapacity.at17) - derating) + } + + private func requiredKW(_ capacityAtDesign: Int) async throws -> Int { + let response = try await RequiredKW.Request( + capacityAtDesign: capacityAtDesign, + heatLoss: heatLoss + ).respond() + return Int(response.requiredKW) + } + +} + +extension HeatPumpHeatingInterpolation.Request: AsyncValidatable { + + public var body: some AsyncValidation { + AsyncValidator.accumulating { + AsyncValidator.greaterThan(\.heatLoss, 0) + AsyncValidator.validate(\.capacity) + } + } +} diff --git a/Sources/ManualS/ManualS.swift b/Sources/ManualS/ManualS.swift index 88eaa4c..69f2d5a 100644 --- a/Sources/ManualS/ManualS.swift +++ b/Sources/ManualS/ManualS.swift @@ -13,7 +13,11 @@ 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 coolingInterpolation: @Sendable (CoolingInterpolation.Request) async throws -> CoolingInterpolation.Response + public var coolingInterpolation: + @Sendable (CoolingInterpolation.Request) async throws -> CoolingInterpolation.Response + public var heatPumpHeatingInterpolation: + @Sendable (HeatPumpHeatingInterpolation.Request) async throws -> HeatPumpHeatingInterpolation.Response + public var requiredKW: @Sendable (RequiredKW.Request) async throws -> RequiredKW.Response public var sizingLimits: @Sendable (SizingLimits.Request) async throws -> SizingLimits.Response } @@ -23,6 +27,7 @@ extension ManualS: DependencyKey { balancePoint: { try await $0.respond() }, derating: { try await $0.respond() }, coolingInterpolation: { try await $0.respond() }, + heatPumpHeatingInterpolation: { try await $0.respond() }, requiredKW: { try await $0.respond() }, sizingLimits: { try await $0.respond() } ) diff --git a/Sources/Models/CoolingInterpolation.swift b/Sources/Models/CoolingInterpolation.swift index 0224d2d..81fc10c 100644 --- a/Sources/Models/CoolingInterpolation.swift +++ b/Sources/Models/CoolingInterpolation.swift @@ -4,7 +4,7 @@ public enum CoolingInterpolation { public let elevation: Int? public let summerDesignInfo: DesignInfo.Summer - public let coolingLoad: Models.Capacity.Cooling + public let coolingLoad: Capacity.Cooling public let systemType: SystemType public let interpolation: InterpolationRequest @@ -27,18 +27,18 @@ public enum CoolingInterpolation { public let failed: Bool public let failures: [String]? - public let interpolatedCapacity: Models.Capacity.Cooling + public let interpolatedCapacity: Capacity.Cooling public let excessLatent: Int - public let finalCapacityAtDesign: Models.Capacity.Cooling + public let finalCapacityAtDesign: Capacity.Cooling public let altitudeDerating: AdjustmentMultiplier? - public let capacityAsPercentOfLoad: Models.Capacity.Cooling + public let capacityAsPercentOfLoad: Capacity.Cooling public let sizingLimits: SizingLimits.Response public init( failures: [String]? = nil, - interpolatedCapacity: Models.Capacity.Cooling, + interpolatedCapacity: Capacity.Cooling, excessLatent: Int, - finalCapacityAtDesign: Models.Capacity.Cooling, + finalCapacityAtDesign: Capacity.Cooling, altitudeDerating: AdjustmentMultiplier? = nil, capacityAsPercentOfLoad: Capacity.Cooling, sizingLimits: SizingLimits.Response @@ -55,7 +55,7 @@ public enum CoolingInterpolation { } public enum InterpolationRequest: Codable, Equatable, Sendable { - case noInterpolation(Models.Capacity.ManufacturersCooling, AdjustmentMultiplier? = nil) + case noInterpolation(Capacity.ManufacturersCooling, AdjustmentMultiplier? = nil) case oneWayIndoor(OneWayIndoor) case oneWayOutdoor(OneWayOutdoor) case twoWay(TwoWay) @@ -82,12 +82,12 @@ public enum CoolingInterpolation { public struct Capacities: Codable, Equatable, Sendable { - public let aboveDewpoint: Models.Capacity.ManufacturersContainer - public let belowDewpoint: Models.Capacity.ManufacturersContainer + public let aboveDewpoint: Capacity.ManufacturersContainer + public let belowDewpoint: Capacity.ManufacturersContainer public init( - aboveDewpoint: Models.Capacity.ManufacturersContainer, - belowDewpoint: Models.Capacity.ManufacturersContainer + aboveDewpoint: Capacity.ManufacturersContainer, + belowDewpoint: Capacity.ManufacturersContainer ) { self.aboveDewpoint = aboveDewpoint self.belowDewpoint = belowDewpoint @@ -116,18 +116,18 @@ public enum CoolingInterpolation { public struct Capacities: Codable, Equatable, Sendable { - public let aboveOutdoor: Capacity - public let belowOutdoor: Capacity + public let aboveOutdoor: CapacityContainer + public let belowOutdoor: CapacityContainer public init( - aboveOutdoor: CoolingInterpolation.OneWayOutdoor.Capacities.Capacity, - belowOutdoor: CoolingInterpolation.OneWayOutdoor.Capacities.Capacity + aboveOutdoor: CoolingInterpolation.OneWayOutdoor.Capacities.CapacityContainer, + belowOutdoor: CoolingInterpolation.OneWayOutdoor.Capacities.CapacityContainer ) { self.aboveOutdoor = aboveOutdoor self.belowOutdoor = belowOutdoor } - public struct Capacity: Codable, Equatable, Sendable { + public struct CapacityContainer: Codable, Equatable, Sendable { public let outdoorTemperature: Int public let totalCapacity: Int @@ -174,13 +174,13 @@ public enum CoolingInterpolation { public struct CapacityContainer: Codable, Equatable, Sendable { public let outdoorTemperature: Int - public let aboveDewPoint: Models.Capacity.ManufacturersContainer - public let belowDewPoint: Models.Capacity.ManufacturersContainer + public let aboveDewPoint: Capacity.ManufacturersContainer + public let belowDewPoint: Capacity.ManufacturersContainer public init( outdoorTemperature: Int, - aboveDewPoint: Models.Capacity.ManufacturersContainer, - belowDewPoint: Models.Capacity.ManufacturersContainer + aboveDewPoint: Capacity.ManufacturersContainer, + belowDewPoint: Capacity.ManufacturersContainer ) { self.outdoorTemperature = outdoorTemperature self.aboveDewPoint = aboveDewPoint diff --git a/Sources/Models/Core/Capacity.swift b/Sources/Models/Core/Capacity.swift index c01a76a..0b96882 100644 --- a/Sources/Models/Core/Capacity.swift +++ b/Sources/Models/Core/Capacity.swift @@ -37,7 +37,6 @@ public enum Capacity { } } - // TODO: Remove. public struct ManufacturersCooling: Codable, Equatable, Sendable { public let airflow: Int diff --git a/Sources/Models/HeatPumpHeatingInterpolation.swift b/Sources/Models/HeatPumpHeatingInterpolation.swift new file mode 100644 index 0000000..ab7fb53 --- /dev/null +++ b/Sources/Models/HeatPumpHeatingInterpolation.swift @@ -0,0 +1,47 @@ +public enum HeatPumpHeatingInterpolation { + public struct Request: Codable, Equatable, Sendable { + + public let elevation: Int? + public let winterDesignTemperature: Int + public let heatLoss: Int + public let climateType: SystemType.ClimateType + public let capacity: Capacity.HeatPumpHeating + + public init( + elevation: Int? = nil, + winterDesignTemperature: Int, + heatLoss: Int, + climateType: SystemType.ClimateType, + capacity: Capacity.HeatPumpHeating + ) { + self.elevation = elevation + self.winterDesignTemperature = winterDesignTemperature + self.heatLoss = heatLoss + self.climateType = climateType + self.capacity = capacity + } + } + + public struct Response: Codable, Equatable, Sendable { + + public let altitudeAdjustmentMultiplier: Double? + public let balancePointTemperature: Int + public let capacityAtDesign: Int + public let finalCapacity: Capacity.HeatPumpHeating + public let supplementalHeatRequired: Int + + public init( + altitudeAdjustmentMultiplier: Double? = nil, + balancePointTemperature: Int, + capacityAtDesign: Int, + finalCapacity: Capacity.HeatPumpHeating, + supplementalHeatRequired: Int + ) { + self.altitudeAdjustmentMultiplier = altitudeAdjustmentMultiplier + self.balancePointTemperature = balancePointTemperature + self.capacityAtDesign = capacityAtDesign + self.finalCapacity = finalCapacity + self.supplementalHeatRequired = supplementalHeatRequired + } + } +} diff --git a/Tests/ManualSTests/ManualSTests.swift b/Tests/ManualSTests/ManualSTests.swift index 867b49b..fa1a564 100644 --- a/Tests/ManualSTests/ManualSTests.swift +++ b/Tests/ManualSTests/ManualSTests.swift @@ -138,6 +138,27 @@ struct ManualSTests { } } + @Test + func heatPumpHeatingInterpolation() async throws { + try await withDependencies { + $0.manualS = .liveValue + } operation: { + @Dependency(\.manualS) var manualS + + let response = try await manualS.heatPumpHeatingInterpolation(.init( + winterDesignTemperature: 5, + heatLoss: 49667, + climateType: .mildWinterOrLatentLoad, + capacity: .mock + )) + + #expect(response.finalCapacity == .mock) + #expect(response.capacityAtDesign == 11308) + #expect(response.balancePointTemperature == 38) + #expect(response.supplementalHeatRequired == 11) + } + } + @Test func balancePoint() async throws { try await withDependencies { @@ -351,3 +372,7 @@ extension Capacity.Cooling { extension DesignInfo.Summer { static let mock = Self(outdoorTemperature: 90, indoorTemperature: 75, indoorHumidity: 50) } + +extension Capacity.HeatPumpHeating { + static let mock = Self(at47: 24600, at17: 15100) +}