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