feat: Initial interpolation calculations, requires tests.

This commit is contained in:
2025-03-12 21:52:42 -04:00
parent 5c684d0537
commit ec58ca6364
4 changed files with 411 additions and 77 deletions

View File

@@ -32,3 +32,14 @@ private struct OptionalContainerValidator: AsyncValidation {
}
}
extension Capacity.ManufacturersContainer: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.wetBulb, 0)
AsyncValidator.greaterThan(\.totalCapacity, 0)
AsyncValidator.greaterThan(\.sensibleCapacity, 0)
AsyncValidator.greaterThanOrEquals(\.totalCapacity, \.sensibleCapacity)
}
}
}

View File

@@ -4,89 +4,375 @@ import Validations
extension Interpolate.Request {
func respond() async throws -> Interpolate.Response {
try await validate()
fatalError()
let interpolatedCapacity = await interpolation.interpolatedCapacity(
outdoorDesignTemperature: designInfo.summer.outdoorTemperature
)
let excessLatent = self.excessLatent(interpolatedLatent: interpolatedCapacity.latent)
let elevationDeratings = try await Derating.Request(
elevation: designInfo.elevation,
systemType: systemType
).respond()
let sizingLimits = try await SizingLimits.Request(
systemType: systemType,
houseLoad: houseLoad
).respond()
let finalCapacity = interpolatedCapacity
.applying(excessLatent: excessLatent)
.applying(adjustmentMultipliers: interpolation.adjustmentMultipliers)
.applying(adjustmentMultipliers: elevationDeratings)
let capacityAsPercentOfLoad = Capacity.Cooling(
total: normalizePercentage(finalCapacity.total, houseLoad.coolingTotal),
sensible: normalizePercentage(finalCapacity.sensible, houseLoad.coolingSensible),
latent: normalizePercentage(finalCapacity.latent, houseLoad.coolingLatent)
)
return .init(
failures: sizingLimits.validate(capacityAsPercentOfLoad: capacityAsPercentOfLoad),
interpolatedCapacity: interpolatedCapacity,
excessLatent: excessLatent,
finalCapacityAtDesign: finalCapacity,
altitudeDerating: elevationDeratings,
capacityAsPercentOfLoad: capacityAsPercentOfLoad,
sizingLimits: sizingLimits
)
}
}
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 Capacity.Cooling {
func applying(excessLatent: Int) -> Self {
.init(total: total, sensible: sensible + excessLatent)
}
func applying(adjustmentMultipliers: AdjustmentMultiplier?) -> Self {
guard let adjustmentMultipliers else { return self }
return .init(
total: Int(Double(total) * (adjustmentMultipliers.coolingTotal ?? 1)),
sensible: Int(Double(sensible) * (adjustmentMultipliers.coolingSensible ?? 1))
)
}
}
private extension Interpolate.Request {
func excessLatent(interpolatedLatent: Int) -> Int {
(interpolatedLatent - houseLoad.coolingLatent) / 2
}
}
private extension Interpolate.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 = Double(below.totalCapacity)
+ (
Double(above.totalCapacity - below.totalCapacity)
/ Double(above.wetBulb - below.wetBulb)
)
* Double(63 - below.wetBulb)
let sensible = Double(below.sensibleCapacity)
+ Double(above.sensibleCapacity - below.sensibleCapacity)
/ Double(below.totalCapacity - above.totalCapacity)
* Double(below.totalCapacity)
- total
return .init(total: Int(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 Interpolate.OneWayIndoor {
func interpolatedCapacity() async -> Capacity.Cooling {
return await interpolateIndoorCapacity(
above: capacities.aboveDewpoint,
below: capacities.belowDewpoint
)
}
}
private extension Interpolate.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<Interpolate.OneWayOutdoor.Capacities.Capacity, Int>,
belowCapacity: KeyPath<Interpolate.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 Interpolate.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 = Interpolate.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 Interpolate.Request: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.validate(\.designInfo)
AsyncValidator.validate(\.houseLoad)
AsyncValidator.validate(\.manufacturersCapacity)
AsyncValidator.validate(\.interpolation)
}
}
}
private extension Interpolate.Request {
extension Interpolate.InterpolationRequest: AsyncValidatable {
func parseInterpolationType() throws -> Interpolate.InterpolationType {
guard let otherCapacity = manufacturersCapacity.otherCapacity else {
let capacity = manufacturersCapacity.capacity
guard capacity.wetBulbTemperature == 63 else {
throw ValidationError(
message: "Expected manufacturers wet bulb temperature to be 63, but found: \(capacity.wetBulbTemperature)"
)
public typealias Value = Interpolate.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()
}
return .noInterpolation
}
}
// check if the
fatalError()
extension Interpolate.OneWayIndoor: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.airflow, 0)
AsyncValidator.greaterThan(\.outdoorTemperature, 0)
AsyncValidator.validate(\.capacities)
}
private func checkOneWayIndoorInterpolation(
_ capacity: Capacity.ManufacturersCooling.Container,
_ other: Capacity.ManufacturersCooling.Container
) -> CapacityContainer? {
// ensure outdoor temperatures match, otherwise we are not a one way indoor interpolation.
guard capacity.outdoorTemperature == other.outdoorTemperature else {
return nil
}
}
// ensure indoor temperatures are not the same.
guard capacity.dryBulbTemperature != other.dryBulbTemperature else {
return nil
extension Interpolate.OneWayIndoor.Capacities: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.validate(\.aboveDewpoint)
AsyncValidator.validate(\.belowDewpoint)
}
if capacity.dryBulbTemperature < other.dryBulbTemperature {
return .init(above: other, below: capacity)
}
}
return .init(above: capacity, below: other)
extension Interpolate.OneWayOutdoor: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.airflow, 0)
AsyncValidator.greaterThan(\.wetBulb, 0)
AsyncValidator.validate(\.capacities)
}
private func checkOneWayOutdoorInterpolation(
_ capacity: Capacity.ManufacturersCooling.Container,
_ other: Capacity.ManufacturersCooling.Container
) -> CapacityContainer? {
// ensure outdoor temperatures match, otherwise we are not a one way indoor interpolation.
guard capacity.dryBulbTemperature == other.dryBulbTemperature else {
return nil
}
}
// ensure outdoor temperatures are not the same.
guard capacity.outdoorTemperature != other.outdoorTemperature else {
return nil
extension Interpolate.OneWayOutdoor.Capacities: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.validate(\.aboveOutdoor)
AsyncValidator.validate(\.belowOutdoor)
}
if capacity.outdoorTemperature < other.outdoorTemperature {
return .init(above: other, below: capacity)
}
}
return .init(above: capacity, below: other)
extension Interpolate.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)
}
}
}
private func checkTwoWayInterpolation(
_ capacity: Capacity.ManufacturersCooling.Container,
_ other: Capacity.ManufacturersCooling.Container
) -> CapacityContainer? {
// ensure outdoor temperatures do not match.
guard capacity.outdoorTemperature != other.outdoorTemperature else { return nil }
guard capacity.dryBulbTemperature != other.dryBulbTemperature else { return nil }
extension Interpolate.TwoWay: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.airflow, 0)
AsyncValidator.validate(\.capacities)
}
}
}
return nil
extension Interpolate.TwoWay.Capacities: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.validate(\.above)
AsyncValidator.validate(\.below)
}
}
}
extension Interpolate.TwoWay.Capacities.CapacityContainer: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.validate(\.aboveDewPoint)
AsyncValidator.validate(\.belowDewPoint)
}
}
}

