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

7
.editorconfig Normal file
View File

@@ -0,0 +1,7 @@
root = true
[*.swift]
indent_style = space
indent_size = 2
tab_width = 2
trim_trailing_whitespace = true

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

11
.swiftformat Normal file
View File

@@ -0,0 +1,11 @@
--self init-only
--indent 2
--ifdef indent
--trimwhitespace always
--wraparguments before-first
--wrapparameters before-first
--wrapcollections preserve
--wrapconditions after-first
--typeblanklines preserve
--commas inline
--stripunusedargs closure-only

11
.swiftlint.yml Normal file
View File

@@ -0,0 +1,11 @@
disabled_rules:
- closing_brace
- fuction_body_length
- opening_brace
- nesting
included:
- Sources
- Tests
ignore_multiline_statement_conditions: true

78
Package.resolved Normal file
View File

@@ -0,0 +1,78 @@
{
"originHash" : "c1e2b47f4dcb7c57922f698b2ddfff2e29dfa400eb006d972bfea53df688f5d9",
"pins" : [
{
"identity" : "combine-schedulers",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/combine-schedulers",
"state" : {
"revision" : "5928286acce13def418ec36d05a001a9641086f2",
"version" : "1.0.3"
}
},
{
"identity" : "swift-case-paths",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths.git",
"state" : {
"revision" : "19b7263bacb9751f151ec0c93ec816fe1ef67c7b",
"version" : "1.6.1"
}
},
{
"identity" : "swift-clocks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-clocks",
"state" : {
"revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e",
"version" : "1.0.6"
}
},
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8",
"version" : "1.3.1"
}
},
{
"identity" : "swift-dependencies",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-dependencies.git",
"state" : {
"revision" : "ec2862d1364536fc22ec56a3094e7a034bbc7da8",
"version" : "1.8.1"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax",
"state" : {
"revision" : "0687f71944021d616d34d922343dcef086855920",
"version" : "600.0.1"
}
},
{
"identity" : "swift-validations",
"kind" : "remoteSourceControl",
"location" : "https://github.com/m-housh/swift-validations.git",
"state" : {
"revision" : "95ea5d267e37f6cdb9f91c5c8a01e718b9299db6",
"version" : "0.3.4"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4",
"version" : "1.5.2"
}
}
],
"version" : 3
}

36
Package.swift Normal file
View File

@@ -0,0 +1,36 @@
// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "swift-manual-s",
platforms: [
.macOS(.v10_15)
],
products: [
.library(name: "ManualS", targets: ["ManualS"])
],
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.0.0"),
.package(url: "https://github.com/m-housh/swift-validations.git", from: "0.3.4")
],
targets: [
.target(
name: "ManualS",
dependencies: [
"Models",
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies"),
.product(name: "Validations", package: "swift-validations")
]
),
.target(
name: "Models"
),
.testTarget(
name: "ManualSTests",
dependencies: ["ManualS"]
)
]
)

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

View File

@@ -0,0 +1,24 @@
public enum BalancePoint {
public struct Request: Codable, Equatable, Sendable {
public let winterDesignTemperature: Int
public let heatLoss: Int
public let heatPumpCapacity: Capacity.HeatPumpHeating
public init(winterDesignTemperature: Int, heatLoss: Int, heatPumpCapacity: Capacity.HeatPumpHeating) {
self.winterDesignTemperature = winterDesignTemperature
self.heatLoss = heatLoss
self.heatPumpCapacity = heatPumpCapacity
}
}
public struct Response: Codable, Equatable, Sendable {
public let balancePointTemperature: Double
public init(balancePointTemperature: Double) {
self.balancePointTemperature = balancePointTemperature
}
}
}

View File

