From 5b2e20921c62de5bd3428b1afb4f7cf820df62de Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Thu, 13 Mar 2025 12:38:42 -0400 Subject: [PATCH] feat: Adds proposed kw interpolation. --- .../Internal/ProposedKWInterpolation.swift | 61 +++++++++++++++++ Sources/ManualS/ManualS.swift | 30 +++++---- Sources/Models/ProposedKWInterpolation.swift | 66 +++++++++++++++++++ Tests/ManualSTests/ManualSTests.swift | 54 +++++++++++++-- 4 files changed, 191 insertions(+), 20 deletions(-) create mode 100644 Sources/ManualS/Internal/ProposedKWInterpolation.swift create mode 100644 Sources/Models/ProposedKWInterpolation.swift diff --git a/Sources/ManualS/Internal/ProposedKWInterpolation.swift b/Sources/ManualS/Internal/ProposedKWInterpolation.swift new file mode 100644 index 0000000..3b818ec --- /dev/null +++ b/Sources/ManualS/Internal/ProposedKWInterpolation.swift @@ -0,0 +1,61 @@ +import CoreFoundation +import Models +import Validations + +extension ProposedKWInterpolation.Request { + + private static let oversizingLimit = 175 + + func respond() async throws -> ProposedKWInterpolation.Response { + try await validate() + let (requiredKW, optionalHeatPump) = try await requiredKW() + let capacityAsPercentOfLoad = normalizePercentage(proposedKW, requiredKW) + + var failures = [String]() + if capacityAsPercentOfLoad > Self.oversizingLimit { + failures.append("Oversizing failure.") + } + + return .init( + failures: failures.isEmpty ? nil : failures, + supplementalHeatRequired: requiredKW, + capacityAsPercentOfLoad: capacityAsPercentOfLoad, + oversizingLimit: Self.oversizingLimit, + altitudeAdjustmentMultiplier: optionalHeatPump?.altitudeAdjustmentMultiplier, + balancePointTemperature: optionalHeatPump?.balancePointTemperature, + capacityAtDesign: optionalHeatPump?.capacityAtDesign, + finalCapacity: optionalHeatPump?.finalCapacity + ) + } + + private var heatPumpRequest: HeatPumpHeatingInterpolation.Request? { + guard let winterDesignTemperature, let climateType, let capacity else { + return nil + } + return .init( + winterDesignTemperature: winterDesignTemperature, + heatLoss: heatLoss, + climateType: climateType, + capacity: capacity + ) + } + + private func requiredKW() async throws -> (Int, HeatPumpHeatingInterpolation.Response?) { + guard let heatPumpRequest = heatPumpRequest else { + let requiredKW = try await RequiredKW.Request(heatLoss: heatLoss).respond() + return (Int(requiredKW.requiredKW), nil) + } + let heatPumpResponse = try await heatPumpRequest.respond() + return (heatPumpResponse.supplementalHeatRequired, heatPumpResponse) + } + +} + +extension ProposedKWInterpolation.Request: AsyncValidatable { + public var body: some AsyncValidation { + AsyncValidator.accumulating { + AsyncValidator.greaterThan(\.heatLoss, 0) + AsyncValidator.greaterThan(\.proposedKW, 0) + } + } +} diff --git a/Sources/ManualS/ManualS.swift b/Sources/ManualS/ManualS.swift index 77d4243..a74ad6d 100644 --- a/Sources/ManualS/ManualS.swift +++ b/Sources/ManualS/ManualS.swift @@ -12,35 +12,39 @@ public extension DependencyValues { @DependencyClient 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: Interpolations + public var requiredKW: @Sendable (RequiredKW.Request) async throws -> RequiredKW.Response + public var sizingLimits: @Sendable (SizingLimits.Request) async throws -> SizingLimits.Response +} - public var coolingInterpolation: - @Sendable (CoolingInterpolation.Request) async throws -> CoolingInterpolation.Response +@DependencyClient +public struct Interpolations: Sendable { + public var cooling: @Sendable (CoolingInterpolation.Request) async throws -> CoolingInterpolation.Response - public var furnaceInterpolation: - @Sendable (FurnaceInterpolation.Request) async throws -> FurnaceInterpolation.Response + public var furnace: @Sendable (FurnaceInterpolation.Request) async throws -> FurnaceInterpolation.Response - public var heatPumpHeatingInterpolation: + public var heatPumpHeating: @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 + public var proposeKW: @Sendable (ProposedKWInterpolation.Request) async throws -> ProposedKWInterpolation.Response } extension ManualS: DependencyKey { public static let liveValue = Self( balancePoint: { try await $0.respond() }, derating: { try await $0.respond() }, - coolingInterpolation: { try await $0.respond() }, - furnaceInterpolation: { try await $0.respond() }, - heatPumpHeatingInterpolation: { try await $0.respond() }, + interpolate: .init( + cooling: { try await $0.respond() }, + furnace: { try await $0.respond() }, + heatPumpHeating: { try await $0.respond() }, + proposeKW: { try await $0.respond() } + ), requiredKW: { try await $0.respond() }, sizingLimits: { try await $0.respond() } ) } extension ManualS: TestDependencyKey { - public static let testValue: ManualS = Self() + public static let testValue: ManualS = Self(interpolate: .init()) } diff --git a/Sources/Models/ProposedKWInterpolation.swift b/Sources/Models/ProposedKWInterpolation.swift new file mode 100644 index 0000000..fa163b6 --- /dev/null +++ b/Sources/Models/ProposedKWInterpolation.swift @@ -0,0 +1,66 @@ +public enum ProposedKWInterpolation { + public struct Request: Codable, Equatable, Sendable { + + public let elevation: Int? + + // These are required if also doing a heat pump heating interpolation. + public let winterDesignTemperature: Int? + public let climateType: SystemType.ClimateType? + public let capacity: Capacity.HeatPumpHeating? + + public let heatLoss: Int + public let proposedKW: Int + + public init( + elevation: Int? = nil, + winterDesignTemperature: Int? = nil, + climateType: SystemType.ClimateType? = nil, + capacity: Capacity.HeatPumpHeating? = nil, + heatLoss: Int, + proposedKW: Int + ) { + self.elevation = elevation + self.winterDesignTemperature = winterDesignTemperature + self.climateType = climateType + self.capacity = capacity + self.heatLoss = heatLoss + self.proposedKW = proposedKW + } + } + + public struct Response: Codable, Equatable, Sendable { + + public let failed: Bool + public let failures: [String]? + public let supplementalHeatRequired: Int + public let capacityAsPercentOfLoad: Int + public let oversizingLimit: Int + + // These are if a heat pump interpolation is also done. + public let altitudeAdjustmentMultiplier: Double? + public let balancePointTemperature: Int? + public let capacityAtDesign: Int? + public let finalCapacity: Capacity.HeatPumpHeating? + + public init( + failures: [String]? = nil, + supplementalHeatRequired: Int, + capacityAsPercentOfLoad: Int, + oversizingLimit: Int, + altitudeAdjustmentMultiplier: Double? = nil, + balancePointTemperature: Int? = nil, + capacityAtDesign: Int? = nil, + finalCapacity: Capacity.HeatPumpHeating? = nil + ) { + self.failed = failures == nil ? false : !failures!.isEmpty + self.failures = failures + self.supplementalHeatRequired = supplementalHeatRequired + self.capacityAsPercentOfLoad = capacityAsPercentOfLoad + self.oversizingLimit = oversizingLimit + self.altitudeAdjustmentMultiplier = altitudeAdjustmentMultiplier + self.balancePointTemperature = balancePointTemperature + self.capacityAtDesign = capacityAtDesign + self.finalCapacity = finalCapacity + } + } +} diff --git a/Tests/ManualSTests/ManualSTests.swift b/Tests/ManualSTests/ManualSTests.swift index 3a1200b..5db4ec8 100644 --- a/Tests/ManualSTests/ManualSTests.swift +++ b/Tests/ManualSTests/ManualSTests.swift @@ -13,7 +13,7 @@ struct ManualSTests { } operation: { @Dependency(\.manualS) var manualS - let response = try await manualS.coolingInterpolation(.init( + let response = try await manualS.interpolate.cooling(.init( summerDesignInfo: .mock, coolingLoad: .mockCoolingLoad, systemType: .airToAir(type: .airConditioner, compressor: .singleSpeed, climate: .mildWinterOrLatentLoad), @@ -48,7 +48,7 @@ struct ManualSTests { } operation: { @Dependency(\.manualS) var manualS - let response = try await manualS.coolingInterpolation(.init( + let response = try await manualS.interpolate.cooling(.init( summerDesignInfo: .mock, coolingLoad: .mockCoolingLoad, systemType: .airToAir(type: .airConditioner, compressor: .singleSpeed, climate: .mildWinterOrLatentLoad), @@ -78,7 +78,7 @@ struct ManualSTests { } operation: { @Dependency(\.manualS) var manualS - let response = try await manualS.coolingInterpolation(.init( + let response = try await manualS.interpolate.cooling(.init( summerDesignInfo: .mock, coolingLoad: .mockCoolingLoad, systemType: .airToAir(type: .airConditioner, compressor: .singleSpeed, climate: .mildWinterOrLatentLoad), @@ -108,7 +108,7 @@ struct ManualSTests { } operation: { @Dependency(\.manualS) var manualS - let response = try await manualS.coolingInterpolation(.init( + let response = try await manualS.interpolate.cooling(.init( summerDesignInfo: .mock, coolingLoad: .mockCoolingLoad, systemType: .airToAir(type: .airConditioner, compressor: .singleSpeed, climate: .mildWinterOrLatentLoad), @@ -145,7 +145,7 @@ struct ManualSTests { } operation: { @Dependency(\.manualS) var manualS - let response = try await manualS.heatPumpHeatingInterpolation(.init( + let response = try await manualS.interpolate.heatPumpHeating(.init( winterDesignTemperature: 5, heatLoss: 49667, climateType: .mildWinterOrLatentLoad, @@ -166,7 +166,7 @@ struct ManualSTests { } operation: { @Dependency(\.manualS) var manualS - let response = try await manualS.furnaceInterpolation( + let response = try await manualS.interpolate.furnace( .init( elevation: nil, winterDesignTemperature: 5, @@ -189,7 +189,7 @@ struct ManualSTests { } operation: { @Dependency(\.manualS) var manualS - let response = try await manualS.furnaceInterpolation( + let response = try await manualS.interpolate.furnace( .init( elevation: nil, winterDesignTemperature: 5, @@ -204,6 +204,46 @@ struct ManualSTests { } } + @Test + func proposedKWInterpolation() async throws { + try await withDependencies { + $0.manualS = .liveValue + } operation: { + @Dependency(\.manualS) var manualS + + let response = try await manualS.interpolate.proposeKW( + .init(heatLoss: 49667, proposedKW: 15) + ) + + #expect(!response.failed) + #expect(response.capacityAsPercentOfLoad == 107) + #expect(response.supplementalHeatRequired == 14) + } + } + + @Test + func proposedKWInterpolation_with_heatPump() async throws { + try await withDependencies { + $0.manualS = .liveValue + } operation: { + @Dependency(\.manualS) var manualS + + let response = try await manualS.interpolate.proposeKW( + .init( + winterDesignTemperature: 5, + climateType: .mildWinterOrLatentLoad, + capacity: .mock, + heatLoss: 49667, + proposedKW: 15 + ) + ) + + #expect(!response.failed) + #expect(response.capacityAsPercentOfLoad == 136) + #expect(response.supplementalHeatRequired == 11) + } + } + @Test func balancePoint() async throws { try await withDependencies {