import CoreFoundation import Dependencies import ManualS import Testing @Suite("ManualSTests") struct ManualSTests { @Test func coolingInterpolation_noInterpolation() async throws { try await withDependencies { $0.manualS = .liveValue } operation: { @Dependency(\.manualS) var manualS let response = try await manualS.coolingInterpolation(.init( summerDesignInfo: .mock, coolingLoad: .mockCoolingLoad, systemType: .airToAir(type: .airConditioner, compressor: .singleSpeed, climate: .mildWinterOrLatentLoad), interpolation: .noInterpolation( .init( airflow: 800, capacity: .init( dryBulbTemperature: 75, wetBulbTemperature: 63, outdoorTemperature: 90, totalCapacity: 22600, sensibleCapacity: 16850 ) ), nil ) )) #expect(response.failed) #expect(response.failures == ["Oversizing total failure."]) #expect(response.interpolatedCapacity == .init(total: 22600, sensible: 16850)) #expect(response.excessLatent == 886) #expect(response.finalCapacityAtDesign == .init(total: 22600, sensible: 17736)) #expect(response.capacityAsPercentOfLoad == .init(total: 126, sensible: 127, latent: 122)) } } @Test func coolingInterpolation_oneWayIndoor() async throws { try await withDependencies { $0.manualS = .liveValue } operation: { @Dependency(\.manualS) var manualS let response = try await manualS.coolingInterpolation(.init( summerDesignInfo: .mock, coolingLoad: .mockCoolingLoad, systemType: .airToAir(type: .airConditioner, compressor: .singleSpeed, climate: .mildWinterOrLatentLoad), interpolation: .oneWayIndoor(.init( airflow: 800, outdoorTemperature: 95, capacities: .init( aboveDewpoint: .init(wetBulb: 67, totalCapacity: 24828, sensibleCapacity: 15937), belowDewpoint: .init(wetBulb: 62, totalCapacity: 23046, sensibleCapacity: 19078) ) )) )) #expect(response.failed) #expect(response.failures == ["Oversizing total failure."]) #expect(response.interpolatedCapacity == .init(total: 23402, sensible: 18450)) #expect(response.excessLatent == 487) #expect(response.finalCapacityAtDesign == .init(total: 23402, sensible: 18937)) #expect(response.capacityAsPercentOfLoad == .init(total: 130, sensible: 136, latent: 112)) } } @Test func coolingInterpolation_oneWayOutdoor() async throws { try await withDependencies { $0.manualS = .liveValue } operation: { @Dependency(\.manualS) var manualS let response = try await manualS.coolingInterpolation(.init( summerDesignInfo: .mock, coolingLoad: .mockCoolingLoad, systemType: .airToAir(type: .airConditioner, compressor: .singleSpeed, climate: .mildWinterOrLatentLoad), interpolation: .oneWayOutdoor(.init( airflow: 800, wetBulb: 63, capacities: .init( aboveOutdoor: .init(outdoorTemperature: 95, totalCapacity: 22000, sensibleCapacity: 16600), belowOutdoor: .init(outdoorTemperature: 85, totalCapacity: 23200, sensibleCapacity: 17100) ) )) )) #expect(response.failed) #expect(response.failures == ["Oversizing total failure."]) #expect(response.interpolatedCapacity == .init(total: 22600, sensible: 16850)) #expect(response.excessLatent == 886) #expect(response.finalCapacityAtDesign == .init(total: 22600, sensible: 17736)) #expect(response.capacityAsPercentOfLoad == .init(total: 126, sensible: 127, latent: 122)) } } @Test func coolingInterpolation_twoWay() async throws { try await withDependencies { $0.manualS = .liveValue } operation: { @Dependency(\.manualS) var manualS let response = try await manualS.coolingInterpolation(.init( summerDesignInfo: .mock, coolingLoad: .mockCoolingLoad, systemType: .airToAir(type: .airConditioner, compressor: .singleSpeed, climate: .mildWinterOrLatentLoad), interpolation: .twoWay(.init( airflow: 800, capacities: .init( above: .init( outdoorTemperature: 95, aboveDewPoint: .init(wetBulb: 67, totalCapacity: 24828, sensibleCapacity: 15937), belowDewPoint: .init(wetBulb: 62, totalCapacity: 23046, sensibleCapacity: 19078) ), below: .init( outdoorTemperature: 85, aboveDewPoint: .init(wetBulb: 67, totalCapacity: 25986, sensibleCapacity: 16330), belowDewPoint: .init(wetBulb: 62, totalCapacity: 24029, sensibleCapacity: 19605) ) ) )) )) #expect(response.failed) #expect(response.failures == ["Oversizing total failure."]) #expect(response.interpolatedCapacity == .init(total: 23915, sensible: 18700)) #expect(response.excessLatent == 618) #expect(response.finalCapacityAtDesign == .init(total: 23915, sensible: 19318)) #expect(response.capacityAsPercentOfLoad == .init(total: 133, sensible: 139, latent: 115)) } } @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, coolingLoad: .init(total: 17872, sensible: 13894) )) #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 } } } extension Capacity.Cooling { static let mockCoolingLoad = Self(total: 17872, sensible: 13894) } extension DesignInfo.Summer { static let mock = Self(outdoorTemperature: 90, indoorTemperature: 75, indoorHumidity: 50) }