@@ -0,0 +1,12 @@
public struct AdjustmentMultiplier: Codable, Equatable, Sendable {
public let coolingTotal: Double?
public let coolingSensible: Double?
public let heating: Double
public init(coolingTotal: Double? = nil, coolingSensible: Double? = nil, heating: Double) {
self.coolingTotal = coolingTotal
self.coolingSensible = coolingSensible
self.heating = heating
}
}

View File

@@ -0,0 +1,104 @@
// A container / namespace for different capacity containers.
public enum Capacity {
public struct Cooling: Codable, Equatable, Sendable {
public let total: Int
public let sensible: Int
public var latent: Int
public init(
total: Int,
sensible: Int,
latent: Int? = nil
) {
self.total = total
self.sensible = sensible
self.latent = latent ?? total - sensible
}
}
public struct Heating: Codable, Equatable, Sendable {
public let total: Int
public init(total: Int) {
self.total = total
}
}
public struct HeatPumpHeating: Codable, Equatable, Sendable {
public let at47: Int
public let at17: Int
public init(at47: Int, at17: Int) {
self.at47 = at47
self.at17 = at17
}
}
public struct ManufacturersCooling: Codable, Equatable, Sendable {
public let airflow: Int
public let capacity: Container
public let otherCapacity: Container?
public init(
airflow: Int,
capacity: Capacity.ManufacturersCooling.Container,
otherCapacity: Capacity.ManufacturersCooling.Container? = nil
) {
self.airflow = airflow
self.capacity = capacity
self.otherCapacity = otherCapacity
}
public struct Container: Codable, Equatable, Sendable {
public let dryBulbTemperature: Int
public let wetBulbTemperature: Int
public let outdoorTemperature: Int
public let totalCapacity: Int
public let sensibleCapacity: Int
public let adjustmentMultipliers: AdjustmentMultiplier?
public var latentCapacity: Int { totalCapacity - sensibleCapacity }
public init(
dryBulbTemperature: Int,
wetBulbTemperature: Int,
outdoorTemperature: Int,
totalCapacity: Int,
sensibleCapacity: Int,
adjustmentMultipliers: AdjustmentMultiplier? = nil
) {
self.dryBulbTemperature = dryBulbTemperature
self.wetBulbTemperature = wetBulbTemperature
self.outdoorTemperature = outdoorTemperature
self.totalCapacity = totalCapacity
self.sensibleCapacity = sensibleCapacity
self.adjustmentMultipliers = adjustmentMultipliers
}
}
}
public struct ManufacturersContainer: Codable, Equatable, Sendable {
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
) {
self.wetBulb = wetBulb
self.totalCapacity = totalCapacity
self.sensibleCapacity = sensibleCapacity
self.adjustmentMultipliers = adjustmentMultipliers
}
}
}

View File

@@ -0,0 +1,45 @@
import Foundation
public struct DesignInfo: Codable, Equatable, Sendable {
public let summer: Summer
public let winter: Winter
public let elevation: Int
public init(
summer: DesignInfo.Summer = .init(),
winter: DesignInfo.Winter = .init(),
elevation: Int = 0
) {
self.summer = summer
self.winter = winter
self.elevation = elevation
}
}
public extension DesignInfo {
struct Summer: Codable, Equatable, Sendable {
public let outdoorTemperature: Int
public let indoorTemperature: Int
public let indoorHumidity: Int
public init(
outdoorTemperature: Int = 90,
indoorTemperature: Int = 75,
indoorHumidity: Int = 50
) {
self.outdoorTemperature = outdoorTemperature
self.indoorTemperature = indoorTemperature
self.indoorHumidity = indoorHumidity
}
}
struct Winter: Codable, Equatable, Sendable {
public let outdoorTemperature: Int
public init(outdoorTemperature: Int = 5) {
self.outdoorTemperature = outdoorTemperature
}
}
}

View File

