feat: Adds heat pump heating interpolation.
All checks were successful
CI / Ubuntu (push) Successful in 11m46s

This commit is contained in:
2025-03-13 10:55:15 -04:00
parent ec787b9f4d
commit bd33827e53
9 changed files with 182 additions and 26 deletions

View File

@@ -3,6 +3,7 @@ disabled_rules:
- fuction_body_length
- opening_brace
- nesting
- type_body_length
included:
- Sources

View File

@@ -2,7 +2,6 @@ import Models
import Validations
extension HouseLoad: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.coolingTotal, 0)

View File

@@ -222,8 +222,8 @@ private extension CoolingInterpolation.OneWayOutdoor {
private func interpolate(
outdoorDesignTemperature: Int,
aboveCapacity: KeyPath<CoolingInterpolation.OneWayOutdoor.Capacities.Capacity, Int>,
belowCapacity: KeyPath<CoolingInterpolation.OneWayOutdoor.Capacities.Capacity, Int>
aboveCapacity: KeyPath<CoolingInterpolation.OneWayOutdoor.Capacities.CapacityContainer, Int>,
belowCapacity: KeyPath<CoolingInterpolation.OneWayOutdoor.Capacities.CapacityContainer, Int>
) 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<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.outdoorTemperature, 0)

View File

@@ -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<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.heatLoss, 0)
AsyncValidator.validate(\.capacity)
}
}
}

View File

@@ -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() }
)

View File

@@ -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

View File

@@ -37,7 +37,6 @@ public enum Capacity {
}
}
// TODO: Remove.
public struct ManufacturersCooling: Codable, Equatable, Sendable {
public let airflow: Int

View File

@@ -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
}
}
}

View File

@@ -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)
}