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,2 @@
@_exported import Dependencies
@_exported import Models

View File

@@ -0,0 +1,19 @@
import Models
import Validations
extension DesignInfo: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.validate(\.summer)
}
}
extension DesignInfo.Summer: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.outdoorTemperature, 0)
AsyncValidator.greaterThan(\.indoorTemperature, 0)
AsyncValidator.greaterThan(\.indoorHumidity, 0)
}
}
}

View File

@@ -0,0 +1,13 @@
import Models
import Validations
extension Capacity.HeatPumpHeating: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.at47, 0)
AsyncValidator.greaterThan(\.at17, 0)
AsyncValidator.greaterThanOrEquals(\.at47, \.at17)
}
}
}

View File

@@ -0,0 +1,14 @@
import Models
import Validations
extension HouseLoad: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.coolingTotal, 0)
AsyncValidator.greaterThan(\.coolingSensible, 0)
AsyncValidator.greaterThan(\.heating, 0)
AsyncValidator.greaterThanOrEquals(\.coolingTotal, \.coolingSensible)
}
}
}

View File

@@ -0,0 +1,34 @@
import Models
import Validations
extension Capacity.ManufacturersCooling: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.airflow, 0)
AsyncValidator.validate(\.capacity)
AsyncValidator.validate(\.otherCapacity, with: OptionalContainerValidator())
}
}
}
extension Capacity.ManufacturersCooling.Container: AsyncValidatable {
public var body: some AsyncValidation<Self> {
AsyncValidator.accumulating {
AsyncValidator.greaterThan(\.dryBulbTemperature, 0)
AsyncValidator.greaterThan(\.wetBulbTemperature, 0)
AsyncValidator.greaterThan(\.totalCapacity, 0)
AsyncValidator.greaterThan(\.sensibleCapacity, 0)
AsyncValidator.greaterThanOrEquals(\.totalCapacity, \.sensibleCapacity)
}
}
}
private struct OptionalContainerValidator: AsyncValidation {
typealias Value = Capacity.ManufacturersCooling.Container?
func validate(_ value: Capacity.ManufacturersCooling.Container?) async throws {
guard let value else { return }
try await value.validate()
}
}

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

View File

@@ -0,0 +1,33 @@
import Dependencies
import DependenciesMacros
import Models
public extension DependencyValues {
var manualS: ManualS {
get { self[ManualS.self] }
set { self[ManualS.self] = newValue }
}
}
@DependencyClient
public struct ManualS: Sendable {
public var balancePoint: @Sendable (BalancePoint.Request) async throws -> BalancePoint.Response
public var derating: @Sendable (Derating.Request) async throws -> Derating.Response
public var interpolate: @Sendable (Interpolate.Request) async throws -> Interpolate.Response
public var requiredKW: @Sendable (RequiredKW.Request) async throws -> RequiredKW.Response
public var sizingLimits: @Sendable (SizingLimits.Request) async throws -> SizingLimits.Response
}
extension ManualS: DependencyKey {
public static let liveValue = Self(
balancePoint: { try await $0.respond() },
derating: { try await $0.respond() },
interpolate: { try await $0.respond() },
requiredKW: { try await $0.respond() },
sizingLimits: { try await $0.respond() }
)
}
extension ManualS: TestDependencyKey {
public static let testValue: ManualS = Self()
}