feat: Initial commit

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

View File

@@ -0,0 +1,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
}
}
}