@@ -0,0 +1,18 @@
public struct HouseLoad: Codable, Equatable, Sendable {
public let coolingTotal: Int
public let coolingSensible: Int
public let heating: Int
public var coolingLatent: Int { coolingTotal - coolingSensible }
public init(
coolingTotal: Int,
coolingSensible: Int,
heating: Int
) {
self.coolingTotal = coolingTotal
self.coolingSensible = coolingSensible
self.heating = heating
}
}

View File

@@ -0,0 +1,27 @@
public enum SystemType: Codable, Equatable, Sendable {
case airToAir(type: EquipmentType, compressor: CompressorType, climate: ClimateType)
case heatingOnly(type: HeatingOnlyType)
public enum ClimateType: String, CaseIterable, Codable, Equatable, Sendable {
case mildWinterOrLatentLoad
case coldWinterOrNoLatentLoad
}
public enum CompressorType: String, CaseIterable, Codable, Equatable, Sendable {
case singleSpeed
case multiSpeed
case variableSpeed
}
public enum EquipmentType: String, CaseIterable, Codable, Equatable, Sendable {
case airConditioner
case heatPump
}
public enum HeatingOnlyType: String, CaseIterable, Codable, Equatable, Sendable {
case boiler
case electric
case furnace
}
}

View File

@@ -0,0 +1,15 @@
public enum Derating {
public struct Request: Codable, Equatable, Sendable {
public let elevation: Int
public let systemType: SystemType
public init(elevation: Int, systemType: SystemType) {
self.elevation = elevation
self.systemType = systemType
}
}
public typealias Response = AdjustmentMultiplier
}

View File

@@ -0,0 +1,148 @@
public enum Interpolate {
public struct Request: Codable, Equatable, Sendable {
public let designInfo: DesignInfo
public let houseLoad: HouseLoad
public let systemType: SystemType
public let manufacturersCapacity: Capacity.ManufacturersCooling
public init(
designInfo: DesignInfo,
houseLoad: HouseLoad,
systemType: SystemType,
manufacturersCapacity: Capacity.ManufacturersCooling
) {
self.designInfo = designInfo
self.houseLoad = houseLoad
self.systemType = systemType
self.manufacturersCapacity = manufacturersCapacity
}
}
public struct Response: Codable, Equatable, Sendable {
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
public let altitudeDerating: AdjustmentMultiplier?
public let capacityAsPercentOfLoad: Capacity.Cooling
public let sizingLimits: SizingLimits.Response
public init(
failures: [String]? = nil,
interpolationType: InterpolationType,
interpolatedCapacity: Capacity.Cooling,
excessLatent: Int,
finalCapacityAtDesign: Capacity.Cooling,
altitudeDerating: AdjustmentMultiplier? = nil,
capacityAsPercentOfLoad: Capacity.Cooling,
sizingLimits: SizingLimits.Response
) {
self.failed = failures != nil ? failures!.count > 0 : false
self.failures = failures
self.interpolationType = interpolationType
self.interpolatedCapacity = interpolatedCapacity
self.excessLatent = excessLatent
self.finalCapacityAtDesign = finalCapacityAtDesign
self.altitudeDerating = altitudeDerating
self.capacityAsPercentOfLoad = capacityAsPercentOfLoad
self.sizingLimits = sizingLimits
}
}
public enum InterpolationType2: Codable, Equatable, Sendable {
case noInterpolation(Capacity.ManufacturersCooling)
case oneWayIndoor(OneWayIndoor)
case oneWayOutdoor(OneWayOutdoor)
}
public struct OneWayIndoor: Codable, Equatable, Sendable {
public let airflow: Int
public let outdoorTemperature: Int
public let capacities: Capacities
public let adjustmentMultipliers: AdjustmentMultiplier?
public init(
airflow: Int,
outdoorTemperature: Int,
capacities: Interpolate.OneWayIndoor.Capacities,
adjustmentMultipliers: AdjustmentMultiplier? = nil
) {
self.airflow = airflow
self.outdoorTemperature = outdoorTemperature
self.capacities = capacities
self.adjustmentMultipliers = adjustmentMultipliers
}
public struct Capacities: Codable, Equatable, Sendable {
public let aboveDewpoint: Capacity.ManufacturersContainer
public let belowDewpoint: Capacity.ManufacturersContainer
public init(aboveDewpoint: Capacity.ManufacturersContainer, belowDewpoint: Capacity.ManufacturersContainer) {
self.aboveDewpoint = aboveDewpoint
self.belowDewpoint = belowDewpoint
}
}
}
public struct OneWayOutdoor: Codable, Equatable, Sendable {
public let airflow: Int
public let wetBulb: Int
public let capacities: Capacities
public let adjustmentMultipliers: AdjustmentMultiplier?
public init(
airflow: Int,
wetBulb: Int,
capacities: Interpolate.OneWayOutdoor.Capacities,
adjustmentMultipliers: AdjustmentMultiplier? = nil
) {
self.airflow = airflow
self.wetBulb = wetBulb
self.capacities = capacities
self.adjustmentMultipliers = adjustmentMultipliers
}
public struct Capacities: Codable, Equatable, Sendable {
public let aboveOutdoor: Capacity
public let belowOutdoor: Capacity
public init(
aboveOutdoor: Interpolate.OneWayOutdoor.Capacities.Capacity,
belowOutdoor: Interpolate.OneWayOutdoor.Capacities.Capacity
) {
self.aboveOutdoor = aboveOutdoor
self.belowOutdoor = belowOutdoor
}
public struct Capacity: Codable, Equatable, Sendable {
public let outdoorTemperature: Int
public let totalCapacity: Int
public let sensibleCapacity: Int
public init(outdoorTemperature: Int, totalCapacity: Int, sensibleCapacity: Int) {
self.outdoorTemperature = outdoorTemperature
self.totalCapacity = totalCapacity
self.sensibleCapacity = sensibleCapacity
}
}
}
}
public enum InterpolationType: String, CaseIterable, Codable, Equatable, Sendable {
case noInterpolation
case oneWayIndoor
case oneWayOutdoor
case twoWay
}
}

