feat: Initial commit
This commit is contained in:
7
.editorconfig
Normal file
7
.editorconfig
Normal 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
8
.gitignore
vendored
Normal 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
11
.swiftformat
Normal 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
11
.swiftlint.yml
Normal 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
78
Package.resolved
Normal 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
36
Package.swift
Normal 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"]
|
||||
)
|
||||
]
|
||||
)
|
||||
2
Sources/ManualS/Exports.swift
Normal file
2
Sources/ManualS/Exports.swift
Normal file
@@ -0,0 +1,2 @@
|
||||
@_exported import Dependencies
|
||||
@_exported import Models
|
||||
19
Sources/ManualS/Extensions/DesignInfo+validator.swift
Normal file
19
Sources/ManualS/Extensions/DesignInfo+validator.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Sources/ManualS/Extensions/HeatPumpCapacity+validator.swift
Normal file
13
Sources/ManualS/Extensions/HeatPumpCapacity+validator.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Sources/ManualS/Extensions/HouseLoad+validator.swift
Normal file
14
Sources/ManualS/Extensions/HouseLoad+validator.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
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() {}
|
||||
}
|
||||
33
Sources/ManualS/ManualS.swift
Normal file
33
Sources/ManualS/ManualS.swift
Normal 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()
|
||||
}
|
||||
24
Sources/Models/BalancePoint.swift
Normal file
24
Sources/Models/BalancePoint.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Sources/Models/Core/AdjustmentMultiplier.swift
Normal file
12
Sources/Models/Core/AdjustmentMultiplier.swift
Normal 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
|
||||
}
|
||||
}
|
||||
104
Sources/Models/Core/Capacity.swift
Normal file
104
Sources/Models/Core/Capacity.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
45
Sources/Models/Core/DesignInfo.swift
Normal file
45
Sources/Models/Core/DesignInfo.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Sources/Models/Core/HouseLoad.swift
Normal file
18
Sources/Models/Core/HouseLoad.swift
Normal 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
|
||||
}
|
||||
}
|
||||
27
Sources/Models/Core/SystemType.swift
Normal file
27
Sources/Models/Core/SystemType.swift
Normal 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
|
||||
}
|
||||
}
|
||||
15
Sources/Models/Derating.swift
Normal file
15
Sources/Models/Derating.swift
Normal 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
|
||||
}
|
||||
148
Sources/Models/Interpolate.swift
Normal file
148
Sources/Models/Interpolate.swift
Normal 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
|
||||
}
|
||||
}
|
||||
22
Sources/Models/RequiredKW.swift
Normal file
22
Sources/Models/RequiredKW.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
44
Sources/Models/SizingLimits.swift
Normal file
44
Sources/Models/SizingLimits.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
213
Tests/ManualSTests/ManualSTests.swift
Normal file
213
Tests/ManualSTests/ManualSTests.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user