AutosensJsonTests.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. @Suite("Autosens using real JSON", .serialized) struct AutosensJsonTests {
  5. let timeZoneForTests = TimeZoneForTests()
  6. // static func from<T: Decodable>(string: String) throws -> T
  7. func loadJson<T: Decodable>(_ name: String) throws -> T {
  8. let testBundle = Bundle(for: BundleReference.self)
  9. let path = testBundle.path(forResource: name, ofType: "json")!
  10. let data = try Data(contentsOf: URL(fileURLWithPath: path))
  11. return try JSONCoding.decoder.decode(T.self, from: data)
  12. }
  13. @Test("Test with resistance") func generateJavascriptInputs() throws {
  14. let glucose: [BloodGlucose] = try loadJson("as-glucose")
  15. let pump: [PumpHistoryEvent] = try loadJson("as-pump")
  16. let basalProfile: [BasalProfileEntry] = try loadJson("as-basal")
  17. let profile: Profile = try loadJson("as-profile")
  18. let carbs: [CarbsEntry] = try loadJson("as-carbs")
  19. let tempTargets: [TempTarget] = try loadJson("as-temp-targets")
  20. let clock = Date("2025-06-08T00:14:35.481Z")!
  21. timeZoneForTests.setTimezone(identifier: "America/Los_Angeles")
  22. let autosensResult = try AutosensGenerator.generate(
  23. glucose: glucose,
  24. pumpHistory: pump,
  25. basalProfile: basalProfile,
  26. profile: profile,
  27. carbs: carbs,
  28. tempTargets: tempTargets,
  29. maxDeviations: 96,
  30. clock: clock,
  31. includeDeviationsForTesting: true
  32. )
  33. let deviationsUnsorted: [Decimal] = try loadJson("deviationsUnsorted")
  34. #expect(autosensResult.ratio == 1.2)
  35. #expect(autosensResult.newisf == 46)
  36. #expect(deviationsUnsorted.count == autosensResult.deviationsUnsorted?.count)
  37. for (ref, calc) in zip(deviationsUnsorted, autosensResult.deviationsUnsorted!) {
  38. // we can get differences due to rounding inconsistencies between
  39. // javascript and swift with negative numbers
  40. #expect(ref.isWithin(0.01, of: calc))
  41. }
  42. timeZoneForTests.resetTimezone()
  43. }
  44. func checkFixedJsAgainstSwift(autosensInputs: AutosensInputs) async throws {
  45. let openAps = OpenAPSFixed()
  46. let (autosensResultSwift, _) = OpenAPSSwift.autosense(
  47. glucose: autosensInputs.glucose,
  48. pumpHistory: autosensInputs.history,
  49. basalProfile: autosensInputs.basalProfile,
  50. profile: try JSONBridge.to(autosensInputs.profile),
  51. carbs: autosensInputs.carbs,
  52. tempTargets: autosensInputs.tempTargets,
  53. clock: autosensInputs.clock,
  54. includeDeviationsForTesting: true
  55. )
  56. let autosensResultJavascript = await openAps.autosenseJavascript(
  57. glucose: autosensInputs.glucose,
  58. pumpHistory: autosensInputs.history,
  59. basalprofile: autosensInputs.basalProfile,
  60. profile: try JSONBridge.to(autosensInputs.profile),
  61. carbs: autosensInputs.carbs,
  62. temptargets: autosensInputs.tempTargets,
  63. clock: autosensInputs.clock
  64. )
  65. let comparison = JSONCompare.createComparison(
  66. function: .autosens,
  67. swift: autosensResultSwift,
  68. swiftDuration: 0.1,
  69. javascript: autosensResultJavascript,
  70. javascriptDuration: 0.1,
  71. iobInputs: nil,
  72. mealInputs: nil,
  73. autosensInputs: nil,
  74. determineBasalInputs: nil
  75. )
  76. if comparison.resultType == .valueDifference {
  77. print(comparison.differences!.prettyPrintedJSON!)
  78. }
  79. if comparison.resultType != .matching {
  80. print("REPLAY ERROR: Fixed JS didn't match")
  81. }
  82. #expect(comparison.resultType == .matching)
  83. }
  84. func compareDeviations(swiftJson: String, jsJson: String) throws {
  85. // Parse both JSON strings
  86. let swiftData = swiftJson.data(using: .utf8)!
  87. let jsData = jsJson.data(using: .utf8)!
  88. let swiftDict = try JSONSerialization.jsonObject(with: swiftData) as! [String: Any]
  89. let jsDict = try JSONSerialization.jsonObject(with: jsData) as! [String: Any]
  90. // Extract deviationsUnsorted arrays
  91. let swiftDeviations = swiftDict["deviationsUnsorted"] as! [Any]
  92. let jsDeviations = jsDict["deviationsUnsorted"] as! [Any]
  93. // Convert both to Double arrays
  94. let swiftDoubles = swiftDeviations.compactMap { value -> Double? in
  95. if let number = value as? NSNumber {
  96. return number.doubleValue
  97. }
  98. return nil
  99. }
  100. let jsDoubles = jsDeviations.compactMap { value -> Double? in
  101. if let number = value as? NSNumber {
  102. return number.doubleValue
  103. } else if let string = value as? String {
  104. return Double(string)
  105. }
  106. return nil
  107. }
  108. // Compare the arrays
  109. print("Swift array count: \(swiftDoubles.count)")
  110. print("JS array count: \(jsDoubles.count)")
  111. guard swiftDoubles.count == jsDoubles.count else {
  112. print("Arrays have different lengths!")
  113. print("Swift: \(swiftDoubles)")
  114. print("JS: \(jsDoubles)")
  115. return
  116. }
  117. var differences: [(index: Int, swift: Double, js: Double)] = []
  118. for (index, (swiftVal, jsVal)) in zip(swiftDoubles, jsDoubles).enumerated() {
  119. if abs(swiftVal - jsVal) > 0.001 { // Small tolerance for floating point comparison
  120. differences.append((index: index, swift: swiftVal, js: jsVal))
  121. }
  122. }
  123. if differences.isEmpty {
  124. print("✅ Arrays are identical (within tolerance)")
  125. } else {
  126. print("❌ Found \(differences.count) differences:")
  127. for diff in differences {
  128. print(" Index \(diff.index): Swift=\(diff.swift), JS=\(diff.js)")
  129. }
  130. }
  131. }
  132. @Test(
  133. "should produce same results for autosens for fixed JS",
  134. .enabled(if: ReplayTests.enabled)
  135. ) func replayErrorInputs() async throws {
  136. let timezone = "America/Los_Angeles"
  137. var skippedTimezones = Set<String>()
  138. let files = try await HttpFiles.listFiles()
  139. for filePath in files {
  140. let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
  141. print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
  142. guard timezone == algorithmComparison.timezone else {
  143. skippedTimezones.insert(algorithmComparison.timezone)
  144. continue
  145. }
  146. guard let autosensInputs = algorithmComparison.autosensInput else {
  147. print("Skipping, no autosensInputs found")
  148. if let str = algorithmComparison.comparisonError {
  149. print(str)
  150. }
  151. if let str = algorithmComparison.swiftException {
  152. print(str)
  153. }
  154. continue
  155. }
  156. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  157. try await checkFixedJsAgainstSwift(autosensInputs: autosensInputs)
  158. print("Checked \(filePath) @ \(algorithmComparison.createdAt)")
  159. timeZoneForTests.resetTimezone()
  160. }
  161. print("Skipped timezones:")
  162. for skippedTimezone in skippedTimezones {
  163. print(" - \(skippedTimezone)")
  164. }
  165. }
  166. @Test("Format autosens inputs for running in JS", .enabled(if: ReplayTests.enabled)) func formatInputs() async throws {
  167. // this test is meant for one-off analysis so it's ok to hard code
  168. // a file, just make sure to _not_ check in updates to this to
  169. // avoid polluting our change logs
  170. let algorithmComparison = try await HttpFiles.downloadFile(at: "/files/432be489-adfd-4799-b469-8d3794d5188e.0.json")
  171. let autosensInputs = algorithmComparison.autosensInput!
  172. let encoder = JSONCoding.encoder
  173. let output = try encoder.encode(autosensInputs)
  174. let sharedDir = FileManager.default.temporaryDirectory
  175. let outputURL = sharedDir.appendingPathComponent("autosens_error_inputs.json")
  176. try output.write(to: outputURL)
  177. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  178. let openAps = OpenAPSFixed()
  179. let (autosensResultSwift, _) = OpenAPSSwift.autosense(
  180. glucose: autosensInputs.glucose,
  181. pumpHistory: autosensInputs.history,
  182. basalProfile: autosensInputs.basalProfile,
  183. profile: try JSONBridge.to(autosensInputs.profile),
  184. carbs: autosensInputs.carbs,
  185. tempTargets: autosensInputs.tempTargets,
  186. clock: autosensInputs.clock,
  187. includeDeviationsForTesting: true
  188. )
  189. let autosensResultJavascript = await openAps.autosenseJavascript(
  190. glucose: autosensInputs.glucose,
  191. pumpHistory: autosensInputs.history,
  192. basalprofile: autosensInputs.basalProfile,
  193. profile: try JSONBridge.to(autosensInputs.profile),
  194. carbs: autosensInputs.carbs,
  195. temptargets: autosensInputs.tempTargets,
  196. clock: autosensInputs.clock
  197. )
  198. if case let .success(swiftJson) = autosensResultSwift, case let .success(jsJson) = autosensResultJavascript {
  199. try compareDeviations(swiftJson: swiftJson, jsJson: jsJson)
  200. }
  201. // Print the path so you can find it
  202. print("Writing to: \(outputURL.path)")
  203. timeZoneForTests.resetTimezone()
  204. }
  205. @Test(
  206. "Testing IoB difference with Autosens",
  207. .enabled(if: ReplayTests.enabled)
  208. ) func testAutosensErrorWithIoB() async throws {
  209. let currentBasalRate = Decimal(0.55)
  210. let currentGlucoseDate = Date("2025-06-27T13:56:54.596Z")!
  211. let iobInputs: IobInputs = try loadJson("as_error_iob_inputs")
  212. let treatments = try IobHistory.calcTempTreatments(
  213. history: iobInputs.history.map({ $0.computedEvent() }),
  214. profile: iobInputs.profile,
  215. clock: iobInputs.clock,
  216. autosens: nil,
  217. zeroTempDuration: nil
  218. )
  219. let encoder = JSONCoding.encoder
  220. let output = try encoder.encode(treatments)
  221. let sharedDir = FileManager.default.temporaryDirectory
  222. let outputURL = sharedDir.appendingPathComponent("treatments-swift.json")
  223. try output.write(to: outputURL)
  224. print("WROTE FILE TO: \(outputURL.path)")
  225. var simulatedProfile = iobInputs.profile
  226. simulatedProfile.currentBasal = currentBasalRate
  227. simulatedProfile.temptargetSet = false
  228. let iob = try IobCalculation.iobTotal(treatments: treatments, profile: simulatedProfile, time: currentGlucoseDate)
  229. print(iob)
  230. }
  231. }