DetermineBasalJsonTests.swift 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. @Suite("DetermineBasal testing using JSON inputs", .serialized) struct DetermineBasalJsonTests {
  5. let timeZoneForTests = TimeZoneForTests()
  6. @Test(
  7. "DetermineBasal should produce same results for fixed JS",
  8. .enabled(if: ReplayTests.enabled)
  9. ) func replayErrorInputs() async throws {
  10. // Note: This test case can only test one timezone per invocation
  11. // so you need to manually change this to try out errors from
  12. // different timezones
  13. let testingTimezone = ReplayTests.timezone
  14. let files = try await HttpFiles.listFiles()
  15. for filePath in files {
  16. let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
  17. print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
  18. guard algorithmComparison.timezone == testingTimezone else {
  19. continue
  20. }
  21. guard let determineBasalInput = algorithmComparison.determineBasalInput else {
  22. print("Skipping, no determineBasalInput found")
  23. if let str = algorithmComparison.comparisonError {
  24. print(str)
  25. }
  26. if let str = algorithmComparison.swiftException {
  27. print(str)
  28. }
  29. continue
  30. }
  31. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  32. try await checkFixedJsAgainstSwift(determineBasalInput: determineBasalInput)
  33. print("Checked \(filePath) \(algorithmComparison.timezone)")
  34. timeZoneForTests.resetTimezone()
  35. }
  36. }
  37. func checkFixedJsAgainstSwift(determineBasalInput: DetermineBasalInputs) async throws {
  38. let openAps = OpenAPSFixed()
  39. let (determineBasalResultSwift, _) = OpenAPSSwift.determineBasal(
  40. glucose: determineBasalInput.glucose,
  41. currentTemp: determineBasalInput.currentTemp,
  42. iob: try JSONBridge.to(determineBasalInput.iob),
  43. profile: try JSONBridge.to(determineBasalInput.profile),
  44. autosens: try JSONBridge.to(determineBasalInput.autosens),
  45. meal: try JSONBridge.to(determineBasalInput.meal),
  46. microBolusAllowed: determineBasalInput.microBolusAllowed,
  47. reservoir: determineBasalInput.reservoir ?? 0,
  48. pumpHistory: determineBasalInput.pumpHistory,
  49. preferences: determineBasalInput.preferences,
  50. basalProfile: determineBasalInput.basalProfile,
  51. trioCustomOrefVariables: determineBasalInput.trioCustomOrefVariables,
  52. clock: determineBasalInput.clock
  53. )
  54. let determineBasalResultJavascript = try await openAps.determineBasalJavascript(
  55. glucose: determineBasalInput.glucose,
  56. currentTemp: determineBasalInput.currentTemp,
  57. iob: try JSONBridge.to(determineBasalInput.iob),
  58. profile: try JSONBridge.to(determineBasalInput.profile),
  59. autosens: try JSONBridge.to(determineBasalInput.autosens),
  60. meal: try JSONBridge.to(determineBasalInput.meal),
  61. microBolusAllowed: determineBasalInput.microBolusAllowed,
  62. reservoir: determineBasalInput.reservoir ?? 0,
  63. pumpHistory: determineBasalInput.pumpHistory,
  64. preferences: determineBasalInput.preferences,
  65. basalProfile: determineBasalInput.basalProfile,
  66. trioCustomOrefVariables: determineBasalInput.trioCustomOrefVariables,
  67. clock: determineBasalInput.clock
  68. )
  69. let comparison = JSONCompare.createComparison(
  70. function: .determineBasal,
  71. swift: determineBasalResultSwift,
  72. swiftDuration: 0.1,
  73. javascript: determineBasalResultJavascript,
  74. javascriptDuration: 0.1,
  75. iobInputs: nil,
  76. mealInputs: nil,
  77. autosensInputs: nil,
  78. determineBasalInputs: nil
  79. )
  80. if comparison.resultType == .valueDifference {
  81. print(comparison.differences!.prettyPrintedJSON!)
  82. }
  83. if comparison.resultType != .matching {
  84. print("REPLAY ERROR: Fixed JS didn't match")
  85. }
  86. #expect(comparison.resultType == .matching)
  87. }
  88. @Test("Format determineBasal inputs for running in JS", .enabled(if: false)) func formatInputs() async throws {
  89. let openAps = OpenAPSFixed()
  90. // this test is meant for one-off analysis so it's ok to hard code
  91. // a file, just make sure to _not_ check in updates to this to
  92. // avoid polluting our change logs
  93. let algorithmComparison = try await HttpFiles.downloadFile(at: "/files/f1d04efa-c39b-4f0a-9955-65ab663ff9fb.0.json")
  94. let determineBasalInput = algorithmComparison.determineBasalInput!
  95. let encoder = JSONCoding.encoder
  96. let output = try encoder.encode(determineBasalInput)
  97. let sharedDir = FileManager.default.temporaryDirectory
  98. let outputURL = sharedDir.appendingPathComponent("determine_basal_error_inputs.json")
  99. // Print the path so you can find it
  100. print("Writing to: \(outputURL.path)")
  101. try output.write(to: outputURL)
  102. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  103. let (determineBasalResultSwift, _) = OpenAPSSwift.determineBasal(
  104. glucose: determineBasalInput.glucose,
  105. currentTemp: determineBasalInput.currentTemp,
  106. iob: try JSONBridge.to(determineBasalInput.iob),
  107. profile: try JSONBridge.to(determineBasalInput.profile),
  108. autosens: try JSONBridge.to(determineBasalInput.autosens),
  109. meal: try JSONBridge.to(determineBasalInput.meal),
  110. microBolusAllowed: determineBasalInput.microBolusAllowed,
  111. reservoir: determineBasalInput.reservoir ?? 0,
  112. pumpHistory: determineBasalInput.pumpHistory,
  113. preferences: determineBasalInput.preferences,
  114. basalProfile: determineBasalInput.basalProfile,
  115. trioCustomOrefVariables: determineBasalInput.trioCustomOrefVariables,
  116. clock: determineBasalInput.clock
  117. )
  118. print("Swift result")
  119. switch determineBasalResultSwift {
  120. case let .success(rawJson):
  121. print(rawJson)
  122. case let .failure(error):
  123. print(error.localizedDescription)
  124. }
  125. let determineBasalResultJavascript = try await openAps.determineBasalJavascript(
  126. glucose: determineBasalInput.glucose,
  127. currentTemp: determineBasalInput.currentTemp,
  128. iob: try JSONBridge.to(determineBasalInput.iob),
  129. profile: try JSONBridge.to(determineBasalInput.profile),
  130. autosens: try JSONBridge.to(determineBasalInput.autosens),
  131. meal: try JSONBridge.to(determineBasalInput.meal),
  132. microBolusAllowed: determineBasalInput.microBolusAllowed,
  133. reservoir: determineBasalInput.reservoir ?? 0,
  134. pumpHistory: determineBasalInput.pumpHistory,
  135. preferences: determineBasalInput.preferences,
  136. basalProfile: determineBasalInput.basalProfile,
  137. trioCustomOrefVariables: determineBasalInput.trioCustomOrefVariables,
  138. clock: determineBasalInput.clock
  139. )
  140. print("Fixed JS result")
  141. switch determineBasalResultJavascript {
  142. case let .success(rawJson):
  143. print(rawJson)
  144. case let .failure(error):
  145. print(error.localizedDescription)
  146. }
  147. let comparison = JSONCompare.createComparison(
  148. function: .determineBasal,
  149. swift: determineBasalResultSwift,
  150. swiftDuration: 0.1,
  151. javascript: determineBasalResultJavascript,
  152. javascriptDuration: 0.1,
  153. iobInputs: nil,
  154. mealInputs: nil,
  155. autosensInputs: nil,
  156. determineBasalInputs: nil
  157. )
  158. if comparison.resultType == .valueDifference {
  159. print(comparison.differences!.prettyPrintedJSON!)
  160. printForecasts(comparison.differences)
  161. }
  162. #expect(comparison.resultType == .matching)
  163. timeZoneForTests.resetTimezone()
  164. }
  165. func printForecasts(_ values: [String: Any]?) {
  166. guard let values = values else { return }
  167. guard let forecasts = values["predBGs"] as? Trio.ValueDifference else { return }
  168. let js = forecasts.js.toDictionary()
  169. let swift = forecasts.swift.toDictionary()
  170. for forecastType in ["IOB", "ZT", "UAM", "COB"] {
  171. print("")
  172. guard let swiftForecast = swift[forecastType]?.toIntArray(),
  173. let jsForecast = js[forecastType]?.toIntArray()
  174. else {
  175. print("missing \(forecastType) forecast, skipping")
  176. continue
  177. }
  178. if swiftForecast.count == jsForecast.count {
  179. print(forecastType)
  180. } else {
  181. print("\(forecastType) has length mismatch ❌")
  182. }
  183. print("Row\tSft\tJS\tMatch")
  184. print("--------------")
  185. for (row, values) in zip(swiftForecast, jsForecast).enumerated() {
  186. let pass: String
  187. if abs(values.0 - values.1) <= 1 {
  188. pass = "✅"
  189. } else {
  190. pass = "❌"
  191. }
  192. print("\(row)\t\(values.0)\t\(values.1)\t\(pass)")
  193. }
  194. }
  195. }
  196. }
  197. extension JSONValue {
  198. func toDictionary() -> [String: Trio.JSONValue] {
  199. switch self {
  200. case let .object(dict):
  201. return dict
  202. default:
  203. fatalError()
  204. }
  205. }
  206. func toIntArray() -> [Int] {
  207. switch self {
  208. case let .array(array):
  209. return array.map { $0.toInt() }
  210. default:
  211. fatalError()
  212. }
  213. }
  214. func toInt() -> Int {
  215. switch self {
  216. case let .number(number):
  217. return Int(number)
  218. default:
  219. fatalError()
  220. }
  221. }
  222. }