AutosensJsonTests.swift 14 KB

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