Files
swift-manual-s/Sources/ManualS/Internal/CoolingInterpolation.swift
Michael Housh bd33827e53
All checks were successful
CI / Ubuntu (push) Successful in 11m46s
feat: Adds heat pump heating interpolation.
2025-03-13 10:55:15 -04:00

410 lines
12 KiB
Swift

import Models
import Validations
extension CoolingInterpolation.Request {
func respond() async throws -> CoolingInterpolation.Response {
try await validate()
let interpolatedCapacity = await interpolatedCapacity()
let excessLatent = excessLatent(interpolatedCapacity.latent)
let elevationDeratings = try await elevationDeratings()
let sizingLimits = try await sizingLimits()
let finalCapacity = calculateFinalCapacity(
interpolated: interpolatedCapacity,
excessLatent: excessLatent,
adjustmentMultipliers: interpolation.adjustmentMultipliers,
elevationDeratings: elevationDeratings
)
let capacityAsPercentOfLoad = Capacity.Cooling(
total: normalizePercentage(finalCapacity.total, coolingLoad.total),
sensible: normalizePercentage(finalCapacity.sensible, coolingLoad.sensible),
latent: normalizePercentage(finalCapacity.latent, coolingLoad.latent)
)
return .init(
failures: sizingLimits.validate(capacityAsPercentOfLoad: capacityAsPercentOfLoad),
interpolatedCapacity: interpolatedCapacity,
excessLatent: excessLatent,
finalCapacityAtDesign: finalCapacity,
altitudeDerating: elevationDeratings,
capacityAsPercentOfLoad: capacityAsPercentOfLoad,
sizingLimits: sizingLimits
)
}
private func interpolatedCapacity() async -> Capacity.Cooling {
await interpolation.interpolatedCapacity(
outdoorDesignTemperature: summerDesignInfo.outdoorTemperature
)
}
private func elevationDeratings() async throws -> AdjustmentMultiplier? {
guard let elevation else { return nil }
return try await Derating.Request(elevation: elevation, systemType: systemType)
.respond()
}
private func sizingLimits() async throws -> SizingLimits.Response {
try await SizingLimits.Request(systemType: systemType, coolingLoad: coolingLoad)
.respond()
}
private func excessLatent(_ interpolatedLatent: Int) -> Int {
(interpolatedLatent - coolingLoad.latent) / 2
}
}
private func calculateFinalCapacity(
interpolated: Capacity.Cooling,
excessLatent: Int,
adjustmentMultipliers: AdjustmentMultiplier?,
elevationDeratings: AdjustmentMultiplier?
) -> Capacity.Cooling {
var total = interpolated.total
var sensible = interpolated.sensible + excessLatent
if let adjustmentMultipliers {
adjustmentMultipliers.apply(total: &total, sensible: &sensible)
}
if let elevationDeratings {
elevationDeratings.apply(total: &total, sensible: &sensible)
}
return .init(total: total, sensible: sensible)
}
private extension AdjustmentMultiplier {
func apply(total: inout Int, sensible: inout Int) {
if let coolingTotal {
total = Int(Double(total) * coolingTotal)
}
if let coolingSensible {
sensible = Int(Double(sensible) * coolingSensible)
}
}
}
private extension SizingLimits.Response {
func validate(capacityAsPercentOfLoad: Capacity.Cooling) -> [String]? {
var failures = [String]()
// Check oversizing limits.
if capacityAsPercentOfLoad.total > oversizing.coolingTotal {
failures.append(
"Oversizing total failure."
)
}
if let coolingLatent = oversizing.coolingLatent,
capacityAsPercentOfLoad.latent > coolingLatent
{
failures.append(
"Oversizing latent failure."
)
}
if capacityAsPercentOfLoad.total < undersizing.coolingTotal {
failures.append(
"Undersizing total failure."
)
}
if let coolingSensible = undersizing.coolingSensible,
capacityAsPercentOfLoad.sensible < coolingSensible
{
failures.append(
"Undersizing sensible failure."
)
}
if let coolingLatent = undersizing.coolingLatent,
capacityAsPercentOfLoad.latent < coolingLatent
{
failures.append(
"Undersizing latent failure."
)
}
return failures.isEmpty ? nil : failures
}
}
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 {
switch self {
case let .noInterpolation(request, _):
return .init(total: request.capacity.totalCapacity, sensible: request.capacity.sensibleCapacity)
case let .oneWayIndoor(request):
return await request.interpolatedCapacity()
case let .oneWayOutdoor(request):
return await request.interpolatedCapacity(outdoorDesignTemperature: outdoorDesignTemperature)
case let .twoWay(request):
return await request.interpolatedCapacity(outdoorDesignTemperature: outdoorDesignTemperature)
}
}
var adjustmentMultipliers: AdjustmentMultiplier? {
switch self {
case let .noInterpolation(_, adjustmentMultipliers):
return adjustmentMultipliers
case let .oneWayIndoor(request):
return request.adjustmentMultipliers
case let .oneWayOutdoor(request):
return request.adjustmentMultipliers
case let .twoWay(request):
return request.adjustmentMultipliers
}
}
}
private func interpolateIndoorCapacity(
above: Capacity.ManufacturersContainer,
below: Capacity.ManufacturersContainer
) async -> Capacity.Cooling {
let total = below.totalCapacity
+ ((above.totalCapacity - below.totalCapacity) / (above.wetBulb - below.wetBulb))
* (63 - below.wetBulb)
let sensible = Double(below.sensibleCapacity)
+ (Double(above.sensibleCapacity) - Double(below.sensibleCapacity))
/ (Double(below.totalCapacity) - Double(above.totalCapacity))
* (Double(below.totalCapacity) - Double(total))
return .init(total: total, sensible: Int(sensible))
}
private func interpolateOutdoorCapacity(
outdoorDesignTemperature: Int,
aboveCapacity: Int,
aboveOutdoorTemperature: Int,
belowCapacity: Int,
belowOutdoorTemperature: Int
) async -> Int {
return belowCapacity
- (outdoorDesignTemperature - belowOutdoorTemperature)
* ((belowCapacity - aboveCapacity) / (aboveOutdoorTemperature - belowOutdoorTemperature))
}
private extension CoolingInterpolation.OneWayIndoor {
func interpolatedCapacity() async -> Capacity.Cooling {
return await interpolateIndoorCapacity(
above: capacities.aboveDewpoint,
below: capacities.belowDewpoint
)
}
}
private extension CoolingInterpolation.OneWayOutdoor {
func interpolatedCapacity(outdoorDesignTemperature: Int) async -> Capacity.Cooling {
let total = await interpolate(
outdoorDesignTemperature: outdoorDesignTemperature,
aboveCapacity: \.totalCapacity,
belowCapacity: \.totalCapacity
)
let sensible = await interpolate(
outdoorDesignTemperature: outdoorDesignTemperature,
aboveCapacity: \.sensibleCapacity,
belowCapacity: \.sensibleCapacity
)
return .init(total: total, sensible: sensible)
}
private func interpolate(
outdoorDesignTemperature: Int,
aboveCapacity: KeyPath<CoolingInterpolation.OneWayOutdoor.Capacities.CapacityContainer, Int>,
belowCapacity: KeyPath<CoolingInterpolation.OneWayOutdoor.Capacities.CapacityContainer, Int>
) async -> Int {
return await interpolateOutdoorCapacity(
outdoorDesignTemperature: outdoorDesignTemperature,
aboveCapacity: capacities.aboveOutdoor[keyPath: aboveCapacity],
aboveOutdoorTemperature: capacities.aboveOutdoor.outdoorTemperature,
belowCapacity: capacities.belowOutdoor[keyPath: belowCapacity],
belowOutdoorTemperature: capacities.belowOutdoor.outdoorTemperature
)
}
}
private extension CoolingInterpolation.TwoWay {
func interpolatedCapacity(outdoorDesignTemperature: Int) async -> Capacity.Cooling {
let aboveIndoorInterpolation = await self.aboveIndoorInterpolation()
let belowIndoorInterpolation = await self.belowIndoorInterpolation()
return await interpolate(
outdoorDesignTemperature: outdoorDesignTemperature,
aboveIndoor: aboveIndoorInterpolation,
belowIndoor: belowIndoorInterpolation
)
}
func interpolate(
outdoorDesignTemperature: Int,
aboveIndoor: Capacity.Cooling,
belowIndoor: Capacity.Cooling
) async -> Capacity.Cooling {
let request = CoolingInterpolation.OneWayOutdoor(
airflow: 0,
wetBulb: 63,
capacities: .init(
aboveOutdoor: .init(
outdoorTemperature: capacities.above.outdoorTemperature,
totalCapacity: aboveIndoor.total,
sensibleCapacity: aboveIndoor.sensible
),
belowOutdoor: .init(
outdoorTemperature: capacities.below.outdoorTemperature,
totalCapacity: belowIndoor.total,
sensibleCapacity: belowIndoor.sensible
)
)
)
return await request.interpolatedCapacity(outdoorDesignTemperature: outdoorDesignTemperature)
}
func aboveIndoorInterpolation() async -> Capacity.Cooling {
return await interpolateIndoorCapacity(
above: capacities.above.aboveDewPoint,
below: capacities.above.belowDewPoint
)
}
func belowIndoorInterpolation() async -> Capacity.Cooling {
return await interpolateIndoorCapacity(
above: capacities.below.aboveDewPoint,
below: capacities.below.belowDewPoint
)
}
}
// MARK: - Validations
// Basic validations of the request.
extension CoolingInterpolation.Request: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.validate(\.summerDesignInfo)
AsyncValidator.validate(\.coolingLoad)
AsyncValidator.validate(\.interpolation)
}
}
}
extension CoolingInterpolation.InterpolationRequest: AsyncValidatable {
public typealias Value = CoolingInterpolation.InterpolationRequest
public func validate(_ value: Self) async throws {
switch value {
case let .noInterpolation(request, _):
try await request.validate()
case let .oneWayIndoor(request):
try await request.validate()
case let .oneWayOutdoor(request):
try await request.validate()
case let .twoWay(request):
try await request.validate()
}
}
}
extension CoolingInterpolation.OneWayIndoor: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.airflow, 0)
AsyncValidator.greaterThan(\.outdoorTemperature, 0)
AsyncValidator.validate(\.capacities)
}
}
}
extension CoolingInterpolation.OneWayIndoor.Capacities: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.validate(\.aboveDewpoint)
AsyncValidator.validate(\.belowDewpoint)
}
}
}
extension CoolingInterpolation.OneWayOutdoor: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.airflow, 0)
AsyncValidator.greaterThan(\.wetBulb, 0)
AsyncValidator.validate(\.capacities)
}
}
}
extension CoolingInterpolation.OneWayOutdoor.Capacities: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.validate(\.aboveOutdoor)
AsyncValidator.validate(\.belowOutdoor)
}
}
}
extension CoolingInterpolation.OneWayOutdoor.Capacities.CapacityContainer: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.outdoorTemperature, 0)
AsyncValidator.greaterThan(\.totalCapacity, 0)
AsyncValidator.greaterThan(\.sensibleCapacity, 0)
AsyncValidator.greaterThanOrEquals(\.totalCapacity, \.sensibleCapacity)
}
}
}
extension CoolingInterpolation.TwoWay: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.airflow, 0)
AsyncValidator.validate(\.capacities)
}
}
}
extension CoolingInterpolation.TwoWay.Capacities: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.validate(\.above)
AsyncValidator.validate(\.below)
}
}
}
extension CoolingInterpolation.TwoWay.Capacities.CapacityContainer: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.validate(\.aboveDewPoint)
AsyncValidator.validate(\.belowDewPoint)
}
}
}
private struct CapacityContainer {
let above: Capacity.ManufacturersCooling.Container
let below: Capacity.ManufacturersCooling.Container
}
public struct ValidationError: Equatable, Error {
public let message: String
public init(message: String) {
self.message = message
}
}