feat: Initial commit

This commit is contained in:
2025-03-12 16:59:10 -04:00
commit 5c684d0537
28 changed files with 1285 additions and 0 deletions

View 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)))
}

View 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

View 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
}
}

View 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()
}
}
}
}

View 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() {}
}