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, 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 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 { 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 { AsyncValidator.accumulating { AsyncValidator.greaterThan(\.airflow, 0) AsyncValidator.greaterThan(\.outdoorTemperature, 0) AsyncValidator.validate(\.capacities) } } } extension CoolingInterpolation.OneWayIndoor.Capacities: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { AsyncValidator.validate(\.aboveDewpoint) AsyncValidator.validate(\.belowDewpoint) } } } extension CoolingInterpolation.OneWayOutdoor: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { AsyncValidator.greaterThan(\.airflow, 0) AsyncValidator.greaterThan(\.wetBulb, 0) AsyncValidator.validate(\.capacities) } } } extension CoolingInterpolation.OneWayOutdoor.Capacities: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { AsyncValidator.validate(\.aboveOutdoor) AsyncValidator.validate(\.belowOutdoor) } } } extension CoolingInterpolation.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 CoolingInterpolation.TwoWay: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { AsyncValidator.greaterThan(\.airflow, 0) AsyncValidator.validate(\.capacities) } } } extension CoolingInterpolation.TwoWay.Capacities: AsyncValidatable { public var body: some AsyncValidation { AsyncValidator.accumulating { AsyncValidator.validate(\.above) AsyncValidator.validate(\.below) } } } extension CoolingInterpolation.TwoWay.Capacities.CapacityContainer: AsyncValidatable { public var body: some AsyncValidation { 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 } }