View File

@@ -37,6 +37,7 @@ public enum Capacity {
}
}
// TODO: Remove.
public struct ManufacturersCooling: Codable, Equatable, Sendable {
public let airflow: Int
@@ -87,18 +88,15 @@ public enum Capacity {
public let wetBulb: Int
public let totalCapacity: Int
public let sensibleCapacity: Int
public let adjustmentMultipliers: AdjustmentMultiplier?
public init(
wetBulb: Int,
totalCapacity: Int,
sensibleCapacity: Int,
adjustmentMultipliers: AdjustmentMultiplier? = nil
sensibleCapacity: Int
) {
self.wetBulb = wetBulb
self.totalCapacity = totalCapacity
self.sensibleCapacity = sensibleCapacity
self.adjustmentMultipliers = adjustmentMultipliers
}
}
}

View File

@@ -5,18 +5,18 @@ public enum Interpolate {
public let designInfo: DesignInfo
public let houseLoad: HouseLoad
public let systemType: SystemType
public let manufacturersCapacity: Capacity.ManufacturersCooling
public let interpolation: InterpolationRequest
public init(
designInfo: DesignInfo,
houseLoad: HouseLoad,
systemType: SystemType,
manufacturersCapacity: Capacity.ManufacturersCooling
interpolation: InterpolationRequest
) {
self.designInfo = designInfo
self.houseLoad = houseLoad
self.systemType = systemType
self.manufacturersCapacity = manufacturersCapacity
self.interpolation = interpolation
}
}
@@ -24,7 +24,6 @@ public enum Interpolate {
public let failed: Bool
public let failures: [String]?
public let interpolationType: InterpolationType
public let interpolatedCapacity: Capacity.Cooling
public let excessLatent: Int
public let finalCapacityAtDesign: Capacity.Cooling
@@ -34,7 +33,6 @@ public enum Interpolate {
public init(
failures: [String]? = nil,
interpolationType: InterpolationType,
interpolatedCapacity: Capacity.Cooling,
excessLatent: Int,
finalCapacityAtDesign: Capacity.Cooling,
@@ -44,7 +42,6 @@ public enum Interpolate {
) {
self.failed = failures != nil ? failures!.count > 0 : false
self.failures = failures
self.interpolationType = interpolationType
self.interpolatedCapacity = interpolatedCapacity
self.excessLatent = excessLatent
self.finalCapacityAtDesign = finalCapacityAtDesign
@@ -54,10 +51,11 @@ public enum Interpolate {
}
}
public enum InterpolationType2: Codable, Equatable, Sendable {
case noInterpolation(Capacity.ManufacturersCooling)
public enum InterpolationRequest: Codable, Equatable, Sendable {
case noInterpolation(Capacity.ManufacturersCooling, AdjustmentMultiplier? = nil)
case oneWayIndoor(OneWayIndoor)
case oneWayOutdoor(OneWayOutdoor)
case twoWay(TwoWay)
}
public struct OneWayIndoor: Codable, Equatable, Sendable {
@@ -136,13 +134,54 @@ public enum Interpolate {
}
}
}
}
public enum InterpolationType: String, CaseIterable, Codable, Equatable, Sendable {
case noInterpolation
case oneWayIndoor
case oneWayOutdoor
case twoWay
public struct TwoWay: Codable, Equatable, Sendable {
public let airflow: Int
public let capacities: Capacities
public let adjustmentMultipliers: AdjustmentMultiplier?
public init(
airflow: Int,
capacities: Interpolate.TwoWay.Capacities,
adjustmentMultipliers: AdjustmentMultiplier? = nil
) {
self.airflow = airflow
self.capacities = capacities
self.adjustmentMultipliers = adjustmentMultipliers
}
public struct Capacities: Codable, Equatable, Sendable {
public let above: CapacityContainer
public let below: CapacityContainer
public init(
above: Interpolate.TwoWay.Capacities.CapacityContainer,
below: Interpolate.TwoWay.Capacities.CapacityContainer
) {
self.above = above
self.below = below
}
public struct CapacityContainer: Codable, Equatable, Sendable {
public let outdoorTemperature: Int
public let aboveDewPoint: Capacity.ManufacturersContainer
public let belowDewPoint: Capacity.ManufacturersContainer
public init(
outdoorTemperature: Int,
aboveDewPoint: Capacity.ManufacturersContainer,
belowDewPoint: Capacity.ManufacturersContainer
) {
self.outdoorTemperature = outdoorTemperature
self.aboveDewPoint = aboveDewPoint
self.belowDewPoint = belowDewPoint
}
}
}
}
}