medpython 1.2.0__cp314-cp314-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of medpython might be problematic. Click here for more details.

@@ -0,0 +1,872 @@
1
+ import ctypes, json, traceback, os
2
+ from functools import wraps
3
+ from typing import Any, List, Callable
4
+
5
+
6
+ class SingleDataElement:
7
+ """SingleDataElement object that holds a single data element in AlgoMarker patient data repository."""
8
+
9
+ times: List[int]
10
+ values: List[float]
11
+
12
+ def __init__(self, times: List[int], values: List[float]):
13
+ """SingleDataElement constructor - receives signal name, times and values"""
14
+ self.times = times
15
+ self.values = values
16
+
17
+ def __repr__(self):
18
+ return f"(times={self.times}, values={self.values})"
19
+
20
+
21
+ class AlgoMarker:
22
+ """AlgoMarker object that holds full model pipeline to calculate meaningfull insights from EMR raw data.
23
+
24
+ Methods
25
+ -------
26
+ calculate
27
+ recieves a request for execution of the model pipeline and returns a responde
28
+ discovery
29
+ returns a json specification of the AlgoMarker information, inputs, etc.
30
+ dispose
31
+ Release object memory - recomanded to use "with" statement
32
+ clear_data
33
+ clears AlgoMarker patient data repository memory
34
+ add_data
35
+ loads the AlgoMarker patient data repository memory with patient data
36
+ """
37
+
38
+ def __test_not_disposed(func: Callable) -> Callable:
39
+ @wraps(func)
40
+ def wrapper(*args):
41
+ s_obj = args[0]
42
+ if s_obj.__disposed:
43
+ raise NameError(
44
+ f"Error - Can't call {func.__name__} after algomarker was disposed"
45
+ )
46
+ return func(*args)
47
+
48
+ return wrapper
49
+
50
+ @staticmethod
51
+ def __load_am_lib(libpath: str) -> tuple[ctypes.CDLL, int]:
52
+ api_level = 2
53
+ # Load the shared library into ctypes
54
+ c_lib = ctypes.CDLL(libpath)
55
+ c_lib.AM_API_Create.argtypes = (ctypes.c_int32, ctypes.POINTER(ctypes.c_void_p))
56
+ c_lib.AM_API_Load.argtypes = (ctypes.c_void_p, ctypes.POINTER(ctypes.c_char))
57
+ c_lib.AM_API_Load.restype = ctypes.c_int32
58
+ c_lib.AM_API_DisposeAlgoMarker.argtypes = [ctypes.c_void_p]
59
+ c_lib.AM_API_DisposeAlgoMarker.restype = None
60
+ c_lib.AM_API_ClearData.argtypes = [ctypes.c_void_p]
61
+
62
+ if (
63
+ hasattr(c_lib, "AM_API_AddDataByType")
64
+ and hasattr(c_lib, "AM_API_CalculateByType")
65
+ and hasattr(c_lib, "AM_API_Discovery")
66
+ ):
67
+ c_lib.AM_API_Discovery.argtypes = (
68
+ ctypes.c_void_p,
69
+ ctypes.POINTER(ctypes.c_char_p),
70
+ )
71
+ c_lib.AM_API_Discovery.restype = None
72
+ c_lib.AM_API_AddDataByType.argtypes = (
73
+ ctypes.c_void_p,
74
+ ctypes.c_char_p,
75
+ ctypes.POINTER(ctypes.c_char_p),
76
+ )
77
+ c_lib.AM_API_CalculateByType.argtypes = (
78
+ ctypes.c_void_p,
79
+ ctypes.c_int32,
80
+ ctypes.c_char_p,
81
+ ctypes.POINTER(ctypes.c_char_p),
82
+ )
83
+ c_lib.AM_API_Dispose.argtypes = [ctypes.c_char_p]
84
+ c_lib.AM_API_Dispose.restype = None
85
+ else:
86
+ print(
87
+ "Warning: AM_API_AddDataByType or AM_API_CalculateByType not found in the library, using old API"
88
+ )
89
+ api_level = 1
90
+
91
+ c_lib.AM_API_AddData.argtypes = (
92
+ ctypes.c_void_p,
93
+ ctypes.c_int32,
94
+ ctypes.POINTER(ctypes.c_char),
95
+ ctypes.c_int32,
96
+ ctypes.POINTER(ctypes.c_long),
97
+ ctypes.c_int32,
98
+ ctypes.POINTER(ctypes.c_float),
99
+ )
100
+ c_lib.AM_API_AddData.restype = ctypes.c_int32
101
+ c_lib.AM_API_GetName.argtypes = (
102
+ ctypes.c_void_p,
103
+ ctypes.POINTER(ctypes.c_char_p),
104
+ )
105
+ c_lib.AM_API_GetName.restype = None
106
+
107
+ c_lib.AM_API_CreateRequest.argtypes = (
108
+ ctypes.c_char_p, # Request type
109
+ ctypes.POINTER(ctypes.c_char_p), # Score Type
110
+ ctypes.c_int32,
111
+ ctypes.POINTER(ctypes.c_int32),
112
+ ctypes.POINTER(ctypes.c_long),
113
+ ctypes.c_int32,
114
+ ctypes.POINTER(ctypes.c_void_p),
115
+ )
116
+ c_lib.AM_API_CreateRequest.restype = ctypes.c_int32
117
+ c_lib.AM_API_CreateResponses.argtypes = (
118
+ ctypes.POINTER(ctypes.c_void_p), # AlgoMarker object
119
+ ) # Request type
120
+ c_lib.AM_API_CreateResponses.restype = None
121
+ c_lib.AM_API_DisposeRequest.argtypes = (ctypes.c_void_p,) # Request object
122
+ c_lib.AM_API_DisposeRequest.restype = None
123
+ c_lib.AM_API_DisposeResponses.argtypes = (ctypes.c_void_p,) # Response object
124
+ c_lib.AM_API_DisposeResponses.restype = None
125
+ c_lib.AM_API_Calculate.argtypes = (
126
+ ctypes.c_void_p, # AlgoMarker object
127
+ ctypes.c_void_p, # Request object
128
+ ctypes.c_void_p, # Response json string
129
+ )
130
+ c_lib.AM_API_Calculate.restype = ctypes.c_int32
131
+
132
+ c_lib.AM_API_GetResponsesNum.argtypes = (ctypes.c_void_p,)
133
+ c_lib.AM_API_GetResponsesNum.restype = ctypes.c_int32
134
+
135
+ c_lib.AM_API_GetResponseAtIndex.argtypes = (
136
+ ctypes.c_void_p,
137
+ ctypes.c_int32,
138
+ ctypes.POINTER(ctypes.c_void_p),
139
+ )
140
+ c_lib.AM_API_GetResponseAtIndex.restype = ctypes.c_int32
141
+
142
+ c_lib.AM_API_GetResponseScoresNum.argtypes = (
143
+ ctypes.c_void_p,
144
+ ctypes.POINTER(ctypes.c_int32),
145
+ )
146
+ c_lib.AM_API_GetResponseScoresNum.restype = ctypes.c_int32
147
+
148
+ c_lib.AM_API_GetResponsePoint.argtypes = (
149
+ ctypes.c_void_p, # Response object
150
+ ctypes.POINTER(ctypes.c_int32), # Patient ID
151
+ ctypes.POINTER(ctypes.c_long), # Timestamp
152
+ )
153
+ c_lib.AM_API_GetResponsePoint.restype = ctypes.c_int32
154
+
155
+ c_lib.AM_API_GetResponseMessages.argtypes = (
156
+ ctypes.c_void_p, # Response object
157
+ ctypes.POINTER(ctypes.c_int32), # Number of messages
158
+ ctypes.POINTER(ctypes.POINTER(ctypes.c_int32)), # Message codes
159
+ ctypes.POINTER(ctypes.POINTER(ctypes.c_char_p)), # Messages errors
160
+ )
161
+ c_lib.AM_API_GetResponseMessages.restype = ctypes.c_int32
162
+
163
+ c_lib.AM_API_GetScoreMessages.argtypes = (
164
+ ctypes.c_void_p, # Response object
165
+ ctypes.c_int32, # score_index
166
+ ctypes.POINTER(ctypes.c_int32), # Number of messages
167
+ ctypes.POINTER(ctypes.POINTER(ctypes.c_int32)), # Message codes
168
+ ctypes.POINTER(ctypes.POINTER(ctypes.c_char_p)), # Messages errors
169
+ )
170
+ c_lib.AM_API_GetScoreMessages.restype = ctypes.c_int32
171
+
172
+ c_lib.AM_API_GetResponseScoreByIndex.argtypes = (
173
+ ctypes.c_void_p, # Response object
174
+ ctypes.c_int32, # score_index
175
+ ctypes.POINTER(ctypes.c_float), # Score value
176
+ ctypes.POINTER(ctypes.c_char_p), # Score type
177
+ )
178
+ c_lib.AM_API_GetResponseScoreByIndex.restype = ctypes.c_int32
179
+
180
+ c_lib.AM_API_GetSharedMessages.argtypes = (
181
+ ctypes.c_void_p, # Response object
182
+ ctypes.POINTER(ctypes.c_int32), # Number of messages
183
+ ctypes.POINTER(ctypes.POINTER(ctypes.c_int32)), # Message codes
184
+ ctypes.POINTER(ctypes.POINTER(ctypes.c_char_p)), # Messages errors
185
+ )
186
+ c_lib.AM_API_GetSharedMessages.restype = ctypes.c_int32
187
+
188
+ return c_lib, api_level
189
+
190
+ @staticmethod
191
+ def create_request_json(patient_id: int, prediction_time: int) -> str:
192
+ """Creates and returns a string json request for patient_id and prediction_time"""
193
+ js_req = (
194
+ '{"type": "request", "request_id": "REQ_ID_1234", '
195
+ + '"export": {"prediction": "pred_0"}, "requests": [ '
196
+ + '{"patient_id":"%d", "time": "%d"} ]}'
197
+ % (int(patient_id), int(prediction_time))
198
+ )
199
+ return js_req
200
+
201
+ def __init__(self, amconfig_path: str, libpath: str | None = None):
202
+ """AlgoMarker constractor - receives AlgoMarker configuration file path "amconfig".
203
+ Optional path to C shared library file. If we want to use other version, not default
204
+ library that is packed in this module.
205
+ """
206
+ if libpath is None:
207
+ libpath = os.path.join(
208
+ os.path.dirname(os.path.abspath(__file__)), "libdyn_AlgoMarker.so"
209
+ )
210
+ self.__lib = None
211
+ self.__lib, self.api_version = AlgoMarker.__load_am_lib(libpath)
212
+ self.__libpath = libpath
213
+ print(f"Loaded library from {self.__libpath}")
214
+ self.__obj = ctypes.c_void_p()
215
+ res = self.__lib.AM_API_Create(1, ctypes.pointer(self.__obj))
216
+ if res != 0:
217
+ print("Error in creating AlgoMarker object")
218
+ self.__disposed = False
219
+ self.__name = None
220
+ self.__amconfig_path = amconfig_path
221
+ self.__load_algomarker(amconfig_path)
222
+ self.__discovery_full = None
223
+
224
+ def __parse_config_amfile(self):
225
+ if self.__discovery_full is not None:
226
+ return self.__discovery_full
227
+ self.__discovery_full = self.get_name()
228
+ self.__discovery_full["version"] = ""
229
+ with open(self.__amconfig_path, "r") as f:
230
+ lines = f.readlines()
231
+ lines = list(
232
+ filter(lambda x: x.strip() != "" or not (x.strip().startswith("#")), lines)
233
+ )
234
+ rep_path = list(filter(lambda x: x.startswith("REPOSITORY\t"),lines))
235
+ if len(rep_path)==1:
236
+ rep_path = rep_path[0].split("\t")[1].strip()
237
+ # Read rep_path - relative to current amconfig:
238
+ if not os.path.isabs(rep_path):
239
+ rep_path = os.path.join(os.path.dirname(self.__amconfig_path), rep_path)
240
+ else:
241
+ rep_path = None
242
+
243
+ if rep_path is None:
244
+ return self.__discovery_full
245
+
246
+ with open(rep_path, "r") as f:
247
+ lines_rep = f.readlines()
248
+ signals = list(filter(lambda x: x.startswith("SIGNAL") ,lines_rep))
249
+ signals = list(map(lambda x: x.split("\t")[1].strip() ,signals))
250
+ dicts_lines = list(filter(lambda x: x.startswith("DICTIONARY") ,lines_rep))
251
+ dicts_lines = list(map(lambda x: x.split("\t")[1].strip() ,dicts_lines))
252
+ if len(signals) ==0:
253
+ return self.__discovery_full
254
+ signals = signals[0]
255
+ if not os.path.isabs(signals):
256
+ signals = os.path.join(os.path.dirname(rep_path), signals)
257
+ with open(signals, "r") as f:
258
+ lines_signals = f.readlines()
259
+ sigs_in_signals = list(filter(lambda x: x.startswith("SIGNAL") ,lines_signals))
260
+ self.__discovery_full["signals"] = []
261
+ for sig in sigs_in_signals:
262
+ tokens = sig.split("\t")
263
+ sig_name = tokens[1].strip()
264
+ unit = "" if len(tokens) < 7 else tokens[6].strip()
265
+ sig_type = tokens[3].strip()
266
+ sig_categ = [] if len(tokens) < 6 else tokens[5].strip().split(",")
267
+ if sig_type == "0":
268
+ sig_type = "V(f)"
269
+ elif sig_type == "1":
270
+ sig_type = "T(i),V(f)"
271
+ elif sig_type == "2":
272
+ sig_type = "T(l),V(f),p,p,p,p"
273
+ elif sig_type == "3":
274
+ sig_type = "T(i,i),V(f)"
275
+ elif sig_type == "4":
276
+ sig_type = "T(l)"
277
+ elif sig_type == "5":
278
+ sig_type = "T(l,l),V(f),p,p,p,p"
279
+ elif sig_type == "6":
280
+ sig_type = "T(i),V(f,us),p,p"
281
+ elif sig_type == "7":
282
+ sig_type = "T(l),V(l)"
283
+ elif sig_type == "8":
284
+ sig_type = "T(i),V(s,s)"
285
+ elif sig_type == "9":
286
+ sig_type = "V(s,s)"
287
+ elif sig_type == "10":
288
+ sig_type = "V(s,s,s,s)"
289
+ elif sig_type == "11":
290
+ sig_type = "T(us),V(us)"
291
+ elif sig_type == "12":
292
+ sig_type = "T(i,i),V(f,f)"
293
+ elif sig_type == "13":
294
+ sig_type = "T(i),V(f,f)"
295
+ elif sig_type == "14":
296
+ sig_type = "T(l,l)"
297
+ elif sig_type == "15":
298
+ sig_type = "T(i),p,p,p,p,V(s,s,s,s)"
299
+ sig_block : dict[str, Any] = {
300
+ "code": sig_name,
301
+ "unit": unit,
302
+ "type": sig_type,
303
+ }
304
+ if len(sig_categ) > 0:
305
+ sig_block["categorical_channels"] = sig_categ
306
+ # Try to fetch category values and populate:
307
+ if len(list(filter(lambda x: x > "0", sig_categ))) > 0:
308
+ pass
309
+ for d in dicts_lines:
310
+ if not os.path.isabs(d):
311
+ d = os.path.join(os.path.dirname(rep_path), d)
312
+ with open(d, "r") as f:
313
+ lines_dict = f.readlines()
314
+ section = list(filter(lambda x: x.startswith("SECTION"),lines_dict))
315
+ if len(section) == 0:
316
+ continue
317
+ section = section[0]
318
+ all_sigs = section.split("\t")[1].strip().split(",")
319
+ if sig_name not in all_sigs:
320
+ continue
321
+ lines_dict = list(filter(lambda x: x.startswith("DEF"),lines_dict))
322
+ # Add this dict only if less than 10 categories:
323
+ full_dict = {}
324
+ for line in lines_dict:
325
+ tokens = line.split("\t")
326
+ id = tokens[1]
327
+ val = tokens[2].strip()
328
+ if id not in full_dict or len(val) > len(full_dict[id]):
329
+ full_dict[id] = val
330
+ if len(full_dict) < 10:
331
+ sig_block["categorical_values"] = []
332
+ for id, val in full_dict.items():
333
+ sig_block["categorical_values"].append(val)
334
+ else:
335
+ sig_block["categorical_values"] = []
336
+
337
+ #sig_block["categorical_values"]
338
+
339
+ self.__discovery_full["signals"].append(sig_block)
340
+
341
+
342
+
343
+ return self.__discovery_full
344
+
345
+ def __load_algomarker(self, amconfig_path: str):
346
+ if not (os.path.exists(amconfig_path)):
347
+ raise NameError(
348
+ f'amconfig path "{amconfig_path}" not found. File Not Found'
349
+ )
350
+ assert self.__lib is not None
351
+ am_path = ctypes.create_string_buffer(amconfig_path.encode("ascii"))
352
+ res = self.__lib.AM_API_Load(self.__obj, am_path)
353
+
354
+ if res != 0:
355
+ raise NameError(f"Error in loading AlgoMarker: {res}")
356
+ else:
357
+ try:
358
+ info_js = self.discovery()
359
+ if "name" in info_js:
360
+ self.__name = info_js["name"]
361
+ print(f"Loaded {self.__name} AlgoMarker succefully")
362
+ except:
363
+ print("Warning: couldn't retrieve AlgoMarker Name")
364
+
365
+ def __repr__(self):
366
+ if self.__disposed:
367
+ return f"AlgoMarker was loaded with library {self.__libpath} and amconfig {self.__amconfig_path}, but disposed!"
368
+ if self.__name is not None:
369
+ return f"AlgoMarker {self.__name} was loaded with library {self.__libpath} and amconfig {self.__amconfig_path}"
370
+ else:
371
+ return f"AlgoMarker was loaded with library {self.__libpath} and amcofig {self.__amconfig_path}"
372
+
373
+ def dispose(self):
374
+ """Disposes the AlgoMarker object and frees the memory"""
375
+ if self.__lib is not None:
376
+ self.__lib.AM_API_DisposeAlgoMarker(self.__obj)
377
+ self.__disposed = True
378
+ if self.__name is None:
379
+ print("Released AlgoMarker object")
380
+ else:
381
+ print(f'Released "{self.__name}" AlgoMarker object')
382
+ self.__lib = None
383
+ self.__obj = None
384
+
385
+ def __del__(self):
386
+ self.dispose()
387
+
388
+ def __enter__(self):
389
+ return self
390
+
391
+ def __exit__(self, exc_type, exc_value, exc_traceback):
392
+ self.dispose()
393
+
394
+ @__test_not_disposed
395
+ def __dispose_string_mem(self, obj):
396
+ assert self.__lib is not None
397
+ self.__lib.AM_API_Dispose(obj)
398
+
399
+ @__test_not_disposed
400
+ def get_name(self) -> dict[str, Any]:
401
+ """Returns information about the Algomarkers in json format - input signals, name, version, etc."""
402
+ assert self.__lib is not None
403
+ res_name = ctypes.c_char_p()
404
+ self.__lib.AM_API_GetName(self.__obj, ctypes.byref(res_name))
405
+ try:
406
+ if res_name.value is None:
407
+ raise NameError("Error in getting AlgoMarker name - name is None")
408
+ res_discovery_str = res_name.value.decode("ascii")
409
+ # Clear memory:
410
+ res_discovery_str = {"name": res_discovery_str}
411
+ return res_discovery_str
412
+ except:
413
+ print("Error in discovery json conversion")
414
+ traceback.print_exc()
415
+ raise
416
+
417
+ @__test_not_disposed
418
+ def discovery(self) -> dict[str, Any]:
419
+ """Returns information about the Algomarkers in json format - input signals, name, version, etc."""
420
+ assert self.__lib is not None
421
+ if self.api_version == 1:
422
+ js_res = self.__parse_config_amfile()
423
+ return js_res
424
+ res_discovery = ctypes.c_char_p()
425
+ self.__lib.AM_API_Discovery(self.__obj, ctypes.byref(res_discovery))
426
+ try:
427
+ res_discovery_str = res_discovery.value
428
+ # Clear memory:
429
+ self.__dispose_string_mem(res_discovery)
430
+ if res_discovery_str is None:
431
+ raise NameError(
432
+ "Error in getting AlgoMarker discovery - discovery is None"
433
+ )
434
+ res_discovery_str = json.loads(res_discovery_str)
435
+ return res_discovery_str
436
+ except:
437
+ print("Error in discovery json conversion")
438
+ traceback.print_exc()
439
+ raise
440
+
441
+ @__test_not_disposed
442
+ def clear_data(self):
443
+ """Frees the algomarker patient data repository"""
444
+ assert self.__lib is not None
445
+ res = self.__lib.AM_API_ClearData(self.__obj)
446
+ if res != 0:
447
+ raise NameError(f"Error in clearing data - error code {res}")
448
+
449
+ @__test_not_disposed
450
+ def add_data_simple(
451
+ self, patient_id: int, signal_name: str, data: List[SingleDataElement]
452
+ ) -> list[str]:
453
+ assert self.__lib is not None
454
+ flat_times = []
455
+ flat_values = []
456
+ times_size = None
457
+ values_size = None
458
+ messages = []
459
+ for elem in data:
460
+ flat_times.extend(elem.times)
461
+ flat_values.extend(elem.values)
462
+ if times_size is None:
463
+ times_size = len(elem.times)
464
+ if values_size is None:
465
+ values_size = len(elem.values)
466
+ if len(elem.times) != times_size:
467
+ raise ValueError(
468
+ f"Error in add_data_simple - all times must have the same size, but got {len(elem.times)} != {times_size}"
469
+ )
470
+ if len(elem.values) != values_size:
471
+ raise ValueError(
472
+ f"Error in add_data_simple - all values must have the same size, but got {len(elem.values)} != {values_size}"
473
+ )
474
+
475
+ # Convert to ctypes arrays
476
+ c_times = (ctypes.c_long * len(flat_times))(*flat_times)
477
+ c_values = (ctypes.c_float * len(flat_values))(*flat_values)
478
+ res = self.__lib.AM_API_AddData(
479
+ self.__obj,
480
+ patient_id,
481
+ ctypes.create_string_buffer(signal_name.encode("ascii")),
482
+ len(flat_times),
483
+ c_times,
484
+ len(flat_values),
485
+ c_values,
486
+ )
487
+ if res != 0:
488
+ msg = f"Error in add_data_simple - error code {res} for patient_id {patient_id}, signal_name {signal_name}, data: {elem}"
489
+ print(f"Error in add_data_simple - error code {res} for a patient more details in response message")
490
+ messages.append(msg)
491
+ # No return value, errors are handled by the library
492
+ return messages
493
+
494
+ @__test_not_disposed
495
+ def __add_data_old_api(self, json_data: str) -> list[str]:
496
+ """This function recieves data json object and loads the data into the algomarker patient data repository.
497
+ Errors are collected in a string - each error in separate line. When there are no errors, the output is None.
498
+
499
+ Notes
500
+ -----
501
+ The input data json request is documented in different document and the potential errors
502
+ """
503
+
504
+ """
505
+ """
506
+ js_req = json.loads(json_data) # Check if the json is valid
507
+
508
+ pid = int(js_req["patient_id"])
509
+ sigs_data = js_req["signals"]
510
+ all_data = []
511
+ messages = []
512
+ for sig_eme in sigs_data:
513
+ sig_name = sig_eme["code"]
514
+ data = sig_eme["data"]
515
+ all_data = []
516
+ for elem in data:
517
+ if "timestamp" not in elem or "value" not in elem:
518
+ raise ValueError(
519
+ f"Error in data json - each signal must have 'timestamp' and 'value' fields, but got {elem}"
520
+ )
521
+ timestamps = list(map(lambda x: int(x), elem["timestamp"]))
522
+ # AddDataStr for categorical signals is not supported right now. In current algomarkersm, there are not categorical signals
523
+ values = list(map(lambda x: float(x), elem["value"]))
524
+ sig_data = SingleDataElement(timestamps, values)
525
+ all_data.append(sig_data)
526
+ res = self.add_data_simple(pid, sig_name, all_data)
527
+ messages.extend(res)
528
+ return messages
529
+
530
+ @__test_not_disposed
531
+ def add_data(self, json_data: str) -> str | None:
532
+ """This function recieves data json object and loads the data into the algomarker patient data repository.
533
+ Errors are collected in a string - each error in separate line. When there are no errors, the output is None.
534
+
535
+ Notes
536
+ -----
537
+ The input data json request is documented in different document and the potential errors
538
+ """
539
+ assert self.__lib is not None
540
+ if self.api_version == 1:
541
+ res = self.__add_data_old_api(json_data)
542
+ if len(res) > 0:
543
+ return "\n".join(res)
544
+ else:
545
+ return None
546
+ # For new API
547
+ js_data = ctypes.create_string_buffer(json_data.encode("ascii"))
548
+ res_messages = ctypes.c_char_p()
549
+ res = self.__lib.AM_API_AddDataByType(
550
+ self.__obj, js_data, ctypes.byref(res_messages)
551
+ )
552
+ if res != 0:
553
+ print(f"AddData Failed {res}, messages ")
554
+ res_messages_str = res_messages.value
555
+ self.__dispose_string_mem(res_messages)
556
+ res_messages_str_val = ""
557
+ if res_messages_str is not None:
558
+ res_messages_str_val = res_messages_str.decode("ascii")
559
+ print(res_messages_str_val)
560
+ return res_messages_str_val
561
+ return None
562
+
563
+ @__test_not_disposed
564
+ def __calculate_old_api(self, request_json: str) -> dict[str, Any]:
565
+ """Recieved json request for calculation and returns json string responde object with the result
566
+
567
+ Notes
568
+ -----
569
+ The input json request and json response results are documented in a different document
570
+ """
571
+ assert self.__lib is not None
572
+ # 1. Create Request Object:
573
+ js_req = json.loads(request_json) # Check if the json is valid
574
+ assert (
575
+ js_req["type"] == "request"
576
+ ) # "Error in request json - type must be 'request'"
577
+ request_type = ctypes.byref(ctypes.c_char_p(b"Raw")) # Default request type
578
+ requests = js_req["requests"]
579
+ load_data = js_req.get("load", 0)
580
+ pids = []
581
+ times = []
582
+ load_err_msgs = []
583
+ for req in requests:
584
+ if "patient_id" not in req or "time" not in req:
585
+ raise ValueError(
586
+ "Error in request json - each request must have patient_id and time"
587
+ )
588
+ patient_id = int(req["patient_id"])
589
+ time = int(req["time"])
590
+ pids.append(patient_id)
591
+ times.append(time)
592
+ if load_data:
593
+ if "data" not in req or "signals" not in req["data"]:
594
+ raise ValueError(
595
+ "Error in request json - when load is true, each request must have 'data' with 'signals'"
596
+ )
597
+ load_res = self.add_data(
598
+ json.dumps(
599
+ {"signals": req["data"]["signals"], "patient_id": patient_id}
600
+ )
601
+ )
602
+ if load_res is not None:
603
+ load_err_msgs.extend(load_res.split("\n"))
604
+
605
+ full_response = {
606
+ "type": "response",
607
+ "responses": [],
608
+ "request_id": js_req["request_id"],
609
+ }
610
+ if len(load_err_msgs) > 0:
611
+ full_response["errors"] = load_err_msgs
612
+ return full_response
613
+ # Convert to ctypes arrays
614
+ c_pids = (ctypes.c_int32 * len(pids))(*pids)
615
+ c_times = (ctypes.c_long * len(times))(*times)
616
+ req_object = ctypes.c_void_p()
617
+ self.__lib.AM_API_CreateRequest(
618
+ ctypes.create_string_buffer(js_req["request_id"].encode("ascii")),
619
+ request_type,
620
+ 1,
621
+ c_pids,
622
+ c_times,
623
+ len(pids),
624
+ ctypes.byref(req_object),
625
+ )
626
+
627
+ # 2. Create response object
628
+ response_object = ctypes.c_void_p()
629
+ self.__lib.AM_API_CreateResponses(ctypes.byref(response_object))
630
+
631
+ # 3. Call the Calculate function
632
+ # res_resp = ctypes.c_char_p()
633
+ res = self.__lib.AM_API_Calculate(self.__obj, req_object, response_object)
634
+ if res != 0:
635
+ print(f"Error in Calculate - error code {res}")
636
+
637
+ # 4. Check the result
638
+ n_resp = self.__lib.AM_API_GetResponsesNum(response_object)
639
+ print(f"Has {n_resp} responses")
640
+ # AM_API_GetSharedMessages(resp, &n_msgs, &msg_codes, &msgs_errs);
641
+ n_msgs = ctypes.c_int32()
642
+ msg_codes = ctypes.POINTER(ctypes.c_int32)()
643
+ msgs_errs = ctypes.POINTER(ctypes.c_char_p)()
644
+ res = self.__lib.AM_API_GetSharedMessages(
645
+ response_object,
646
+ ctypes.byref(n_msgs), # Number of messages
647
+ ctypes.byref(msg_codes), # Message codes
648
+ ctypes.byref(msgs_errs), # Messages errors
649
+ )
650
+ if res != 0:
651
+ print(f"Error in AM_API_GetSharedMessages - error code {res}")
652
+ full_response["errors"] = [
653
+ f"Error in AM_API_GetSharedMessages - error code {res}"
654
+ ]
655
+ n_msgs = n_msgs.value
656
+ print(f"Response has {n_msgs} shared messages")
657
+ for i in range(n_msgs):
658
+ msg_code = msg_codes[i]
659
+ msg_err = msgs_errs[i].decode("ascii") if msgs_errs else "None"
660
+ if "errors" not in full_response:
661
+ full_response["errors"] = []
662
+ full_response["errors"].append(f"({msg_code}){msg_err}")
663
+ print(f"Message {i}: Code: {msg_code}, Error: {msg_err}")
664
+
665
+ for i in range(n_resp):
666
+ # AM_API_GetResponseAtIndex(response_object, i, &response);
667
+ # We would normally retrieve the response data here, but the old API does not provide a way to do this.
668
+ # Here we would normally retrieve the response data, but the old API does not provide a way to do this.
669
+ # We would need to implement the necessary functions in the C library to retrieve the response data.
670
+ # For example:
671
+ curr_resp_obj = ctypes.c_void_p()
672
+ curr_num = ctypes.c_int32()
673
+ # AM_API_GetResponseAtIndex(response_object, i, &response);
674
+ res = self.__lib.AM_API_GetResponseAtIndex(
675
+ response_object, i, ctypes.byref(curr_resp_obj)
676
+ )
677
+ if res != 0:
678
+ print(f"Error in fetch response {i} - error code {res}")
679
+ # AM_API_GetResponseScoresNum(response, &n_scores);
680
+ res = self.__lib.AM_API_GetResponseScoresNum(
681
+ curr_resp_obj, ctypes.byref(curr_num)
682
+ )
683
+ if res != 0:
684
+ print(f"Error in AM_API_GetResponseScoresNum {i} - error code {res}")
685
+ curr_num = curr_num.value
686
+ print(f"Has {curr_num} scores in response {i}")
687
+ # AM_API_GetResponsePoint(response, &pid, &ts);
688
+ pid = ctypes.c_int32()
689
+ ts = ctypes.c_long()
690
+ res = self.__lib.AM_API_GetResponsePoint(
691
+ curr_resp_obj, ctypes.byref(pid), ctypes.byref(ts)
692
+ )
693
+ if res != 0:
694
+ print(f"Error in AM_API_GetResponsePoint {i} - error code {res}")
695
+ pid = pid.value
696
+ ts = ts.value
697
+ print(f"Response {i} - Patient ID: {pid}, Timestamp: {ts}")
698
+ n_msgs = ctypes.c_int32()
699
+ msg_codes = ctypes.POINTER(ctypes.c_int32)()
700
+ msgs_errs = ctypes.POINTER(ctypes.c_char_p)()
701
+ # AM_API_GetResponseMessages(response, &n_msgs, &msg_codes, &msgs_errs);
702
+ res = self.__lib.AM_API_GetResponseMessages(
703
+ curr_resp_obj,
704
+ ctypes.byref(n_msgs), # Number of messages
705
+ ctypes.byref(msg_codes), # Message codes
706
+ ctypes.byref(msgs_errs), # Messages errors
707
+ )
708
+ if res != 0:
709
+ print(f"Error in AM_API_GetResponseMessages {i} - error code {res}")
710
+ n_msgs = n_msgs.value
711
+ js_resp = {
712
+ "patient_id": pid,
713
+ "time": ts,
714
+ "prediction": -9999,
715
+ "messages": [],
716
+ }
717
+
718
+ print(f"Response {i} has {n_msgs} messages")
719
+ for j in range(n_msgs):
720
+ msg_code = msg_codes[j]
721
+ msg_err = msgs_errs[j].decode("ascii") if msgs_errs else "None"
722
+ js_resp["messages"].append(f"({msg_code}){msg_err}")
723
+ print(f"Message {j}: Code: {msg_code}, Error: {msg_err}")
724
+ # AM_API_GetScoreMessages
725
+ for j in range(curr_num):
726
+ res = self.__lib.AM_API_GetScoreMessages(
727
+ curr_resp_obj,
728
+ j, # Assuming we want the first score messages
729
+ ctypes.byref(ctypes.c_int32(n_msgs)), # Number of messages
730
+ ctypes.byref(msg_codes), # Message codes
731
+ ctypes.byref(msgs_errs), # Messages errors
732
+ )
733
+ if res != 0:
734
+ print(
735
+ f"Error in AM_API_GetScoreMessages {i} {j} - error code {res}"
736
+ )
737
+ # resp_rc = AM_API_GetResponseScoreByIndex(response, 0, &_scr, &_scr_type);
738
+ scr_value: ctypes.c_float = ctypes.c_float()
739
+ scr_type: ctypes.c_char_p = ctypes.c_char_p()
740
+ for j in range(curr_num):
741
+ res = self.__lib.AM_API_GetResponseScoreByIndex(
742
+ curr_resp_obj, j, ctypes.byref(scr_value), ctypes.byref(scr_type)
743
+ )
744
+ if res != 0:
745
+ print(
746
+ f"Error in AM_API_GetResponseScoreByIndex {i} - error code {res}"
747
+ )
748
+ scr_value_v = scr_value.value
749
+ scr_type_v = None
750
+ if scr_type.value is not None:
751
+ scr_type_v = scr_type.value.decode("ascii") if scr_type else "None"
752
+ print(
753
+ f"Response {i} Score {j}: Value: {scr_value_v}, Type: {scr_type_v}"
754
+ )
755
+ # Take the right index from exports - currently only 'pred_0' is supported for sigle pred score
756
+
757
+ js_resp["prediction"] = scr_value_v
758
+ full_response["responses"].append(js_resp)
759
+
760
+ # 5. Dispose request and response objects
761
+ self.__lib.AM_API_DisposeRequest(req_object)
762
+ self.__lib.AM_API_DisposeResponses(response_object)
763
+ return full_response
764
+
765
+ @__test_not_disposed
766
+ def calculate(self, request_json: str) -> dict[str, Any]:
767
+ """Recieved json request for calculation and returns json string responde object with the result
768
+
769
+ Notes
770
+ -----
771
+ The input json request and json response results are documented in a different document
772
+ """
773
+ assert self.__lib is not None
774
+ if self.api_version == 1:
775
+ return self.__calculate_old_api(request_json)
776
+ js_req = ctypes.create_string_buffer(request_json.encode("ascii"))
777
+ res_resp = ctypes.c_char_p()
778
+ res = self.__lib.AM_API_CalculateByType(
779
+ self.__obj, 3001, js_req, ctypes.byref(res_resp)
780
+ )
781
+ if res != 0:
782
+ print(f"Calculate Failed {res}")
783
+ try:
784
+ res_resp_str = res_resp.value
785
+ self.__dispose_string_mem(res_resp)
786
+ if res_resp_str is None:
787
+ raise NameError("Error in Calculate - response is None")
788
+ res_resp_str = json.loads(res_resp_str)
789
+ return res_resp_str
790
+ except:
791
+ print("Error in converting respond json in calculate")
792
+ traceback.print_exc()
793
+ raise
794
+
795
+
796
+ # Old API testing
797
+ # bdate=(ctypes.c_long * 1)(*[1988])
798
+ # bdate_right=(ctypes.c_float * 1)(*[19880327])
799
+ # am.lib.AM_API_AddData(am.obj,1,ctypes.create_string_buffer(b"BDATE"),1, bdate,0 ,ctypes.POINTER(ctypes.c_float)())
800
+ # am.lib.AM_API_AddData(am.obj,1,ctypes.create_string_buffer(b"BDATE"),0, ctypes.POINTER(ctypes.c_long)(),1 ,bdate_right)
801
+
802
+ if __name__ == "__main__":
803
+ print(
804
+ "This is a module for AlgoMarker Python API. Use it as a module, not as a script."
805
+ )
806
+ print("Example usage:")
807
+ AlgoMarker_path = os.path.join(
808
+ os.environ["HOME"],
809
+ "Documents/MES/AlgoMarkers/AM_LGI/AlgoMarker/ColonFlag_3.1.0.0/ColonFlag-3.1.amconfig",
810
+ # "Documents/MES/AlgoMarkers/docker_images/LGI-Flag-ButWhy-3.1.2-Scorer/data/app/LGI-Flag-ButWhy-3.1.2-Scorer/LGI-ColonFlag-3.1.amconfig"
811
+ )
812
+ libpath = None
813
+ libpath = os.path.join(
814
+ os.environ["HOME"],
815
+ "Documents/MES/AlgoMarkers/AM_LGI/AlgoMarker/ColonFlag_3.1.0.0/libdyn_AlgoMarker.25102018_1.so",
816
+ # "Documents/MES/AlgoMarkers/docker_images/LGI-Flag-ButWhy-3.1.2-Scorer/data/app/LGI-Flag-ButWhy-3.1.2-Scorer/lib/libdyn_AlgoMarker.so"
817
+ )
818
+ request_json = AlgoMarker.create_request_json(1, 20240101)
819
+ with AlgoMarker(AlgoMarker_path, libpath) as am:
820
+ print(am.discovery())
821
+ am.clear_data()
822
+ am.add_data_simple(1, "BYEAR", [SingleDataElement([], [1978])])
823
+ am.add_data_simple(1, "GENDER", [SingleDataElement([], [1])])
824
+ am.add_data_simple(
825
+ 1,
826
+ "Hemoglobin",
827
+ [
828
+ SingleDataElement([20220101], [14.5]),
829
+ SingleDataElement([20230101], [14.5]),
830
+ SingleDataElement([20240101], [14.5]),
831
+ ],
832
+ )
833
+ am.add_data_simple(
834
+ 1,
835
+ "Hematocrit",
836
+ [
837
+ SingleDataElement([20220101], [33]),
838
+ SingleDataElement([20230101], [33]),
839
+ SingleDataElement([20240101], [33]),
840
+ ],
841
+ )
842
+ am.add_data_simple(
843
+ 1,
844
+ "MCH",
845
+ [
846
+ SingleDataElement([20220101], [33]),
847
+ SingleDataElement([20230101], [33]),
848
+ SingleDataElement([20240101], [33]),
849
+ ],
850
+ )
851
+ am.add_data_simple(
852
+ 1,
853
+ "RBC",
854
+ [
855
+ SingleDataElement([20220101], [4.5]),
856
+ SingleDataElement([20230101], [4.5]),
857
+ SingleDataElement([20240101], [4.5]),
858
+ ],
859
+ )
860
+ am.add_data_simple(
861
+ 1,
862
+ "MCV",
863
+ [
864
+ SingleDataElement([20220101], [90]),
865
+ SingleDataElement([20230101], [90]),
866
+ SingleDataElement([20240101], [90]),
867
+ ],
868
+ )
869
+ resp = am.calculate(request_json)
870
+ print("Response:")
871
+ print(resp)
872
+ print("Done with AlgoMarker example")