feat: Adds furnace heating interpolations.
All checks were successful
CI / Ubuntu (push) Successful in 11m23s
All checks were successful
CI / Ubuntu (push) Successful in 11m23s
This commit is contained in:
9
Sources/ManualS/Extensions/NormalizePercentage.swift
Normal file
9
Sources/ManualS/Extensions/NormalizePercentage.swift
Normal 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)
|
||||||
|
}
|
||||||
@@ -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 {
|
private extension CoolingInterpolation.InterpolationRequest {
|
||||||
|
|
||||||
func interpolatedCapacity(outdoorDesignTemperature: Int) async -> Capacity.Cooling {
|
func interpolatedCapacity(outdoorDesignTemperature: Int) async -> Capacity.Cooling {
|
||||||
|
|||||||
55
Sources/ManualS/Internal/FurnaceInterpolation.swift
Normal file
55
Sources/ManualS/Internal/FurnaceInterpolation.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,13 +12,20 @@ public extension DependencyValues {
|
|||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct ManualS: Sendable {
|
public struct ManualS: Sendable {
|
||||||
public var balancePoint: @Sendable (BalancePoint.Request) async throws -> BalancePoint.Response
|
public var balancePoint: @Sendable (BalancePoint.Request) async throws -> BalancePoint.Response
|
||||||
|
|
||||||
public var derating: @Sendable (Derating.Request) async throws -> Derating.Response
|
public var derating: @Sendable (Derating.Request) async throws -> Derating.Response
|
||||||
|
|
||||||
public var coolingInterpolation:
|
public var coolingInterpolation:
|
||||||
@Sendable (CoolingInterpolation.Request) async throws -> CoolingInterpolation.Response
|
@Sendable (CoolingInterpolation.Request) async throws -> CoolingInterpolation.Response
|
||||||
|
|
||||||
|
public var furnaceInterpolation:
|
||||||
|
@Sendable (FurnaceInterpolation.Request) async throws -> FurnaceInterpolation.Response
|
||||||
|
|
||||||
public var heatPumpHeatingInterpolation:
|
public var heatPumpHeatingInterpolation:
|
||||||
@Sendable (HeatPumpHeatingInterpolation.Request) async throws -> HeatPumpHeatingInterpolation.Response
|
@Sendable (HeatPumpHeatingInterpolation.Request) async throws -> HeatPumpHeatingInterpolation.Response
|
||||||
|
|
||||||
public var requiredKW: @Sendable (RequiredKW.Request) async throws -> RequiredKW.Response
|
public var requiredKW: @Sendable (RequiredKW.Request) async throws -> RequiredKW.Response
|
||||||
|
|
||||||
public var sizingLimits: @Sendable (SizingLimits.Request) async throws -> SizingLimits.Response
|
public var sizingLimits: @Sendable (SizingLimits.Request) async throws -> SizingLimits.Response
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +34,7 @@ extension ManualS: DependencyKey {
|
|||||||
balancePoint: { try await $0.respond() },
|
balancePoint: { try await $0.respond() },
|
||||||
derating: { try await $0.respond() },
|
derating: { try await $0.respond() },
|
||||||
coolingInterpolation: { try await $0.respond() },
|
coolingInterpolation: { try await $0.respond() },
|
||||||
|
furnaceInterpolation: { try await $0.respond() },
|
||||||
heatPumpHeatingInterpolation: { try await $0.respond() },
|
heatPumpHeatingInterpolation: { try await $0.respond() },
|
||||||
requiredKW: { try await $0.respond() },
|
requiredKW: { try await $0.respond() },
|
||||||
sizingLimits: { try await $0.respond() }
|
sizingLimits: { try await $0.respond() }
|
||||||
|
|||||||
53
Sources/Models/FurnaceInterpolation.swift
Normal file
53
Sources/Models/FurnaceInterpolation.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
@Test
|
||||||
func balancePoint() async throws {
|
func balancePoint() async throws {
|
||||||
try await withDependencies {
|
try await withDependencies {
|
||||||
|
|||||||
Reference in New Issue
Block a user