feat: Initial commit
This commit is contained in:
39
Sources/ManualS/Internal/BalancePoint.swift
Normal file
39
Sources/ManualS/Internal/BalancePoint.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
import CoreFoundation
|
||||
import Models
|
||||
import Validations
|
||||
|
||||
extension BalancePoint.Request {
|
||||
|
||||
func respond() async throws -> BalancePoint.Response {
|
||||
try await validate()
|
||||
let balancePoint = await thermalBalancePoint(
|
||||
heatLoss: Double(heatLoss),
|
||||
at47: Double(heatPumpCapacity.at47),
|
||||
at17: Double(heatPumpCapacity.at17),
|
||||
designTemperature: Double(winterDesignTemperature)
|
||||
)
|
||||
return .init(balancePointTemperature: balancePoint)
|
||||
}
|
||||
}
|
||||
|
||||
extension BalancePoint.Request: AsyncValidatable {
|
||||
|
||||
@inlinable
|
||||
public var body: some AsyncValidation<Self> {
|
||||
AsyncValidator.accumulating {
|
||||
AsyncValidator.greaterThan(\.heatLoss, 0)
|
||||
AsyncValidator.validate(\.heatPumpCapacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func thermalBalancePoint(
|
||||
heatLoss: Double,
|
||||
at47: Double,
|
||||
at17: Double,
|
||||
designTemperature: Double
|
||||
) async -> Double {
|
||||
(30.0 * (((designTemperature - 65.0) * at47) + (65.0 * heatLoss))
|
||||
- ((designTemperature - 65.0) * (at47 - at17) * 47.0))
|
||||
/ ((30.0 * heatLoss) - ((designTemperature - 65.0) * (at47 - at17)))
|
||||
}
|
||||
101
Sources/ManualS/Internal/Derating.swift
Normal file
101
Sources/ManualS/Internal/Derating.swift
Normal file
@@ -0,0 +1,101 @@
|
||||
import Models
|
||||
|
||||
extension Derating.Request {
|
||||
func respond() async throws -> Derating.Response {
|
||||
switch systemType {
|
||||
case .airToAir:
|
||||
return .init(
|
||||
coolingTotal: totalWetDerating(elevation: elevation),
|
||||
coolingSensible: sensibleWetDerating(elevation: elevation),
|
||||
heating: totalDryDerating(elevation: elevation)
|
||||
)
|
||||
|
||||
case let .heatingOnly(type: type):
|
||||
switch type {
|
||||
case .boiler, .furnace:
|
||||
return .init(heating: furnaceDerating(elevation: elevation))
|
||||
case .electric:
|
||||
return .init(heating: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable cyclomatic_complexity
|
||||
private func furnaceDerating(elevation: Int) -> Double {
|
||||
guard elevation > 0 else { return 1 }
|
||||
|
||||
if (0 ..< 1000).contains(elevation) { return 1 }
|
||||
if (1000 ..< 2000).contains(elevation) { return 0.96 }
|
||||
if (2000 ..< 3000).contains(elevation) { return 0.92 }
|
||||
if (3000 ..< 4000).contains(elevation) { return 0.88 }
|
||||
if (4000 ..< 5000).contains(elevation) { return 0.84 }
|
||||
if (5000 ..< 6000).contains(elevation) { return 0.8 }
|
||||
if (6000 ..< 7000).contains(elevation) { return 0.76 }
|
||||
if (7000 ..< 8000).contains(elevation) { return 0.72 }
|
||||
if (8000 ..< 9000).contains(elevation) { return 0.68 }
|
||||
if (9000 ..< 10000).contains(elevation) { return 0.64 }
|
||||
if (10000 ..< 11000).contains(elevation) { return 0.6 }
|
||||
if (11000 ..< 12000).contains(elevation) { return 0.56 }
|
||||
// greater than 12,000 feet in elevation.
|
||||
return 0.52
|
||||
}
|
||||
|
||||
private func totalWetDerating(elevation: Int) -> Double {
|
||||
guard elevation > 0 else { return 1 }
|
||||
|
||||
if (0 ..< 1000).contains(elevation) { return 1 }
|
||||
if (1000 ..< 2000).contains(elevation) { return 0.99 }
|
||||
if (2000 ..< 3000).contains(elevation) { return 0.98 }
|
||||
if (3000 ..< 4000).contains(elevation) { return 0.98 }
|
||||
if (4000 ..< 5000).contains(elevation) { return 0.97 }
|
||||
if (5000 ..< 6000).contains(elevation) { return 0.96 }
|
||||
if (6000 ..< 7000).contains(elevation) { return 0.95 }
|
||||
if (7000 ..< 8000).contains(elevation) { return 0.94 }
|
||||
if (8000 ..< 9000).contains(elevation) { return 0.94 }
|
||||
if (9000 ..< 10000).contains(elevation) { return 0.93 }
|
||||
if (10000 ..< 11000).contains(elevation) { return 0.92 }
|
||||
if (11000 ..< 12000).contains(elevation) { return 0.91 }
|
||||
// greater than 12,000 feet in elevation.
|
||||
return 0.9
|
||||
}
|
||||
|
||||
private func sensibleWetDerating(elevation: Int) -> Double {
|
||||
guard elevation > 0 else { return 1 }
|
||||
|
||||
if (0 ..< 1000).contains(elevation) { return 1 }
|
||||
if (1000 ..< 2000).contains(elevation) { return 0.97 }
|
||||
if (2000 ..< 3000).contains(elevation) { return 0.94 }
|
||||
if (3000 ..< 4000).contains(elevation) { return 0.91 }
|
||||
if (4000 ..< 5000).contains(elevation) { return 0.88 }
|
||||
if (5000 ..< 6000).contains(elevation) { return 0.85 }
|
||||
if (6000 ..< 7000).contains(elevation) { return 0.82 }
|
||||
if (7000 ..< 8000).contains(elevation) { return 0.8 }
|
||||
if (8000 ..< 9000).contains(elevation) { return 0.77 }
|
||||
if (9000 ..< 10000).contains(elevation) { return 0.74 }
|
||||
if (10000 ..< 11000).contains(elevation) { return 0.71 }
|
||||
if (11000 ..< 12000).contains(elevation) { return 0.68 }
|
||||
// greater than 12,000 feet in elevation.
|
||||
return 0.65
|
||||
}
|
||||
|
||||
private func totalDryDerating(elevation: Int) -> Double {
|
||||
guard elevation > 0 else { return 1 }
|
||||
|
||||
if (0 ..< 1000).contains(elevation) { return 1 }
|
||||
if (1000 ..< 2000).contains(elevation) { return 0.98 }
|
||||
if (2000 ..< 3000).contains(elevation) { return 0.97 }
|
||||
if (3000 ..< 4000).contains(elevation) { return 0.95 }
|
||||
if (4000 ..< 5000).contains(elevation) { return 0.94 }
|
||||
if (5000 ..< 6000).contains(elevation) { return 0.92 }
|
||||
if (6000 ..< 7000).contains(elevation) { return 0.9 }
|
||||
if (7000 ..< 8000).contains(elevation) { return 0.89 }
|
||||
if (8000 ..< 9000).contains(elevation) { return 0.87 }
|
||||
if (9000 ..< 10000).contains(elevation) { return 0.86 }
|
||||
if (10000 ..< 11000).contains(elevation) { return 0.84 }
|
||||
if (11000 ..< 12000).contains(elevation) { return 0.82 }
|
||||
// greater than 12,000 feet in elevation.
|
||||
return 0.81
|
||||
}
|
||||
|
||||
// swiftlint:enable cyclomatic_complexity
|
||||
104
Sources/ManualS/Internal/Interpolate.swift
Normal file
104
Sources/ManualS/Internal/Interpolate.swift
Normal file
@@ -0,0 +1,104 @@
|
||||
import Models
|
||||
import Validations
|
||||
|
||||
extension Interpolate.Request {
|
||||
func respond() async throws -> Interpolate.Response {
|
||||
try await validate()
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Interpolate.Request {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 }
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
24
Sources/ManualS/Internal/RequiredKW.swift
Normal file
24
Sources/ManualS/Internal/RequiredKW.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
import Models
|
||||
import Validations
|
||||
|
||||
extension RequiredKW.Request {
|
||||
func respond() async throws -> RequiredKW.Response {
|
||||
try await validate()
|
||||
let capacityAtDesign = self.capacityAtDesign ?? 0
|
||||
let requiredKW = (Double(heatLoss) - Double(capacityAtDesign)) / 3413
|
||||
return .init(requiredKW: requiredKW)
|
||||
}
|
||||
}
|
||||
|
||||
extension RequiredKW.Request: AsyncValidatable {
|
||||
|
||||
@inlinable
|
||||
public var body: some AsyncValidation<Self> {
|
||||
AsyncValidator.accumulating {
|
||||
AsyncValidator.greaterThan(\.heatLoss, 0)
|
||||
AsyncValidator.validate(\.capacityAtDesign) {
|
||||
AsyncValidator.greaterThan(0).optional()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
Sources/ManualS/Internal/SizingLimits.swift
Normal file
79
Sources/ManualS/Internal/SizingLimits.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
import CoreFoundation
|
||||
import Models
|
||||
|
||||
extension SizingLimits.Request {
|
||||
func respond() async throws -> SizingLimits.Response {
|
||||
return try .init(
|
||||
oversizing: .oversizingLimit(systemType: systemType, houseLoad: houseLoad),
|
||||
undersizing: .undersizingLimits
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private extension SizingLimits.Limits {
|
||||
static let undersizingLimits = Self(
|
||||
heating: 90,
|
||||
coolingTotal: 90,
|
||||
coolingSensible: 90,
|
||||
coolingLatent: 90
|
||||
)
|
||||
|
||||
static func oversizingLimit(
|
||||
systemType: SystemType,
|
||||
houseLoad: HouseLoad?
|
||||
) throws -> Self {
|
||||
switch systemType {
|
||||
case let .heatingOnly(type: type):
|
||||
return .init(heating: type.oversizingLimit(), coolingTotal: 115)
|
||||
case let .airToAir(type: _, compressor: compressor, climate: climate):
|
||||
return try .init(
|
||||
heating: 140,
|
||||
coolingTotal: coolingTotalOversizingLimit(
|
||||
houseLoad: houseLoad,
|
||||
compressorType: compressor,
|
||||
climateType: climate
|
||||
),
|
||||
coolingSensible: nil,
|
||||
coolingLatent: 150
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension SystemType.HeatingOnlyType {
|
||||
|
||||
func oversizingLimit() -> Int {
|
||||
switch self {
|
||||
case .boiler, .furnace: return 140
|
||||
case .electric: return 175
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func coolingTotalOversizingLimit(
|
||||
houseLoad: HouseLoad?,
|
||||
compressorType: SystemType.CompressorType,
|
||||
climateType: SystemType.ClimateType
|
||||
) throws -> Int {
|
||||
switch (compressorType, climateType) {
|
||||
case (.singleSpeed, .mildWinterOrLatentLoad):
|
||||
return 115
|
||||
case (.multiSpeed, .mildWinterOrLatentLoad):
|
||||
return 120
|
||||
case (.variableSpeed, .mildWinterOrLatentLoad):
|
||||
return 130
|
||||
default:
|
||||
|
||||
guard let houseLoad else {
|
||||
throw HouseLoadError()
|
||||
}
|
||||
let decimal = Double(houseLoad.coolingTotal + 15000) / Double(houseLoad.coolingTotal)
|
||||
return Int(round(decimal * 100))
|
||||
}
|
||||
}
|
||||
|
||||
public struct HouseLoadError: Error, Equatable {
|
||||
public let message = "House load not supplied."
|
||||
|
||||
public init() {}
|
||||
}
|
||||
Reference in New Issue
Block a user