View File

@@ -0,0 +1,22 @@
public enum RequiredKW {
public struct Request: Codable, Equatable, Sendable {
public let capacityAtDesign: Int?
public let heatLoss: Int
public init(capacityAtDesign: Int? = nil, heatLoss: Int) {
self.capacityAtDesign = capacityAtDesign
self.heatLoss = heatLoss
}
}
public struct Response: Codable, Equatable, Sendable {
public let requiredKW: Double
public init(requiredKW: Double) {
self.requiredKW = requiredKW
}
}
}

View File

@@ -0,0 +1,44 @@
public enum SizingLimits {
public struct Request: Codable, Equatable, Sendable {
public let systemType: SystemType
public let houseLoad: HouseLoad?
public init(systemType: SystemType, houseLoad: HouseLoad? = nil) {
self.systemType = systemType
self.houseLoad = houseLoad
}
}
public struct Response: Codable, Equatable, Sendable {
public let oversizing: SizingLimits.Limits
public let undersizing: SizingLimits.Limits
public init(oversizing: SizingLimits.Limits, undersizing: SizingLimits.Limits) {
self.oversizing = oversizing
self.undersizing = undersizing
}
}
public struct Limits: Codable, Equatable, Sendable {
public let heating: Int
public let coolingTotal: Int
public let coolingSensible: Int?
public let coolingLatent: Int?
public init(
heating: Int,
coolingTotal: Int,
coolingSensible: Int? = nil,
coolingLatent: Int? = nil
) {
self.heating = heating
self.coolingTotal = coolingTotal
self.coolingSensible = coolingSensible
self.coolingLatent = coolingLatent
}
}
}

View File

