SleePyPhases 0.7.0__tar.gz → 0.8.2__tar.gz

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.
Files changed (63) hide show
  1. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/PKG-INFO +1 -1
  2. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/EventScorer.py +2 -2
  3. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/FeatureExtraction.py +1 -1
  4. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/Plugin.py +3 -2
  5. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/SleePyPhases.py +2 -2
  6. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/SleepMetaData.py +7 -3
  7. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/Eval.py +6 -15
  8. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/EvalReport.py +21 -12
  9. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/Extract.py +30 -6
  10. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/ExtractFeatures.py +100 -12
  11. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/Predict.py +8 -5
  12. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/Setup.py +25 -2
  13. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/ThresholdOptimisation.py +6 -5
  14. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/Training.py +33 -5
  15. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/Validation.py +1 -1
  16. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases.egg-info/PKG-INFO +1 -1
  17. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/setup.py +1 -1
  18. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_Extract.py +1 -0
  19. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/LICENSE +0 -0
  20. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/README.md +0 -0
  21. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/DataManipulation.py +0 -0
  22. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/MultiScorer.py +0 -0
  23. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/Plot.py +0 -0
  24. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/PreManipulation.py +0 -0
  25. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/RecordFeatureExtraction.py +0 -0
  26. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/Reporter.py +0 -0
  27. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/SignalPreprocessing.py +0 -0
  28. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/__init__.py +0 -0
  29. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/config.yaml +0 -0
  30. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/BuildDataset.py +0 -0
  31. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/DataAnalysis.py +0 -0
  32. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/DataReport.py +0 -0
  33. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/EvalPlotExamples.py +0 -0
  34. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/ExportMetadataToMeticalDB.py +0 -0
  35. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/ExtractEvents.py +0 -0
  36. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/ExtractRecordFeatures.py +0 -0
  37. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/GatherMetadata.py +0 -0
  38. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/LogCleanup.py +0 -0
  39. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/LogOverview.py +0 -0
  40. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/TestRun.py +0 -0
  41. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/VisualizeConfig.py +0 -0
  42. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/__init__.py +0 -0
  43. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases.egg-info/SOURCES.txt +0 -0
  44. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases.egg-info/dependency_links.txt +0 -0
  45. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases.egg-info/requires.txt +0 -0
  46. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases.egg-info/top_level.txt +0 -0
  47. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/pyproject.toml +0 -0
  48. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/setup.cfg +0 -0
  49. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/__init__.py +0 -0
  50. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/conftest.py +0 -0
  51. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/__init__.py +0 -0
  52. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_BuildDataset.py +0 -0
  53. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_BuildDatasetFolded.py +0 -0
  54. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_BuildDatasetMetadataSync.py +0 -0
  55. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_BuildDatasetSegments.py +0 -0
  56. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_DataManipulation.py +0 -0
  57. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_Eval.py +0 -0
  58. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_EventScorer.py +0 -0
  59. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_PreManipulation.py +0 -0
  60. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_RecordFeatureExtraction.py +0 -0
  61. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_RecordProcessor.py +0 -0
  62. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_Setup.py +0 -0
  63. {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_SignalPreprocessing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SleePyPhases
3
- Version: 0.7.0
3
+ Version: 0.8.2
4
4
  Summary: A framwork for creating deep learning pipelines for sleep data
5
5
  Home-page: https://gitlab.com/sleep-is-all-you-need/sleepyphases
6
6
  Author: Franz Ehrlich
@@ -7,8 +7,8 @@ from SleepHarmonizer.PSGEventManager import PSGEventManager
7
7
  class EventScorer(Scorer):
8
8
  title = "Eventbasiert-Tolerant"
9
9
 
10
- def __init__(self, numClasses=None, classNames=None, trace=False) -> None:
11
- super().__init__(numClasses=numClasses, classNames=classNames, trace=trace)
10
+ def __init__(self, numClasses=None, classNames=None, trace=False, multilabel=False) -> None:
11
+ super().__init__(numClasses=numClasses, classNames=classNames, trace=trace, multilabel=multilabel)
12
12
  self.metrics = ["kappa", "accuracy"]
13
13
  self.majorityVote = False
14
14
 
@@ -84,7 +84,7 @@ class FeatureExtraction(pyPhasesFeatureExtraction, Swappable):
84
84
  array, _ = da((array, None))
85
85
  array = array.transpose(2, 1, 0)
86
86
 
87
- features = model.predict(array, get_likelihood=True, returnNumpy=True)
87
+ features = model.predict(array, returnNumpy=True)
88
88
  _, features = da.restoreLength(None, features, length=segmentSignal.getShape()[1])
89
89
  features = features[:, :, 1]
90
90
 
@@ -33,7 +33,7 @@ class Plugin(PluginAdapter):
33
33
  "Setup": (Setup, ["dataversionmanager"]),
34
34
  "Extract": (Extract, ["removedRecordIds", "data-processed", "data-features", "record-preprocessed"]),
35
35
  "BuildDataset": (BuildDataset, ["dataset-training", "dataset-validation", "dataset-test", "dataset-bySplit", "record-data"]),
36
- "Training": (Training, ["modelState", "modelStateConfig"]),
36
+ "Training": (Training, ["modelState", "modelStateConfig", "trainedModel"]),
37
37
  "Validation": (Validation, []),
38
38
  "ThresholdOptimisation": (ThresholdOptimisation, ["threshold", "validationResult"]),
39
39
  "Eval": (Eval, ["evalResults", "eventResults"]),
@@ -56,10 +56,11 @@ class Plugin(PluginAdapter):
56
56
  "dataversionmanager": ["data-processed", "datafold", "dataversion"],
57
57
  "removedRecordIds": ["data-processed"],
58
58
  "modelState": ["dataset-bySplit", "modelName", "model", "inputShape", "segmentManipulation", "trainingParameter", "fold"],
59
+ "trainedModel": ["dataset-bySplit", "modelName", "model", "inputShape", "segmentManipulation", "trainingParameter", "fold", "pretraining"],
59
60
  "modelStateConfig": ["modelState"],
60
61
  "threshold": ["modelState", "eventEval", "thresholdMetric", "fixedThreshold"],
61
62
  "validationResult": ["modelState", "segmentManipulationEval", "thresholdMetric", "optimizeOn"],
62
- "evalResults": ["threshold", "evalOn", "manipulationAfterPredict", "segmentManipulationEval"],
63
+ "evalResults": ["threshold", "evalOn", "manipulationAfterPredict", "segmentManipulationEval", "pretraining"],
63
64
  "eventResults": ["evalResults", "eventEval"],
64
65
  "dataset-bySplit": ["dataversionmanager"],
65
66
  "dataset-training": ["dataversionmanager"],
@@ -9,7 +9,7 @@ class SleePyPhases(Project):
9
9
  """
10
10
 
11
11
  @staticmethod
12
- def create(configFiles=None, plugins=None):
12
+ def create(configFiles=None, plugins=None, projectFile=None):
13
13
  if plugins is None:
14
14
  plugins = []
15
15
 
@@ -18,7 +18,7 @@ class SleePyPhases(Project):
18
18
  "pyPhasesML",
19
19
  "pyPhasesRecordloader",
20
20
  ]
21
- project = DefaultProject.create(configFiles, plugins + defaultPlugins, project=SleePyPhases())
21
+ project = DefaultProject.create(configFiles, plugins + defaultPlugins, project=SleePyPhases(), projectFile=projectFile)
22
22
 
23
23
  return project
24
24
 
@@ -86,8 +86,9 @@ class SleepMetaData:
86
86
  def fromSleepSignalBin(self, signal, frequency = 1):
87
87
  # all time values in seconds
88
88
  trt = len(signal) / (frequency)
89
- tst = sum(signal) / (frequency)
90
89
  whereSleep = np.where(signal > 0)[0]
90
+ tst = len(whereSleep) / (frequency)
91
+
91
92
  if len(whereSleep) == 0:
92
93
  sLatency = 0
93
94
  else:
@@ -107,8 +108,11 @@ class SleepMetaData:
107
108
  self.registerMetadata("sEfficiency", tst / trt * 100, unit="%")
108
109
 
109
110
  def fromSleepSignalFour(self, signal, frequency = 1):
110
- whereREM = np.where(signal == 1)
111
- rLatency = whereREM[0] - self.getMetaData("sLatency")
111
+ whereREM = np.where(signal == 1)[0]
112
+ if len(whereREM) > 0:
113
+ rLatency = whereREM[0] - self.getMetaData("sLatency")
114
+ else:
115
+ rLatency = 0
112
116
  self.registerMetadata("rLatency", rLatency)
113
117
 
114
118
 
@@ -1,3 +1,4 @@
1
+ from pathlib import Path
1
2
  import random
2
3
  import numpy as np
3
4
  import pandas as pd
@@ -15,13 +16,6 @@ class Eval(Phase):
15
16
  allowTraining: bool = False
16
17
  exampleCount: int = 3
17
18
 
18
- def getModel(self) -> Model:
19
- modelState = self.project.getData("modelState", Model, generate=self.allowTraining)
20
- model = ModelManager.getModel(True)
21
- model.build()
22
- model.loadState(modelState)
23
- return model
24
-
25
19
  def getTestMetadata(self):
26
20
  if not bool(self.getConfig("evalOn", {})):
27
21
  return self.getData("metadata", list)
@@ -77,10 +71,9 @@ class Eval(Phase):
77
71
  def segmentEvaluation(self, name):
78
72
  testData = self.getTestData()
79
73
  self.setConfig("datasetSplits", list(self.getConfig("dataversion.split", {}).keys()))
80
- model = self.getModel()
74
+ model = self.getData("trainedModel")
81
75
 
82
- # setup data and scorer
83
-
76
+ # setup data and scorer
84
77
  classNums = self.getConfig("numClasses", [2])
85
78
  labelNames = self.getConfig("classification.labelNames")
86
79
  ignoreIndex = self.getConfig("classification.ignoreIndex", -1)
@@ -93,13 +86,11 @@ class Eval(Phase):
93
86
  threshold = threshold or self.getData("threshold", list)
94
87
  scorer.setThresholds(threshold)
95
88
 
96
- manipulationAfterPredict = self.getConfig("manipulationAfterPredict", False)
89
+ manipulationAfterPredict = self.getConfig("manipulationAfterPredict", [])
97
90
  da = DataManipulation.getInstance(manipulationAfterPredict, "test", self.project.config, threshold=threshold)
98
- batchSize = self.getConfig("eval.batchSize")
99
91
 
100
92
  for data in tqdm(testData):
101
93
  x, truth = data
102
-
103
94
  prediction = model.predict(x, returnNumpy=False)
104
95
  prediction, truth = da((prediction, truth))
105
96
 
@@ -145,7 +136,7 @@ class Eval(Phase):
145
136
  with self.project:
146
137
  self.project.config.update(self.getConfig("eventEval", {}))
147
138
 
148
- evalMetrics = self.getConfig("eval.metrics")
139
+ evalMetrics = self.getConfig("eval.metrics", self.getConfig("trainingParameter.validationMetrics"))
149
140
  classNums = self.getConfig("numClasses", [2])
150
141
  labelNames = self.getConfig("classification.labelNames")
151
142
  predictionNames = self.getConfig("classification.predictionSignals")
@@ -169,7 +160,7 @@ class Eval(Phase):
169
160
  # metaData["indexArousal"] = metaData["indexArousal"].astype(float)
170
161
  numClasses = self.getConfig("numClasses")
171
162
 
172
- model = self.getModel()
163
+ model = self.getData("trainedModel")
173
164
 
174
165
  resultRows = []
175
166
  result = {}
@@ -177,11 +177,13 @@ class EvalReport(Phase):
177
177
 
178
178
  def main(self):
179
179
  import pandas as pd
180
+ eventEvalEnabled = bool(self.getConfig("eventEval", True))
180
181
 
181
182
  threshold = self.getConfig("fixedThreshold", False)
182
183
  self.threshold = threshold or self.getData("threshold", float)
183
184
 
184
- modelConfigString = self.project.getDataFromName("eventResults").getTagString()
185
+ resultDataName = "eventResults" if eventEvalEnabled else "evalResults"
186
+ modelConfigString = self.project.getDataFromName(resultDataName).getTagString()
185
187
 
186
188
  evalPath = self.getConfig("eval-path", "eval/")
187
189
  evalPath = f"{evalPath}/{modelConfigString}/"
@@ -206,13 +208,16 @@ class EvalReport(Phase):
206
208
  df.to_csv(f"{evalPath}recordResultsSegment.csv", index=False)
207
209
 
208
210
  self.logSuccess(f"result segments: {segmentResults}")
209
- resultRows, result = self.getData("eventResults", list)
210
- self.logSuccess(f"result events: {result}")
211
- df = pd.DataFrame(resultRows)
211
+ if eventEvalEnabled:
212
+ resultRows, result = self.getData("eventResults", list)
213
+ self.logSuccess(f"result events: {result}")
214
+ df = pd.DataFrame(resultRows)
215
+ else:
216
+ result = {}
212
217
 
213
218
 
214
219
  metrics = self.getConfig("trainingParameter.validationMetrics")
215
- evalMetrics = self.getConfig("eval.metrics")
220
+ evalMetrics = self.getConfig("eval.metrics", metrics)
216
221
  labelNames = self.getConfig("classification.labelNames")
217
222
 
218
223
  results = {}
@@ -225,12 +230,13 @@ class EvalReport(Phase):
225
230
  if metricName in segmentResults:
226
231
  results[f"{metricName}-seg"] = segmentResults[metricName]
227
232
 
228
- relMetaData = self.getConfig("eval.clinicalMetrics")
229
- for m in relMetaData:
230
- df[f"{m}-prediction-diff"] = df[f"{m}-prediction"] - df[f"{m}-truth"]
231
- df[f"{m}-prediction-error"] = df[f"{m}-prediction-diff"].abs()
233
+ if eventEvalEnabled:
234
+ relMetaData = self.getConfig("eval.clinicalMetrics")
235
+ for m in relMetaData:
236
+ df[f"{m}-prediction-diff"] = df[f"{m}-prediction"] - df[f"{m}-truth"]
237
+ df[f"{m}-prediction-error"] = df[f"{m}-prediction-diff"].abs()
232
238
 
233
- df.to_csv(f"{evalPath}recordResultsEvents.csv", index=False)
239
+ df.to_csv(f"{evalPath}recordResultsEvents.csv", index=False)
234
240
 
235
241
  self.logSuccess(f"Results: {results}")
236
242
  results = {k: float(v) for k, v in results.items() if not isinstance(v, Iterable)}
@@ -243,8 +249,11 @@ class EvalReport(Phase):
243
249
  def copyRawData(self):
244
250
  modelConfigString = self.project.getDataFromName("modelState").getTagString()
245
251
  trainingLogPath = f"logs/{modelConfigString}/"
246
- shutil.copyfile(trainingLogPath + "log.csv", self.evalPath + "training.log")
247
- shutil.copyfile(trainingLogPath + "project.config", self.evalPath + "project.json")
252
+ if Path(trainingLogPath).exists():
253
+ shutil.copyfile(trainingLogPath + "log.csv", self.evalPath + "training.log")
254
+ shutil.copyfile(trainingLogPath + "project.config", self.evalPath + "project.json")
255
+ else:
256
+ self.logError(f"training log path does not exist, could not backup training and config: {trainingLogPath}")
248
257
 
249
258
  def trainingPlot(self, logFile):
250
259
  import matplotlib.pyplot as plt
@@ -51,6 +51,7 @@ class RecordProcessor:
51
51
  if "sleepStage" not in eventSignal:
52
52
  raise ChannelsNotPresent("sleepStage")
53
53
  eventSignal = eventSignal["sleepStage"]
54
+ labelSignalArray[eventSignal == 0] = -1
54
55
  labelSignalArray[eventSignal == PSGEventManager.INDEX_WAKE] = 0
55
56
  labelSignalArray[eventSignal == PSGEventManager.INDEX_REM] = 1
56
57
  labelSignalArray[eventSignal == PSGEventManager.INDEX_NREM1] = 2
@@ -170,14 +171,16 @@ class RecordProcessor:
170
171
  recordSignal, events = self.recordLoader.loadRecord(recordId)
171
172
 
172
173
  targetSignals = self.preProcessingConfig["targetChannels"] if "targetChannels" in self.preProcessingConfig else []
173
- targetFrequency = self.preProcessingConfig["targetFrequency"] if "targetFrequency" in self.preProcessingConfig else []
174
- labelFrequency = self.preProcessingConfig["labelFrequency"] if "labelFrequency" in self.preProcessingConfig else []
174
+ targetFrequency = self.preProcessingConfig["targetFrequency"] if "targetFrequency" in self.preProcessingConfig else 0
175
+ labelFrequency = self.preProcessingConfig["labelFrequency"] if "labelFrequency" in self.preProcessingConfig else targetFrequency
175
176
  featureChannels = self.preProcessingConfig["featureChannels"] if "featureChannels" in self.preProcessingConfig else []
177
+ tailorToTargetSignals = self.preProcessingConfig["tailorToTargetSignals"] if "tailorToTargetSignals" in self.preProcessingConfig else False
176
178
  recordSignal.targetFrequency = targetFrequency
177
179
 
178
180
  # tailor to targetsignals
179
- signalNames = [recordSignal.getFirstSignalName(s) for s in targetSignals]
180
- recordSignal = recordSignal[signalNames]
181
+ if tailorToTargetSignals:
182
+ signalNames = [recordSignal.getFirstSignalName(s) for s in targetSignals]
183
+ recordSignal = recordSignal[signalNames]
181
184
 
182
185
  # set the target frequency for the signal to get the correct signal length
183
186
  self.signalProcessing.preprocessingSignal(recordSignal)
@@ -186,11 +189,14 @@ class RecordProcessor:
186
189
  self.featureExtraction.extractChannelsByConfig(recordSignal, featureChannels)
187
190
 
188
191
  signalLength = round(recordSignal.getSignalLength() * labelFrequency / targetFrequency)
192
+ sleepEvents = [e for e in events if e.name in ["R", "N1", "N2", "N3", "W", "undefined"]]
193
+ lastDefaultEventDuration = sleepEvents[-1].duration if sleepEvents and sleepEvents[-1].duration > 0 else 30 * labelFrequency
189
194
  eventSignal = self.eventManager.getEventSignalFromList(
190
195
  events,
191
196
  signalLength,
192
197
  targetFrequency=labelFrequency,
193
198
  forceGapBetweenEvents=False,
199
+ lastDefaultEventDuration=lastDefaultEventDuration,
194
200
  )
195
201
  # signalLength * 30
196
202
  eventSignal = self.preparelabelChannels(eventSignal, signalLength)
@@ -222,12 +228,14 @@ class Extract(Phase):
222
228
  """
223
229
 
224
230
  useMultiThreading = True
231
+ spawnMultiprocessing = False
225
232
  threads = None
226
233
 
227
234
  def getRecordProcessor(self):
228
235
  preprocessingConfig = self.getConfig("preprocessing")
229
236
  featureExtraction = FeatureExtraction.getInstance(self.project)
230
- preDataManipulation = PreManipulation.getInstance(preprocessingConfig["manipulationSteps"])
237
+ manipulationSteps = preprocessingConfig.get("manipulationSteps", [])
238
+ preDataManipulation = PreManipulation.getInstance(manipulationSteps)
231
239
  processRecord = RecordProcessor(
232
240
  recordLoader=RecordLoader.get(),
233
241
  preProcessingConfig=preprocessingConfig,
@@ -281,6 +289,8 @@ class Extract(Phase):
281
289
  bp.useMultiThreading = self.useMultiThreading
282
290
  if self.threads is not None:
283
291
  bp.threads = self.threads
292
+ if self.spawnMultiprocessing:
293
+ bp.startMethod = "spawn"
284
294
 
285
295
  def combine(recordList, dataIndex):
286
296
  recordList = recordList.copy()
@@ -334,4 +344,18 @@ class Extract(Phase):
334
344
  self.registerData("removedRecordIds", removedRecordIds)
335
345
 
336
346
  def main(self):
337
- self.generateData("data-processed")
347
+ datasets = self.getConfig("datafold.datasets", False)
348
+ datasets = datasets if isinstance(datasets, list) else [{"config": {}}]
349
+ for i, source in enumerate(datasets):
350
+ ds_config = source.get("config", {})
351
+ loader_id = ds_config.get("useLoader", f"dataset-{i}")
352
+ self.log(f"Processing dataset {i + 1}/{len(datasets)}: {loader_id}")
353
+
354
+ with self.project:
355
+ # Clear per-dataset dataversion so it gets recomputed
356
+ if "dataversion" in ds_config:
357
+ self.project.setConfig("dataversion", {})
358
+ self.project.updateConfig(ds_config)
359
+
360
+ _ = self.project.getData("data-processed", np.memmap)
361
+ self.logSuccess(f"Dataset '{loader_id}' – extraction done")
@@ -1,6 +1,6 @@
1
1
  from typing import List
2
2
  from pyPhases import Phase
3
- from tqdm import tqdm
3
+ from pyPhases.util import BatchProgress
4
4
  import pandas as pd
5
5
 
6
6
  from pyPhasesRecordloader import AnnotationNotFound, RecordLoader, ChannelsNotPresent, Event, ParseError
@@ -8,10 +8,62 @@ from pyPhasesRecordloader import AnnotationNotFound, RecordLoader, ChannelsNotPr
8
8
  from SleePyPhases.RecordFeatureExtraction import RecordFeatureExtraction
9
9
  from SleePyPhases.SleepMetaData import SleepMetaData
10
10
 
11
+
12
+
13
+ class RecordFeatureProcessor:
14
+ """Processor class for multithreaded/multiprocess record feature extraction"""
15
+
16
+ def __init__(self, featureConfigs, recordLoader, recordFeatureExtraction):
17
+ self.featureConfigs = featureConfigs
18
+ self.recordLoader = recordLoader
19
+ self.recordFeatureExtraction = recordFeatureExtraction
20
+
21
+ def __call__(self, recordId):
22
+ recordLoader = self.recordLoader
23
+ try:
24
+ recordSignal, eventlist = recordLoader.loadRecord(recordId)
25
+ except AnnotationNotFound as e:
26
+ print(f"\033[31;1;4m\033[0mAnnotation missing in {recordId}: {e.name} ... skipping record")
27
+ return None
28
+ except ChannelsNotPresent as e:
29
+ print(f"\033[31;1;4m\033[0mChannels missing in {recordId}: {e.channels} ... skipping record")
30
+ return None
31
+ except ParseError as e:
32
+ print(f"\033[31;1;4m\033[0mParseError in record {recordId}, record will be skipped: {e}")
33
+ return None
34
+
35
+ features = pd.DataFrame(
36
+ index=pd.MultiIndex(levels=[[], []], codes=[[], []], names=["recordId", "channel"])
37
+ )
38
+
39
+ for featureConfig in self.featureConfigs:
40
+ checkedCol = featureConfig["name"] + "_checked"
41
+ channels = featureConfig["channels"]
42
+ for c in channels:
43
+ idx = (recordId, c)
44
+ try:
45
+ featureStep = featureConfig["type"]
46
+ self.recordFeatureExtraction.updateMetadataByStep(
47
+ featureStep, recordSignal, eventlist, metadata=features, channel=c
48
+ )
49
+ except ChannelsNotPresent as e:
50
+ print(f"\033[31;1;4m\033[0mChannels not present in {recordId}: {e.channels}")
51
+
52
+ features.loc[idx, "recordId"] = recordId
53
+ features.loc[idx, "channel"] = c
54
+ features.loc[idx, checkedCol] = True
55
+
56
+ return features
57
+
58
+
11
59
  class ExtractFeatures(Phase):
12
60
  allowTraining = False
13
61
  features = None
14
62
 
63
+ useMultiThreading = True
64
+ spawnMultiprocessing = False
65
+ threads = None
66
+
15
67
  def getChannelMetadata(self, recordId):
16
68
  recordLoader = RecordLoader.get()
17
69
  recordLoader.getSignalHeaders(recordId)
@@ -212,16 +264,52 @@ class ExtractFeatures(Phase):
212
264
  recordIds = metadata["recordId"]
213
265
  forceCalculation = self.getConfig("featureExtraction.force", False)
214
266
 
215
- reqFeatures = {f["name"] + "_checked": True for f in self.getConfig("featureExtraction.features", [])}
216
-
217
267
  recordId = self.getConfig("recordId", False)
218
- recordIds = [recordId] if recordId else recordIds.iloc
219
-
220
- for recordId in tqdm(recordIds):
221
- if forceCalculation:
222
- # the parameter forced has no meaning, it is just known to be not in the metadata, so the record will always be recalculated
223
- reqFeatures["forced"] = True # should never be found
224
- rows = self.getData("features", pd.DataFrame, recordId=recordId, **reqFeatures)
225
- if len(rows) == 0:
226
- self.logError(f"no features for {recordId}")
268
+ recordIds = [recordId] if recordId else recordIds.tolist()
269
+
270
+ featureConfigs = self.getConfig("featureExtraction.features", [])
271
+ checkedCols = [f["name"] + "_checked" for f in featureConfigs]
272
+
273
+ self.features = self.getData("features", pd.DataFrame)
274
+
275
+ if not forceCalculation and len(self.features) > 0:
276
+ # a record is complete if all _checked columns are True for it
277
+ completedIds = set()
278
+ if all(col in self.features.columns for col in checkedCols):
279
+ byRecord = self.features.groupby(level="recordId")
280
+ for rid, group in byRecord:
281
+ if group[checkedCols].all(axis=None):
282
+ completedIds.add(rid)
283
+
284
+ pendingIds = [r for r in recordIds if r not in completedIds]
285
+ self.logSuccess(f"Skipping {len(recordIds) - len(pendingIds)} already-calculated records, {len(pendingIds)} to process")
286
+ else:
287
+ pendingIds = recordIds
288
+
289
+ if len(pendingIds) == 0:
290
+ self.logSuccess("All records already have features calculated")
291
+ return
292
+
293
+ recordLoader = RecordLoader.get()
294
+ recordFeatureExtraction = RecordFeatureExtraction.getInstance()
295
+ processor = RecordFeatureProcessor(featureConfigs, recordLoader, recordFeatureExtraction)
296
+
297
+ bp = BatchProgress(pendingIds)
298
+ bp.useMultiThreading = self.useMultiThreading
299
+ if self.threads is not None:
300
+ bp.threads = self.threads
301
+ if self.spawnMultiprocessing:
302
+ bp.startMethod = "spawn"
303
+
304
+ def combine(resultList, dataIndex):
305
+ for result in resultList:
306
+ if result is None:
307
+ continue
308
+ if len(result) > 0:
309
+ self.features = pd.concat([self.features, result])
310
+ else:
311
+ self.logError(f"no features for record at index {dataIndex}")
312
+ self.registerData("features", self.features)
313
+
314
+ bp.start(processor, afterBatch=combine)
227
315
 
@@ -138,8 +138,9 @@ class Predict(Phase):
138
138
  recordSignal.targetFrequency = targetFrequency
139
139
 
140
140
  # Get signal names
141
- signalNames = [recordSignal.getFirstSignalName(s) for s in targetSignals]
142
- recordSignal = recordSignal[signalNames]
141
+ if self.getConfig("preprocessing.tailorToTargetSignals", False):
142
+ signalNames = [recordSignal.getFirstSignalName(s) for s in targetSignals]
143
+ recordSignal = recordSignal[signalNames]
143
144
 
144
145
  # Apply preprocessing
145
146
  signalProcessing = SignalPreprocessing.getInstance(preprocessingConfig)
@@ -167,7 +168,7 @@ class Predict(Phase):
167
168
 
168
169
  return self.predictRecordSignal(recordSignal)
169
170
 
170
- def predictRecordSignal(self, recordSignal, dummyY=None):
171
+ def predictRecordSignal(self, recordSignal, metadata=None, dummyY=None):
171
172
  processedSignal = self.preprocessSignal(recordSignal)
172
173
 
173
174
  with self.project:
@@ -177,8 +178,10 @@ class Predict(Phase):
177
178
  # Apply segment manipulation for evaluation
178
179
  manipulationSteps = self.getConfig("segmentManipulationEval", [])
179
180
  da = DataManipulation.getInstance(manipulationSteps, "test", self.project.config)
181
+ da.recordMetadata = [metadata] if metadata is not None else None
182
+ da.currentIndex = 0
180
183
  dummyY = np.array([0]).reshape(1, -1, 1) if dummyY is None else dummyY
181
- processedSignal, dummyY = da((processedSignal, dummyY)) # Dummy label
184
+ processedSignal, dummyY = da((processedSignal, dummyY), index=0) # Dummy label
182
185
 
183
186
  # Load model with optional custom weights
184
187
  weightsPath = self.getConfig("predict.weights", False)
@@ -193,7 +196,7 @@ class Predict(Phase):
193
196
  threshold = self.getConfig("fixedThreshold", False)
194
197
  threshold = threshold or self.getData("threshold", list, generate=False)
195
198
  da = DataManipulation.getInstance(manipulationAfterPredict, "test", self.project.config, threshold=threshold)
196
- prediction, _ = da((prediction, dummyY))
199
+ prediction, _ = da((prediction, dummyY), index=0)
197
200
 
198
201
  return prediction
199
202
 
@@ -28,6 +28,9 @@ class Setup(Phase):
28
28
  # default fold is 0 if none is set
29
29
  self.setConfig("fold", self.getConfig("fold", 0))
30
30
  # self.setConfig("datasetSplits", list(self.getConfig("dataversion.split", {}).keys()))
31
+ namedFolds = self.getConfig("dataversion.namedFolds", False)
32
+ if namedFolds:
33
+ self.setConfig("dataversion.folds", len(namedFolds))
31
34
  self.setConfig("hasFolds", self.getConfig("dataversion.folds", 0) > 0)
32
35
 
33
36
  s = Scorer(5)
@@ -75,13 +78,33 @@ class Setup(Phase):
75
78
 
76
79
  dm = DataversionManager(groupedRecords, seed=seed)
77
80
 
81
+ # named fold presets: dataversion.namedFolds resolved by name (foldName) or integer index (fold)
82
+ namedFolds = self.getConfig("dataversion.namedFolds", False)
83
+ if namedFolds:
84
+ foldNames = list(namedFolds.keys())
85
+ foldName = self.getConfig("foldName", False)
86
+ if not foldName:
87
+ foldIndex = self.getConfig("fold", 0)
88
+ if foldIndex >= len(foldNames):
89
+ raise Exception("fold index %i out of range for namedFolds (len %i)" % (foldIndex, len(foldNames)))
90
+ foldName = foldNames[foldIndex]
91
+ elif foldName not in namedFolds:
92
+ raise Exception("foldName '%s' not found in dataversion.namedFolds. Available: %s" % (foldName, foldNames))
93
+ foldConfig = namedFolds[foldName]
94
+ self.log(f"Using named fold '{foldName}'")
95
+ for splitName in ["training", "validation", "test"]:
96
+ if splitName in foldConfig:
97
+ dm.addSplitBySlices(splitName, foldConfig[splitName])
98
+ dm.validatDatasetVersion(raiseException=self.getConfig("validatDataset", True))
99
+ return dm
100
+
78
101
  # add manual addes splits
79
102
  for splitName in ["training", "validation", "test"]:
80
103
  if splitName in splits:
81
104
  dm.addSplitBySlices(splitName, splits[splitName])
82
105
 
83
106
  # add training folding splits
84
- if "trainval" in splits:
107
+ if "trainval" in splits and splits["trainval"] is not None:
85
108
  foldcount = self.getConfig("dataversion.folds", 0)
86
109
  if foldcount > 0:
87
110
  dm.addSplitsByFold("training", "validation", splits["trainval"], self.getConfig("dataversion.folds", 0), self.getConfig("fold", 0))
@@ -92,7 +115,7 @@ class Setup(Phase):
92
115
  if "training" in splits or "validation" in splits:
93
116
  raise Exception("trainval and training/validation split are mutually exclusive. Please remove one of them from the config. This can be caused by loading multiple configs with different splits.")
94
117
 
95
- if "trainvaltest" in splits:
118
+ if "trainvaltest" in splits and splits["trainvaltest"] is not None:
96
119
  foldCount = self.getConfig("dataversion.folds")
97
120
  currentFold = self.getConfig("fold", 0)
98
121
  testSlice, trainingSlice = dm.getSplitsByPercentage(
@@ -35,7 +35,7 @@ class ThresholdOptimisation(Phase):
35
35
  truth = []
36
36
  for i in tqdm(range(0, len(validationData))):
37
37
  x, t = validationData[i]
38
- p = model.predict(x, get_likelihood=True, returnNumpy=False)
38
+ p = model.predict(x, returnNumpy=False)
39
39
  p, t = da((p, t))
40
40
  [prediction.append(x) for x in p]
41
41
  [truth.append(x) for x in t]
@@ -49,7 +49,7 @@ class ThresholdOptimisation(Phase):
49
49
  thresholdMetrics = self.getConfig("thresholdMetric", None)
50
50
  thresholdMetrics = thresholdMetrics if isinstance(thresholdMetrics, list) else [thresholdMetrics]
51
51
 
52
- classNums = self.getConfig("model.multiclassificationSize", [2])
52
+ classNums = self.getConfig("numClasses", [2])
53
53
  labelNames = self.getConfig("classification.labelNames")
54
54
  ignoreIndex = self.getConfig("classification.ignoreIndex", -1)
55
55
  optimizeThresholdFor = self.getConfig("optimizeThresholdFor", None)
@@ -62,6 +62,7 @@ class ThresholdOptimisation(Phase):
62
62
  s.noTN = self.getConfig("eventEval.tnStrat", "eventcount") == "noTN"
63
63
  s.trace = True
64
64
 
65
+ classNums = [cn if cn == 1 else 2 for cn in classNums]
65
66
 
66
67
  manipulationAfterPredict = self.getConfig("eventEval.manipulationAfterPredict", False)
67
68
  daSteps = self.getConfig("eventEval.augmentationCountThatShouldNotBeOptimized", 0)
@@ -69,11 +70,11 @@ class ThresholdOptimisation(Phase):
69
70
 
70
71
  # get Validation data
71
72
  if self.storeValidationPrediction:
72
- # Calculate validation data fresh for each optimization (not stored)
73
- prediction, truth = self.calculateValidationData()
74
- else:
75
73
  # Use stored validation data
76
74
  prediction, truth = self.getData("validationResult", tuple)
75
+ else:
76
+ # Calculate validation data fresh for each optimization call, to save disk space
77
+ prediction, truth = self.calculateValidationData()
77
78
 
78
79
  def score(threshold, p, t, scorerIndex, scorerName, optimizeMetric):
79
80
  biggerIsBetter = scorer.getMetricDefinition(optimizeMetric)[2]
@@ -57,9 +57,37 @@ class Training(Phase):
57
57
  return trainedModel
58
58
 
59
59
  def generateData(self, name):
60
- trainedModel = self.train()
61
- self.project.registerData("modelState", trainedModel)
62
- self.project.registerData("modelStateConfig", self.project.getDataFromName("modelState").getDependencyDict())
60
+ if name == "modelState":
61
+ trainedModel = self.train()
62
+ self.project.registerData("modelState", trainedModel)
63
+ elif name == "modelStateConfig":
64
+ self.project.registerData("modelStateConfig", self.project.getDataFromName("modelState").getDependencyDict())
65
+ elif name == "trainedModel":
66
+
67
+ model = ModelManager.getModel(True)
68
+ pretrainingConfig = self.getConfig("pretraining", {})
69
+
70
+ pretrainedPath = pretrainingConfig.get("pretrainedModel", False)
71
+ preModelState = None
72
+
73
+ if pretrainedPath:
74
+ if not Path(pretrainedPath).exists():
75
+ err = f"model path for evaluation does not exist (pretraining.pretrainedModel): {pretrainedPath}"
76
+ self.logError(err)
77
+ raise Exception(err)
78
+ else:
79
+ preModelState = model.load(pretrainedPath)
80
+
81
+ if preModelState is None:
82
+ with self.project:
83
+ self.project.config["dataversion"] = {}
84
+ self.project.config.update(pretrainingConfig)
85
+ self.project.trigger("configChanged", None)
86
+ preModelState = self.project.getData("modelState", Model)
87
+
88
+ model.build()
89
+ model.loadState(preModelState)
90
+ return model
63
91
 
64
92
  def main(self):
65
93
  startfold = self.getConfig("startFold", 0)
@@ -88,9 +116,9 @@ class Training(Phase):
88
116
  self.project.unregister("modelStateConfig")
89
117
  gridOutputs.append(self.gridOutput)
90
118
  # reset model state
91
- ModelManager.getModel().model.load_state_dict(startState)
119
+ ModelManager.getModel().resetParameters()
92
120
 
93
- self.project.gridOutput = {
121
+ self.project.gridOutput= {
94
122
  "epochs": str(np.mean([m["epochs"] for m in gridOutputs])),
95
123
  "best": str(np.mean([m["best"] for m in gridOutputs])),
96
124
  "folds": len(gridOutputs),
@@ -21,7 +21,7 @@ class Validation(Phase):
21
21
  validationData = self.project.generateData("dataset-validation")
22
22
 
23
23
  for x, t in tqdm(validationData):
24
- p = model.predict(x, get_likelihood=True, returnNumpy=True)
24
+ p = model.predict(x, returnNumpy=True)
25
25
  scorer.score(t, p, trace=True)
26
26
 
27
27
  res = scorer.scoreAllRecords()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SleePyPhases
3
- Version: 0.7.0
3
+ Version: 0.8.2
4
4
  Summary: A framwork for creating deep learning pipelines for sleep data
5
5
  Home-page: https://gitlab.com/sleep-is-all-you-need/sleepyphases
6
6
  Author: Franz Ehrlich
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="SleePyPhases",
8
- version="v0.7.0"[1:],
8
+ version="v0.8.2"[1:],
9
9
  author="Franz Ehrlich",
10
10
  author_email="fehrlichd@gmail.com",
11
11
  description="A framwork for creating deep learning pipelines for sleep data",
@@ -117,6 +117,7 @@ class TestExtract(TestCase):
117
117
  signalLength,
118
118
  targetFrequency=1,
119
119
  forceGapBetweenEvents=False,
120
+ lastDefaultEventDuration=10 # (Event("N1", 0, duration=10)])
120
121
  )
121
122
 
122
123
  # Check that the sliced recordSignal has the correct targetFrequency
File without changes
File without changes
File without changes