feat: Adds furnace heating interpolations.
All checks were successful
CI / Ubuntu (push) Successful in 11m23s

This commit is contained in:
2025-03-13 11:49:32 -04:00
parent bd33827e53
commit 5f6d2a7b6c
6 changed files with 170 additions and 8 deletions

View File

@@ -0,0 +1,9 @@
import CoreFoundation
func normalizePercentage(
_ lhs: Int,
_ rhs: Int
) -> Int {
let value = Double(lhs) / Double(rhs)
return Int((value * 1000).rounded() / 10.0)
}

View File

@@ -129,14 +129,6 @@ private extension SizingLimits.Response {
}
}
private func normalizePercentage(
_ lhs: Int,
_ rhs: Int
) -> Int {
let value = Double(lhs) / Double(rhs)
return Int((value * 1000).rounded() / 10.0)
}
private extension CoolingInterpolation.InterpolationRequest {
func interpolatedCapacity(outdoorDesignTemperature: Int) async -> Capacity.Cooling {

View File

@@ -0,0 +1,55 @@
import Models
import Validations
extension FurnaceInterpolation.Request {
private static let undersizingLimit = 90
private static let oversizingLimit = 140
func respond() async throws -> FurnaceInterpolation.Response {
try await validate()
let altitudeDerating = try await altitudeDerating()
let outputCapacity = Int(
(Double(inputRating) * (altitudeDerating ?? 1.0))
* (afue / 100)
)
let capacityAsPercentOfLoad = normalizePercentage(outputCapacity, heatLoss)
var failures = [String]()
if capacityAsPercentOfLoad < Self.undersizingLimit {
failures.append("Undersizing failure.")
}
if capacityAsPercentOfLoad > Self.oversizingLimit {
failures.append("Oversizing failure.")
}
return .init(
failures: failures.isEmpty ? nil : failures,
altitudeAdjustmentMultiplier: altitudeDerating,
capacityAsPercentOfLoad: capacityAsPercentOfLoad,
oversizingLimit: Self.oversizingLimit,
undersizingLimit: Self.undersizingLimit,
outputCapacity: outputCapacity
)
}
func altitudeDerating() async throws -> Double? {
guard let elevation else { return nil }
let response = try await Derating.Request(
elevation: elevation,
systemType: .heatingOnly(type: .furnace)
).respond()
return response.heating
}
}
extension FurnaceInterpolation.Request: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.heatLoss, 0)
AsyncValidator.greaterThan(\.inputRating, 0)
AsyncValidator.greaterThan(\.afue, 0)
AsyncValidator.lessThan(\.afue, 100)
}
}
}

View File

@@ -12,13 +12,20 @@ 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 coolingInterpolation:
@Sendable (CoolingInterpolation.Request) async throws -> CoolingInterpolation.Response
public var furnaceInterpolation:
@Sendable (FurnaceInterpolation.Request) async throws -> FurnaceInterpolation.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
}
@@ -27,6 +34,7 @@ extension ManualS: DependencyKey {
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() },
requiredKW: { try await $0.respond() },
sizingLimits: { try await $0.respond() }

View File

@@ -0,0 +1,53 @@
public enum FurnaceInterpolation {
public struct Request: Codable, Equatable, Sendable {
public let elevation: Int?
public let winterDesignTemperature: Int
public let heatLoss: Int
public let inputRating: Int
public let afue: Double
public init(
elevation: Int? = nil,
winterDesignTemperature: Int,
heatLoss: Int,
inputRating: Int,
afue: Double
) {
self.elevation = elevation
self.winterDesignTemperature = winterDesignTemperature
self.heatLoss = heatLoss
self.inputRating = inputRating
self.afue = afue
}
}
public struct Response: Codable, Equatable, Sendable {
public let failed: Bool
public let failures: [String]?
public let altitudeAdjustmentMultiplier: Double?
public let capacityAsPercentOfLoad: Int
public let oversizingLimit: Int
public let undersizingLimit: Int
public let outputCapacity: Int
public init(
failures: [String]? = nil,
altitudeAdjustmentMultiplier: Double? = nil,
capacityAsPercentOfLoad: Int,
oversizingLimit: Int,
undersizingLimit: Int,
outputCapacity: Int
) {
self.failed = failures == nil ? false : !failures!.isEmpty
self.failures = failures
self.altitudeAdjustmentMultiplier = altitudeAdjustmentMultiplier
self.capacityAsPercentOfLoad = capacityAsPercentOfLoad
self.oversizingLimit = oversizingLimit
self.undersizingLimit = undersizingLimit
self.outputCapacity = outputCapacity
}
}
}

View File

@@ -159,6 +159,51 @@ struct ManualSTests {
}
}
@Test
func furnaceInterpolation() async throws {
try await withDependencies {
$0.manualS = .liveValue
} operation: {
@Dependency(\.manualS) var manualS
let response = try await manualS.furnaceInterpolation(
.init(
elevation: nil,
winterDesignTemperature: 5,
heatLoss: 49667,
inputRating: 60000,
afue: 96
)
)
#expect(response.failed == false)
#expect(response.capacityAsPercentOfLoad == 116)
#expect(response.outputCapacity == 57600)
}
}
@Test
func furnaceInterpolation_with_oversizedFurnace() async throws {
try await withDependencies {
$0.manualS = .liveValue
} operation: {
@Dependency(\.manualS) var manualS
let response = try await manualS.furnaceInterpolation(
.init(
elevation: nil,
winterDesignTemperature: 5,
heatLoss: 49667,
inputRating: 160_000,
afue: 96
)
)
#expect(response.failed)
#expect(response.failures == ["Oversizing failure."])
}
}
@Test
func balancePoint() async throws {
try await withDependencies {