feat: Adds cooling interpolation tests.
This commit is contained in:
409
Sources/ManualS/Internal/CoolingInterpolation.swift
Normal file
409
Sources/ManualS/Internal/CoolingInterpolation.swift
Normal file
@@ -0,0 +1,409 @@
|
||||
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.Capacity, Int>,
|
||||
belowCapacity: KeyPath<CoolingInterpolation.OneWayOutdoor.Capacities.Capacity, 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.Capacity: 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user