diff --git a/Sources/ManualS/Extensions/ManufacturersCoolingCapacity+validator.swift b/Sources/ManualS/Extensions/Capacity+validator.swift similarity index 73% rename from Sources/ManualS/Extensions/ManufacturersCoolingCapacity+validator.swift rename to Sources/ManualS/Extensions/Capacity+validator.swift index 38c1d46..84945cb 100644 --- a/Sources/ManualS/Extensions/ManufacturersCoolingCapacity+validator.swift +++ b/Sources/ManualS/Extensions/Capacity+validator.swift @@ -32,3 +32,14 @@ private struct OptionalContainerValidator: AsyncValidation { } } + +extension Capacity.ManufacturersContainer: AsyncValidatable { + public var body: some AsyncValidation { + AsyncValidator.accumulating { + AsyncValidator.greaterThan(\.wetBulb, 0) + AsyncValidator.greaterThan(\.totalCapacity, 0) + AsyncValidator.greaterThan(\.sensibleCapacity, 0) + AsyncValidator.greaterThanOrEquals(\.totalCapacity, \.sensibleCapacity) + } + } +} diff --git a/Sources/ManualS/Internal/Interpolate.swift b/Sources/ManualS/Internal/Interpolate.swift index 43358fe..1be4fff 100644 --- a/Sources/ManualS/Internal/Interpolate.swift +++ b/Sources/ManualS/Internal/Interpolate.swift @@ -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, + belowCapacity: KeyPath, + ) 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 { 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)" - ) - } - return .noInterpolation + 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() } - - // check if the - fatalError() } +} - 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 +extension Interpolate.OneWayIndoor: AsyncValidatable { + public var body: some AsyncValidation { + AsyncValidator.accumulating { + AsyncValidator.greaterThan(\.airflow, 0) + AsyncValidator.greaterThan(\.outdoorTemperature, 0) + AsyncValidator.validate(\.capacities) } - - // ensure indoor temperatures are not the same. - guard capacity.dryBulbTemperature != other.dryBulbTemperature else { - return nil - } - - if capacity.dryBulbTemperature < other.dryBulbTemperature { - return .init(above: other, below: capacity) - } - - return .init(above: capacity, below: other) } +} - 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 +extension Interpolate.OneWayIndoor.Capacities: AsyncValidatable { + public var body: some AsyncValidation { + AsyncValidator.accumulating { + AsyncValidator.validate(\.aboveDewpoint) + AsyncValidator.validate(\.belowDewpoint) } - - // ensure outdoor temperatures are not the same. - guard capacity.outdoorTemperature != other.outdoorTemperature else { - return nil - } - - if capacity.outdoorTemperature < other.outdoorTemperature { - return .init(above: other, below: capacity) - } - - return .init(above: capacity, below: other) } +} - 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.OneWayOutdoor: AsyncValidatable { + public var body: some AsyncValidation { + AsyncValidator.accumulating { + AsyncValidator.greaterThan(\.airflow, 0) + AsyncValidator.greaterThan(\.wetBulb, 0) + AsyncValidator.validate(\.capacities) + } + } +} - return nil +extension Interpolate.OneWayOutdoor.Capacities: AsyncValidatable { + + public var body: some AsyncValidation { + AsyncValidator.accumulating { + AsyncValidator.validate(\.aboveOutdoor) + AsyncValidator.validate(\.belowOutdoor) + } + } +} + +extension Interpolate.OneWayOutdoor.Capacities.Capacity: AsyncValidatable { + public var body: some AsyncValidation { + AsyncValidator.accumulating { + AsyncValidator.greaterThan(\.outdoorTemperature, 0) + AsyncValidator.greaterThan(\.totalCapacity, 0) + AsyncValidator.greaterThan(\.sensibleCapacity, 0) + AsyncValidator.greaterThanOrEquals(\.totalCapacity, \.sensibleCapacity) + } + } +} + +extension Interpolate.TwoWay: AsyncValidatable { + public var body: some AsyncValidation { + AsyncValidator.accumulating { + AsyncValidator.greaterThan(\.airflow, 0) + AsyncValidator.validate(\.capacities) + } + } +} + +extension Interpolate.TwoWay.Capacities: AsyncValidatable { + public var body: some AsyncValidation { + AsyncValidator.accumulating { + AsyncValidator.validate(\.above) + AsyncValidator.validate(\.below) + } + } +} + +extension Interpolate.TwoWay.Capacities.CapacityContainer: AsyncValidatable { + public var body: some AsyncValidation { + AsyncValidator.accumulating { + AsyncValidator.validate(\.aboveDewPoint) + AsyncValidator.validate(\.belowDewPoint) + } } } diff --git a/Sources/Models/Core/Capacity.swift b/Sources/Models/Core/Capacity.swift index 412371e..c01a76a 100644 --- a/Sources/Models/Core/Capacity.swift +++ b/Sources/Models/Core/Capacity.swift @@ -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 } } } diff --git a/Sources/Models/Interpolate.swift b/Sources/Models/Interpolate.swift index 5b2b020..6ffb6b6 100644 --- a/Sources/Models/Interpolate.swift +++ b/Sources/Models/Interpolate.swift @@ -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 + } + } + } } + }