@@ -0,0 +1,213 @@
import CoreFoundation
import Dependencies
import ManualS
import Testing
@Suite("ManualSTests")
struct ManualSTests {
@Test
func balancePoint() async throws {
try await withDependencies {
$0.manualS = .liveValue
} operation: {
@Dependency(\.manualS) var manualS
let balancePoint = try await manualS.balancePoint(.init(
winterDesignTemperature: 5,
heatLoss: 49667,
heatPumpCapacity: .init(at47: 24600, at17: 15100)
))
let rounded = round(balancePoint.balancePointTemperature * 10) / 10
#expect(rounded == 38.5)
}
}
@Test(
arguments: SystemType.makeAirToAirTestCases(climate: .mildWinterOrLatentLoad)
)
func mildWinterSizingLimits(system: SystemType) async throws {
try await withDependencies {
$0.manualS = .liveValue
} operation: {
@Dependency(\.manualS) var manualS
let limits = try await manualS.sizingLimits(.init(systemType: system))
#expect(limits.oversizing.coolingLatent == 150)
#expect(limits.oversizing.coolingTotal == system.compressorType!.mildWinterTotalLimit)
#expect(limits.undersizing.coolingTotal == 90)
#expect(limits.undersizing.coolingSensible == 90)
#expect(limits.undersizing.coolingLatent == 90)
}
}
@Test(
arguments: SystemType.makeAirToAirTestCases(climate: .coldWinterOrNoLatentLoad)
)
func coldWinterSizingLimits(system: SystemType) async throws {
try await withDependencies {
$0.manualS = .liveValue
} operation: {
@Dependency(\.manualS) var manualS
let limits = try await manualS.sizingLimits(.init(
systemType: system,
houseLoad: .init(coolingTotal: 17872, coolingSensible: 13894, heating: 49667)
))
#expect(limits.oversizing.coolingLatent == 150)
#expect(limits.oversizing.coolingTotal == 184)
#expect(limits.undersizing.coolingTotal == 90)
#expect(limits.undersizing.coolingSensible == 90)
#expect(limits.undersizing.coolingLatent == 90)
await #expect(throws: HouseLoadError()) {
try await manualS.sizingLimits(.init(systemType: system))
}
}
}
@Test(
arguments: SystemType.HeatingOnlyType.allCases
)
func heatingOnlySizingLimits(heatingType: SystemType.HeatingOnlyType) async throws {
try await withDependencies {
$0.manualS = .liveValue
} operation: {
@Dependency(\.manualS) var manualS
let limits = try await manualS.sizingLimits(.init(
systemType: .heatingOnly(type: heatingType),
))
#expect(limits.oversizing.heating == heatingType.oversizingLimit)
#expect(limits.undersizing.coolingTotal == 90)
#expect(limits.undersizing.heating == 90)
#expect(limits.undersizing.coolingSensible == 90)
#expect(limits.undersizing.coolingLatent == 90)
}
}
@Test(
arguments: [
(RequiredKW.Request(heatLoss: 49667), 14.55),
(RequiredKW.Request(capacityAtDesign: 11300, heatLoss: 49667), 11.24)
]
)
func requiredKW(request: RequiredKW.Request, expected: Double) async throws {
try await withDependencies {
$0.manualS = .liveValue
} operation: {
@Dependency(\.manualS) var manualS
let requiredKW = try await manualS.requiredKW(request)
let rounded = round(requiredKW.requiredKW * 100) / 100
#expect(rounded == expected)
}
}
@Test(
arguments: [
(elevation: 0, expected: 1.0),
(elevation: 1000, expected: 0.96),
(elevation: 2000, expected: 0.92),
(elevation: 3000, expected: 0.88),
(elevation: 4000, expected: 0.84),
(elevation: 5000, expected: 0.8),
(elevation: 6000, expected: 0.76),
(elevation: 7000, expected: 0.72),
(elevation: 8000, expected: 0.68),
(elevation: 9000, expected: 0.64),
(elevation: 10000, expected: 0.6),
(elevation: 11000, expected: 0.56),
(elevation: 12000, expected: 0.52),
(elevation: 13000, expected: 0.52)
]
)
func heatingDerating(elevation: Int, expected: Double) async throws {
try await withDependencies {
$0.manualS = .liveValue
} operation: {
@Dependency(\.manualS) var manualS
for heatingType in [SystemType.HeatingOnlyType.boiler, .furnace] {
let derating = try await manualS.derating(.init(
elevation: elevation,
systemType: .heatingOnly(type: heatingType)
))
#expect(derating.heating == expected)
}
}
}
@Test(
arguments: [
(elevation: 0, expected: AdjustmentMultiplier(coolingTotal: 1, coolingSensible: 1, heating: 1)),
(elevation: 1000, expected: AdjustmentMultiplier(coolingTotal: 0.99, coolingSensible: 0.97, heating: 0.98)),
(elevation: 2000, expected: AdjustmentMultiplier(coolingTotal: 0.98, coolingSensible: 0.94, heating: 0.97)),
(elevation: 3000, expected: AdjustmentMultiplier(coolingTotal: 0.98, coolingSensible: 0.91, heating: 0.95)),
(elevation: 4000, expected: AdjustmentMultiplier(coolingTotal: 0.97, coolingSensible: 0.88, heating: 0.94)),
(elevation: 5000, expected: AdjustmentMultiplier(coolingTotal: 0.96, coolingSensible: 0.85, heating: 0.92)),
(elevation: 6000, expected: AdjustmentMultiplier(coolingTotal: 0.95, coolingSensible: 0.82, heating: 0.9)),
(elevation: 7000, expected: AdjustmentMultiplier(coolingTotal: 0.94, coolingSensible: 0.8, heating: 0.89)),
(elevation: 8000, expected: AdjustmentMultiplier(coolingTotal: 0.94, coolingSensible: 0.77, heating: 0.87)),
(elevation: 9000, expected: AdjustmentMultiplier(coolingTotal: 0.93, coolingSensible: 0.74, heating: 0.86)),
(elevation: 10000, expected: AdjustmentMultiplier(coolingTotal: 0.92, coolingSensible: 0.71, heating: 0.84)),
(elevation: 11000, expected: AdjustmentMultiplier(coolingTotal: 0.91, coolingSensible: 0.68, heating: 0.82)),
(elevation: 12000, expected: AdjustmentMultiplier(coolingTotal: 0.9, coolingSensible: 0.65, heating: 0.81)),
(elevation: 13000, expected: AdjustmentMultiplier(coolingTotal: 0.9, coolingSensible: 0.65, heating: 0.81))
]
)
func airToAirDerating(elevation: Int, expected: AdjustmentMultiplier) async throws {
try await withDependencies {
$0.manualS = .liveValue
} operation: {
@Dependency(\.manualS) var manualS
let derating = try await manualS.derating(.init(
elevation: elevation,
systemType: .airToAir(type: .airConditioner, compressor: .singleSpeed, climate: .mildWinterOrLatentLoad)
))
#expect(derating == expected)
}
}
}
extension SystemType {
static func makeAirToAirTestCases(climate: SystemType.ClimateType) -> [Self] {
var items: [Self] = []
for compressor in SystemType.CompressorType.allCases {
for equipment in SystemType.EquipmentType.allCases {
items.append(.airToAir(type: equipment, compressor: compressor, climate: climate))
}
}
return items
}
var compressorType: SystemType.CompressorType? {
switch self {
case let .airToAir(type: _, compressor: compressor, climate: _): return compressor
case .heatingOnly: return nil
}
}
}
extension SystemType.CompressorType {
var mildWinterTotalLimit: Int {
switch self {
case .singleSpeed: return 115
case .multiSpeed: return 120
case .variableSpeed: return 130
}
}
}
extension SystemType.HeatingOnlyType {
var oversizingLimit: Int {
switch self {
case .boiler, .furnace: return 140
case .electric: return 175
}
}
}