AutosensJsonTests.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. @Suite("Autosens using real JSON", .serialized) struct AutosensJsonTests {
  5. let timeZoneForTests = TimeZoneForTests()
  6. func checkFixedJsAgainstSwift(autosensInputs: AutosensInputs) async throws {
  7. let openAps = OpenAPSFixed()
  8. let (autosensResultSwift, _) = OpenAPSSwift.autosense(
  9. glucose: autosensInputs.glucose,
  10. pumpHistory: autosensInputs.history,
  11. basalProfile: autosensInputs.basalProfile,
  12. profile: try JSONBridge.to(autosensInputs.profile),
  13. carbs: autosensInputs.carbs,
  14. tempTargets: autosensInputs.tempTargets,
  15. clock: autosensInputs.clock,
  16. includeDeviationsForTesting: true
  17. )
  18. let autosensResultJavascript = await openAps.autosenseJavascript(
  19. glucose: autosensInputs.glucose,
  20. pumpHistory: autosensInputs.history,
  21. basalprofile: autosensInputs.basalProfile,
  22. profile: try JSONBridge.to(autosensInputs.profile),
  23. carbs: autosensInputs.carbs,
  24. temptargets: autosensInputs.tempTargets,
  25. clock: autosensInputs.clock,
  26. prepareFile: OpenAPSFixed.prepare
  27. )
  28. let comparison = JSONCompare.createComparison(
  29. function: .autosens,
  30. swift: autosensResultSwift,
  31. swiftDuration: 0.1,
  32. javascript: autosensResultJavascript,
  33. javascriptDuration: 0.1,
  34. iobInputs: nil,
  35. mealInputs: nil,
  36. autosensInputs: nil,
  37. determineBasalInputs: nil
  38. )
  39. if comparison.resultType == .valueDifference {
  40. print(comparison.differences!.prettyPrintedJSON!)
  41. }
  42. if comparison.resultType != .matching {
  43. print("REPLAY ERROR: Fixed JS didn't match")
  44. }
  45. #expect(comparison.resultType == .matching)
  46. }
  47. func compareDeviations(swiftJson: String, jsJson: String) throws {
  48. // Parse both JSON strings
  49. let swiftData = swiftJson.data(using: .utf8)!
  50. let jsData = jsJson.data(using: .utf8)!
  51. let swiftDict = try JSONSerialization.jsonObject(with: swiftData) as! [String: Any]
  52. let jsDict = try JSONSerialization.jsonObject(with: jsData) as! [String: Any]
  53. // Extract debug info
  54. let swiftDebugInfo = swiftDict["debugInfo"] as! [Any]
  55. let jsDebugInfo = jsDict["debugInfo"] as! [Any]
  56. // Extract deviationsUnsorted arrays
  57. let swiftDeviations = swiftDict["deviationsUnsorted"] as! [Any]
  58. let jsDeviations = jsDict["deviationsUnsorted"] as! [Any]
  59. let combined: [String: Any] = [
  60. "swiftDebugInfo": swiftDebugInfo,
  61. "jsDebugInfo": jsDebugInfo,
  62. "swiftDeviations": swiftDeviations,
  63. "jsDeviations": jsDeviations
  64. ]
  65. let sharedDir = FileManager.default.temporaryDirectory
  66. let outputURL = sharedDir.appendingPathComponent("autosens_debug.json")
  67. let jsonData = try JSONSerialization.data(withJSONObject: combined, options: .prettyPrinted)
  68. try jsonData.write(to: outputURL)
  69. print("Writing debug info to: \(outputURL.path)")
  70. // Convert both to Double arrays
  71. let swiftDoubles = swiftDeviations.compactMap { value -> Double? in
  72. if let number = value as? NSNumber {
  73. return number.doubleValue
  74. }
  75. return nil
  76. }
  77. let jsDoubles = jsDeviations.compactMap { value -> Double? in
  78. if let number = value as? NSNumber {
  79. return number.doubleValue
  80. } else if let string = value as? String {
  81. return Double(string)
  82. }
  83. return nil
  84. }
  85. // Compare the arrays
  86. print("Swift array count: \(swiftDoubles.count)")
  87. print("JS array count: \(jsDoubles.count)")
  88. guard swiftDoubles.count == jsDoubles.count else {
  89. print("Arrays have different lengths!")
  90. let count = max(swiftDoubles.count, jsDoubles.count)
  91. var index = 0
  92. while index < count {
  93. let swiftDouble = index < swiftDoubles.count ? String(swiftDoubles[index]) : "nil"
  94. let jsDouble = index < jsDoubles.count ? String(jsDoubles[index]) : "nil"
  95. print("Index: \(index), Swift: \(swiftDouble), JS: \(jsDouble)")
  96. index += 1
  97. }
  98. return
  99. }
  100. var differences: [(index: Int, swift: Double, js: Double)] = []
  101. for (index, (swiftVal, jsVal)) in zip(swiftDoubles, jsDoubles).enumerated() {
  102. if abs(swiftVal - jsVal) > 0.001 { // Small tolerance for floating point comparison
  103. differences.append((index: index, swift: swiftVal, js: jsVal))
  104. }
  105. }
  106. if differences.isEmpty {
  107. print("✅ Arrays are identical (within tolerance)")
  108. } else {
  109. print("❌ Found \(differences.count) differences:")
  110. for diff in differences {
  111. print(" Index \(diff.index): Swift=\(diff.swift), JS=\(diff.js)")
  112. }
  113. }
  114. }
  115. @Test(
  116. "should produce same results for autosens for fixed JS",
  117. .enabled(if: ReplayTests.enabled)
  118. ) func replayErrorInputs() async throws {
  119. let timezone = ReplayTests.timezone
  120. let files = try await HttpFiles.listFiles()
  121. for filePath in files {
  122. let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
  123. print("Checking \(filePath) @ \(algorithmComparison.createdAt)")
  124. guard timezone == algorithmComparison.timezone else {
  125. continue
  126. }
  127. guard let autosensInputs = algorithmComparison.autosensInput else {
  128. print("Skipping, no autosensInputs found")
  129. if let str = algorithmComparison.comparisonError {
  130. print(str)
  131. }
  132. if let str = algorithmComparison.swiftException {
  133. print(str)
  134. }
  135. continue
  136. }
  137. if IobJsonTests.pumpIsSuspended(history: autosensInputs.history) {
  138. print("Skipping, known issue with JS and currently suspended pumps")
  139. continue
  140. }
  141. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  142. try await checkFixedJsAgainstSwift(autosensInputs: autosensInputs)
  143. print("Checked \(filePath) @ \(algorithmComparison.createdAt)")
  144. timeZoneForTests.resetTimezone()
  145. }
  146. }
  147. @Test("Compare IoB calculation at specific time", .enabled(if: false)) func compareIobAtTime() async throws {
  148. // Hard-code the file and time to investigate
  149. let filePath = "/files/9e146319-5160-482e-9135-f461b97f1a9f.0.json"
  150. let targetClock = Date("2025-09-08T10:42:44.333Z")!
  151. let algorithmComparison = try await HttpFiles.downloadFile(at: filePath)
  152. guard let autosensInputs = algorithmComparison.autosensInput else {
  153. print("No autosensInputs found")
  154. return
  155. }
  156. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  157. let profile = autosensInputs.profile
  158. // Prepare treatments the same way AutosensGenerator does
  159. let swiftTreatments = try IobHistory.calcTempTreatments(
  160. history: autosensInputs.history.map { $0.computedEvent() },
  161. profile: profile,
  162. clock: autosensInputs.clock,
  163. autosens: nil,
  164. zeroTempDuration: nil
  165. )
  166. let encoder = JSONCoding.encoder
  167. var output = try encoder.encode(swiftTreatments)
  168. let sharedDir = FileManager.default.temporaryDirectory
  169. var outputURL = sharedDir.appendingPathComponent("swift_treatments.json")
  170. try output.write(to: outputURL)
  171. print("Writing \(outputURL.path)")
  172. // Set up profile with currentBasal for this time (both Swift and JS autosens do this)
  173. var simulationProfile = profile
  174. simulationProfile.currentBasal = try Basal.basalLookup(autosensInputs.basalProfile, now: targetClock)
  175. simulationProfile.temptargetSet = false
  176. // Calculate Swift IoB at this time
  177. let swiftIob = try IobCalculation.iobTotal(
  178. treatments: swiftTreatments,
  179. profile: simulationProfile,
  180. time: targetClock
  181. )
  182. let openAps = OpenAPSFixed()
  183. let jsTreatmentsRaw = try await openAps.iobHistory(
  184. pumphistory: autosensInputs.history,
  185. profile: try JSONBridge.to(autosensInputs.profile),
  186. clock: autosensInputs.clock,
  187. autosens: RawJSON.null,
  188. zeroTempDuration: RawJSON.null
  189. )
  190. let jsTreatments = try JSONDecoder()
  191. .decode([IobJsonTests.IobHistoryResult].self, from: jsTreatmentsRaw.rawJSON.data(using: .utf8)!)
  192. output = try encoder.encode(jsTreatments)
  193. outputURL = sharedDir.appendingPathComponent("js_treatments.json")
  194. try output.write(to: outputURL)
  195. print("Writing \(outputURL.path)")
  196. print("Swift IoB at \(targetClock):")
  197. print(" iob: \(swiftIob.iob)")
  198. print(" activity: \(swiftIob.activity)")
  199. timeZoneForTests.resetTimezone()
  200. }
  201. @Test("Format autosens inputs for running in JS", .enabled(if: false)) func formatInputs() async throws {
  202. // this test is meant for one-off analysis so it's ok to hard code
  203. // a file, just make sure to _not_ check in updates to this to
  204. // avoid polluting our change logs
  205. let algorithmComparison = try await HttpFiles.downloadFile(at: "/files/2084152d-a95e-4d0e-9254-e0951f7aa519.0.json")
  206. let autosensInputs = algorithmComparison.autosensInput!
  207. let encoder = JSONCoding.encoder
  208. let output = try encoder.encode(autosensInputs)
  209. let sharedDir = FileManager.default.temporaryDirectory
  210. let outputURL = sharedDir.appendingPathComponent("autosens_error_inputs.json")
  211. try output.write(to: outputURL)
  212. // Print the path so you can find it
  213. print("Writing to: \(outputURL.path)")
  214. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  215. let openAps = OpenAPSFixed()
  216. let (autosensResultSwift, _) = OpenAPSSwift.autosense(
  217. glucose: autosensInputs.glucose,
  218. pumpHistory: autosensInputs.history,
  219. basalProfile: autosensInputs.basalProfile,
  220. profile: try JSONBridge.to(autosensInputs.profile),
  221. carbs: autosensInputs.carbs,
  222. tempTargets: autosensInputs.tempTargets,
  223. clock: autosensInputs.clock,
  224. includeDeviationsForTesting: true
  225. )
  226. let autosensResultJavascript = await openAps.autosenseJavascript(
  227. glucose: autosensInputs.glucose,
  228. pumpHistory: autosensInputs.history,
  229. basalprofile: autosensInputs.basalProfile,
  230. profile: try JSONBridge.to(autosensInputs.profile),
  231. carbs: autosensInputs.carbs,
  232. temptargets: autosensInputs.tempTargets,
  233. clock: autosensInputs.clock,
  234. prepareFile: OpenAPSFixed.prepare
  235. )
  236. if case let .success(swiftJson) = autosensResultSwift, case let .success(jsJson) = autosensResultJavascript {
  237. try compareDeviations(swiftJson: swiftJson, jsJson: jsJson)
  238. }
  239. try await checkFixedJsAgainstSwift(autosensInputs: autosensInputs)
  240. timeZoneForTests.resetTimezone()
  241. }
  242. @Test(
  243. "Format autosens inputs for running in JS, 24 hours only",
  244. .enabled(if: false)
  245. ) func formatInputsFixedTime() async throws {
  246. // this test is meant for one-off analysis so it's ok to hard code
  247. // a file, just make sure to _not_ check in updates to this to
  248. // avoid polluting our change logs
  249. let algorithmComparison = try await HttpFiles.downloadFile(at: "/files/2084152d-a95e-4d0e-9254-e0951f7aa519.0.json")
  250. let autosensInputs = algorithmComparison.autosensInput!
  251. // change these variables to switch between 24 and 8 hours
  252. // 288 for 24 hours, 96 for 8 hours
  253. let maxDeviations = 288
  254. // OpenAPSFixed.prepare24 and OpenAPSFixed.prepare8
  255. let prepareFile = OpenAPSFixed.prepare24
  256. let encoder = JSONCoding.encoder
  257. let output = try encoder.encode(autosensInputs)
  258. let sharedDir = FileManager.default.temporaryDirectory
  259. let outputURL = sharedDir.appendingPathComponent("autosens_error_inputs.json")
  260. try output.write(to: outputURL)
  261. // Print the path so you can find it
  262. print("Writing to: \(outputURL.path)")
  263. timeZoneForTests.setTimezone(identifier: algorithmComparison.timezone)
  264. let openAps = OpenAPSFixed()
  265. let glucose = try JSONBridge.glucose(from: autosensInputs.glucose)
  266. let pumpHistory = try JSONBridge.pumpHistory(from: autosensInputs.history)
  267. let basalProfile = try JSONBridge.basalProfile(from: autosensInputs.basalProfile)
  268. let profile = autosensInputs.profile
  269. let carbs = try JSONBridge.carbs(from: autosensInputs.carbs)
  270. let tempTargets = try JSONBridge.tempTargets(from: autosensInputs.tempTargets)
  271. let clock = autosensInputs.clock
  272. let autosensResultSwift = try AutosensGenerator.generate(
  273. glucose: glucose,
  274. pumpHistory: pumpHistory,
  275. basalProfile: basalProfile,
  276. profile: profile,
  277. carbs: carbs,
  278. tempTargets: tempTargets,
  279. maxDeviations: maxDeviations,
  280. clock: clock,
  281. includeDeviationsForTesting: true
  282. )
  283. let autosensResultJavascript = await openAps.autosenseJavascript(
  284. glucose: autosensInputs.glucose,
  285. pumpHistory: autosensInputs.history,
  286. basalprofile: autosensInputs.basalProfile,
  287. profile: try JSONBridge.to(autosensInputs.profile),
  288. carbs: autosensInputs.carbs,
  289. temptargets: autosensInputs.tempTargets,
  290. clock: autosensInputs.clock,
  291. prepareFile: prepareFile
  292. )
  293. if case let .success(jsJson) = autosensResultJavascript {
  294. try compareDeviations(swiftJson: JSONBridge.to(autosensResultSwift), jsJson: jsJson)
  295. }
  296. try await checkFixedJsAgainstSwift(autosensInputs: autosensInputs)
  297. timeZoneForTests.resetTimezone()
  298. }
  299. }