SleePyPhases 0.6.2__tar.gz → 0.7.0__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.6.2 → sleepyphases-0.7.0}/PKG-INFO +1 -1
  2. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/MultiScorer.py +27 -11
  3. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/Plugin.py +2 -0
  4. sleepyphases-0.7.0/SleePyPhases/SleePyPhases.py +34 -0
  5. sleepyphases-0.7.0/SleePyPhases/config.yaml +167 -0
  6. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/BuildDataset.py +18 -23
  7. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/Extract.py +7 -7
  8. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/Predict.py +8 -12
  9. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/Setup.py +2 -2
  10. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases.egg-info/PKG-INFO +1 -1
  11. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases.egg-info/SOURCES.txt +3 -0
  12. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/setup.py +1 -1
  13. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/tests/__init__.py +11 -0
  14. sleepyphases-0.7.0/tests/unit/test_BuildDatasetMetadataSync.py +261 -0
  15. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/tests/unit/test_DataManipulation.py +1 -1
  16. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/LICENSE +0 -0
  17. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/README.md +0 -0
  18. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/DataManipulation.py +0 -0
  19. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/EventScorer.py +0 -0
  20. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/FeatureExtraction.py +0 -0
  21. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/Plot.py +0 -0
  22. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/PreManipulation.py +0 -0
  23. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/RecordFeatureExtraction.py +0 -0
  24. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/Reporter.py +0 -0
  25. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/SignalPreprocessing.py +0 -0
  26. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/SleepMetaData.py +0 -0
  27. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/__init__.py +0 -0
  28. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/DataAnalysis.py +0 -0
  29. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/DataReport.py +0 -0
  30. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/Eval.py +0 -0
  31. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/EvalPlotExamples.py +0 -0
  32. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/EvalReport.py +0 -0
  33. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/ExportMetadataToMeticalDB.py +0 -0
  34. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/ExtractEvents.py +0 -0
  35. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/ExtractFeatures.py +0 -0
  36. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/ExtractRecordFeatures.py +0 -0
  37. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/GatherMetadata.py +0 -0
  38. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/LogCleanup.py +0 -0
  39. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/LogOverview.py +0 -0
  40. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/TestRun.py +0 -0
  41. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/ThresholdOptimisation.py +0 -0
  42. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/Training.py +0 -0
  43. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/Validation.py +0 -0
  44. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/VisualizeConfig.py +0 -0
  45. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases/phases/__init__.py +0 -0
  46. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases.egg-info/dependency_links.txt +0 -0
  47. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases.egg-info/requires.txt +0 -0
  48. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/SleePyPhases.egg-info/top_level.txt +0 -0
  49. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/pyproject.toml +0 -0
  50. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/setup.cfg +0 -0
  51. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/tests/conftest.py +0 -0
  52. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/tests/unit/__init__.py +0 -0
  53. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/tests/unit/test_BuildDataset.py +0 -0
  54. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/tests/unit/test_BuildDatasetFolded.py +0 -0
  55. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/tests/unit/test_BuildDatasetSegments.py +0 -0
  56. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/tests/unit/test_Eval.py +0 -0
  57. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/tests/unit/test_EventScorer.py +0 -0
  58. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/tests/unit/test_Extract.py +0 -0
  59. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/tests/unit/test_PreManipulation.py +0 -0
  60. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/tests/unit/test_RecordFeatureExtraction.py +0 -0
  61. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/tests/unit/test_RecordProcessor.py +0 -0
  62. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/tests/unit/test_Setup.py +0 -0
  63. {sleepyphases-0.6.2 → sleepyphases-0.7.0}/tests/unit/test_SignalPreprocessing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SleePyPhases
3
- Version: 0.6.2
3
+ Version: 0.7.0
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
@@ -2,7 +2,7 @@ import numpy as np
2
2
  from pyPhasesML.scorer.Scorer import Scorer
3
3
 
4
4
  class MultiScorer(Scorer):
5
- def __init__(self, numClasses, metrics, scorerNames=[], ignoreClasses=None, classNames=None, scorerClasses=None) -> None:
5
+ def __init__(self, numClasses, metrics, scorerNames=[], ignoreClasses=None, classNames=None, scorerClasses=None, multilabel=None) -> None:
6
6
 
7
7
  ignoreClasses = ignoreClasses or [-1]
8
8
 
@@ -13,11 +13,17 @@ class MultiScorer(Scorer):
13
13
  self.scorerNames = scorerNames
14
14
  self.metricDefinitions = {}
15
15
 
16
+ # Normalise multilabel to a per-scorer list
17
+ if multilabel is None:
18
+ multilabel = [False] * len(numClasses)
19
+ elif isinstance(multilabel, bool):
20
+ multilabel = [multilabel] * len(numClasses)
21
+
16
22
  metrics = metrics if isinstance(metrics[0], list) else [metrics for _ in range(len(numClasses))]
17
23
 
18
24
  for i, (numClasses, scorerName) in enumerate(zip(numClasses, scorerNames)):
19
25
  scorerClass = scorerClasses[i] if scorerClasses is not None else Scorer
20
- s = scorerClass(numClasses)
26
+ s = scorerClass(numClasses, multilabel=multilabel[i])
21
27
  s.trace = True
22
28
  s.ignoreClasses = ignoreClasses
23
29
  s.numClasses = numClasses if numClasses > 1 else 2
@@ -96,29 +102,39 @@ class MultiScorer(Scorer):
96
102
 
97
103
  return scorer.score(truth, prediction, trace=trace)
98
104
 
99
- def prettyPrintConfusionMatrix(self, confusion_matrix):
100
- num_rows, num_cols = confusion_matrix.shape
101
- max_value_length = max(len(str(confusion_matrix.max())), len(str(confusion_matrix.min())))
102
- separator = "-" * ((max_value_length + 2) * num_cols)
103
-
105
+ def _print2DConfusionMatrix(self, cm):
106
+ cm = np.array(cm)
107
+ num_rows, num_cols = cm.shape
108
+ max_value_length = max(len(str(int(cm.max()))), len(str(int(cm.min()))))
109
+ separator = "-" * ((max_value_length + 3) * num_cols)
104
110
  rows = []
105
111
  for i in range(num_rows):
106
- row_data = [f"{int(confusion_matrix[i, j]):>{max_value_length}}" for j in range(num_cols)]
112
+ row_data = [f"{int(cm[i, j]):>{max_value_length}}" for j in range(num_cols)]
107
113
  rows.append(" | ".join(row_data))
108
-
109
114
  print(separator)
110
115
  print("\n".join(rows))
111
116
  print(separator)
117
+
118
+ def prettyPrintConfusionMatrix(self, confusion_matrix, classNames=None):
119
+ cm = np.array(confusion_matrix)
120
+ if cm.ndim == 3:
121
+ # multilabel: shape (n_classes, 2, 2)
122
+ for i, cm_i in enumerate(cm):
123
+ label = classNames[i] if classNames is not None else f"Class {i}"
124
+ print(f" [{label}]")
125
+ self._print2DConfusionMatrix(cm_i)
126
+ else:
127
+ self._print2DConfusionMatrix(cm)
112
128
 
113
129
  def scoreAllRecords(self):
114
130
  results = {}
115
131
  self.recordResult = {}
116
132
  for name, s in self.scorer.items():
117
133
  r = s.scoreAllRecords()
118
- results.update({f"{m}_{name}": v for m,v in r.items()})
134
+ results.update({f"{m}_{name}": v for m, v in r.items()})
119
135
  if "confusion" in r:
120
136
  print(f"######{name}########")
121
- self.prettyPrintConfusionMatrix(r["confusion"] )
137
+ self.prettyPrintConfusionMatrix(r["confusion"], classNames=s.classNames)
122
138
  self.recordResult[name] = s.recordResult
123
139
  self.results = results
124
140
 
@@ -24,6 +24,8 @@ from pyPhasesML.exporter.MemmapRecordExporter import MemmapRecordExporter
24
24
 
25
25
 
26
26
  class Plugin(PluginAdapter):
27
+ defaultConfig = "config.yaml"
28
+
27
29
  def __init__(self, project: Project, options=None):
28
30
  super().__init__(project, options)
29
31
 
@@ -0,0 +1,34 @@
1
+ from phases import DefaultProject
2
+ from pyPhases import Project
3
+ from pyPhasesRecordloader import RecordSignal
4
+
5
+
6
+ class SleePyPhases(Project):
7
+ """
8
+ This is the main class for the SleePyPhases project. It defines the phases that will be executed in the project.
9
+ """
10
+
11
+ @staticmethod
12
+ def create(configFiles=None, plugins=None):
13
+ if plugins is None:
14
+ plugins = []
15
+
16
+ defaultPlugins = [
17
+ "SleePyPhases",
18
+ "pyPhasesML",
19
+ "pyPhasesRecordloader",
20
+ ]
21
+ project = DefaultProject.create(configFiles, plugins + defaultPlugins, project=SleePyPhases())
22
+
23
+ return project
24
+
25
+
26
+ def predictRecordSignal(self, recordSignal: RecordSignal):
27
+ from SleePyPhases.phases.Predict import Predict
28
+ predict: Predict = self.getPhase("Predict")
29
+ return predict.predictRecordSignal(recordSignal)
30
+
31
+ def predictFromFile(self, filepath: str):
32
+ from SleePyPhases.phases.Predict import Predict
33
+ predict: Predict = self.getPhase("Predict")
34
+ return predict.predict(filepath)
@@ -0,0 +1,167 @@
1
+ # isFullConfig: True
2
+
3
+ # phases:
4
+ # - name: Setup
5
+ # description: Prepare the project and compute neccessary config values
6
+ # exports:
7
+ # - dataversionmanager
8
+ # - name: Extract
9
+ # description: Preprocess and save the raw data
10
+ # exports:
11
+ # - removedRecordIds
12
+ # - data-processed
13
+ # - data-features
14
+ # - record-preprocessed
15
+ # - name: BuildDataset
16
+ # description: Combine datasets and manipulate the data live for the model
17
+ # exports:
18
+ # # - dataset-trainval
19
+ # - dataset-training
20
+ # - dataset-validation
21
+ # - dataset-test
22
+ # - dataset-bySplit
23
+ # - data-records
24
+ # - record-data
25
+ # - name: Training
26
+ # description: Train my model
27
+ # exports:
28
+ # - modelState
29
+ # - modelStateConfig
30
+ # - name: Validation
31
+ # description: Run Model on validation data for optimization
32
+ # - name: ThresholdOptimisation
33
+ # description: Threshold optimisation by optimising the threshold metric on validation data
34
+ # exports:
35
+ # - threshold
36
+ # - validationResult
37
+ # - name: Eval
38
+ # description: Evalutate my model against the test data
39
+ # exports:
40
+ # - evalResults
41
+ # - eventResults
42
+ # - name: EvalReport
43
+ # description: generate evaluation report
44
+ # - name: DataAnalysis
45
+ # description: Get class distribution and other statistics
46
+ # exports:
47
+ # - datasetstats
48
+ # - name: DataReport
49
+ # - name: ExtractEvents
50
+ # exports:
51
+ # - events
52
+ # - eventDF
53
+ # - name: ExportMetadataToMeticalDB
54
+ # - name: ExtractFeatures
55
+ # exports:
56
+ # - features
57
+ # - name: Predict
58
+ # description: Execute trained model on a given EDF file for prediction
59
+ # exports:
60
+ # - prediction
61
+
62
+
63
+ # data:
64
+ # # - name: data-records
65
+ # - name: features
66
+ # dependsOn:
67
+ # - metadata
68
+ # - name: events
69
+ # dependsOn:
70
+ # - allDBRecordIds
71
+ # - name: eventDF
72
+ # dependsOn:
73
+ # - allDBRecordIds
74
+ # - name: data-processed
75
+ # dependsOn:
76
+ # - allDBRecordIds
77
+ # - preprocessing
78
+ # - labelChannels
79
+ # - name: record-preprocessed
80
+ # dependsOn:
81
+ # - recordId
82
+ # - data-processed
83
+ # - name: record-data
84
+ # - name: data-features
85
+ # dependsOn:
86
+ # - data-processed
87
+ # - name: dataversionmanager
88
+ # dependsOn:
89
+ # - data-processed
90
+ # - datafold
91
+ # - dataversion
92
+ # - name: removedRecordIds
93
+ # dependsOn:
94
+ # - data-processed
95
+ # - name: modelState
96
+ # dependsOn:
97
+ # - dataset-bySplit
98
+ # - modelName
99
+ # - model
100
+ # - inputShape
101
+ # - segmentManipulation
102
+ # - trainingParameter
103
+ # - fold
104
+ # - name: modelStateConfig
105
+ # dependsOn:
106
+ # - modelState
107
+ # - name: threshold
108
+ # dependsOn:
109
+ # - modelState
110
+ # - eventEval
111
+ # - thresholdMetric
112
+ # - fixedThreshold
113
+ # - name: validationResult
114
+ # dependsOn:
115
+ # - modelState
116
+ # - segmentManipulationEval
117
+ # - thresholdMetric
118
+ # - optimizeOn
119
+ # - name: evalResults
120
+ # dependsOn:
121
+ # - threshold
122
+ # - evalOn
123
+ # - manipulationAfterPredict
124
+ # - segmentManipulationEval # is applied to the test dataset
125
+ # - name: eventResults
126
+ # dependsOn:
127
+ # - evalResults
128
+ # - eventEval
129
+ # - eval.clinicalMetrics
130
+ # - name: dataset-bySplit
131
+ # dependsOn:
132
+ # # does not require the split, since its encoded in the name and will not be saved
133
+ # - dataversionmanager
134
+ # - name: dataset-training
135
+ # dependsOn:
136
+ # - dataversionmanager
137
+ # - name: dataset-validation
138
+ # dependsOn:
139
+ # - dataversionmanager
140
+ # # - name: dataset-trainval
141
+ # # dependsOn:
142
+ # # - dataset-bySplit
143
+ # # - labelChannels
144
+ # # - fold
145
+ # - name: dataset-test
146
+ # dependsOn:
147
+ # - dataversionmanager
148
+ # - evalOn
149
+ # - name: datasetstats
150
+ # dependsOn:
151
+ # - data-processed
152
+ # - data-features
153
+ # - name: prediction
154
+ # dependsOn:
155
+ # - predict.inputFile
156
+ # - predict.weights
157
+ # - predict.recordLoader
158
+ # - predict.channelMapping
159
+
160
+ # config:
161
+
162
+ predict:
163
+ inputFile: null
164
+ channelMapping: {}
165
+ weights: null
166
+
167
+ inputShape: null
@@ -20,10 +20,11 @@ class BuildDataset(Phase):
20
20
  splitName = self.getConfig("datasetSplit")
21
21
 
22
22
  datasetVersions = self.getConfig("datafold.datasets", {"config": {}})
23
- datasetVersions = datasetVersions if isinstance(datasetVersions, list) else [datasetVersions]
23
+ datasetVersions = datasetVersions if isinstance(datasetVersions, list) else [{"config": {}}]
24
24
 
25
25
  seed = self.getConfig("datafold.seed", 2024)
26
- multiSrcMap = MultiSourceMap(seed=None)
26
+ shouldShuffle = self.getConfig("datafold.shuffle", False)
27
+ multiSrcMap = MultiSourceMap(seed=seed)
27
28
  recordMetadata = []
28
29
  for sourceVersion in datasetVersions:
29
30
  overwriteConfig = sourceVersion["config"]
@@ -46,7 +47,7 @@ class BuildDataset(Phase):
46
47
 
47
48
  recordIds = dm.getRecordsForSplit(splitName)
48
49
  recordIndexes = [allRecordIds.index(r) for r in recordIds]
49
- recordMetadata = recordMetadata + [metadataMap[r] for r in recordIds]
50
+ recordMetadata = [metadataMap[r] for r in allRecordIds] # unshuffled
50
51
 
51
52
  # handle segment wise data
52
53
  recordLengthsInSegments = None
@@ -67,40 +68,34 @@ class BuildDataset(Phase):
67
68
  )
68
69
 
69
70
  dataset = DatasetXY(dataExporterSignals, dataExporterFeatures)
70
- recordSet = RecordMap(dataset, recordIndexes, mappingLengths=recordLengthsInSegments)
71
71
 
72
- multiSrcMap.addRecordMap(recordSet)
72
+ useConfigName = "segmentManipulationEval" if splitName != "training" else "segmentManipulation"
73
+ manipulationSteps = self.getConfig(useConfigName)
74
+ segmentManipulation = DataManipulation.getInstance(
75
+ manipulationSteps, splitName, self.project.config, recordMetadata=recordMetadata
76
+ )
73
77
 
74
- # shuffle recordings or segments
75
- if self.getConfig("datafold.shuffle", False):
76
- multiSrcMap.shuffle()
77
- # use same seed to shuffle the metadata
78
- np.random.seed(seed)
79
- np.random.shuffle(recordMetadata)
78
+ manipulator = Manipulator(dataset, segmentManipulation)
79
+
80
+ recordSet = RecordMap(manipulator, recordIndexes, mappingLengths=recordLengthsInSegments)
80
81
 
81
- return multiSrcMap, recordMetadata
82
+ multiSrcMap.addRecordMap(recordSet)
82
83
 
83
- def buildDatasetManipulatorBySplit(self):
84
- split = self.getConfig("datasetSplit", "training")
84
+ if shouldShuffle:
85
+ multiSrcMap.shuffle()
85
86
 
86
- dataRecords, recordMetaData = self.buildDataRecords()
87
+ return multiSrcMap
87
88
 
88
- useConfigName = "segmentManipulationEval" if split != "training" else "segmentManipulation"
89
- manipulationSteps = self.getConfig(useConfigName)
90
- segmentManipulation = DataManipulation.getInstance(
91
- manipulationSteps, split, self.project.config, recordMetadata=recordMetaData
92
- )
93
- return Manipulator(dataRecords, segmentManipulation)
94
89
 
95
90
  def buildDatasetBySplit(self):
96
91
  split = self.getConfig("datasetSplit")
97
92
  batchSize = self.getConfig("trainingParameter.batchSize") if split != "test" else self.getConfig("eval.batchSize")
98
93
  buildConfig = self.getConfig("BuildDataset", {"useMultiThreading": True})
99
94
 
100
- manipulator = self.buildDatasetManipulatorBySplit()
95
+ datasets = self.buildDataRecords()
101
96
 
102
97
  dataloader = DataLoader.build(
103
- manipulator,
98
+ datasets,
104
99
  preload=buildConfig["useMultiThreading"],
105
100
  batchSize=batchSize,
106
101
  shuffle=self.getConfig("trainingParameter.shuffle", False),
@@ -93,7 +93,7 @@ class RecordProcessor:
93
93
  signal = signal.reshape(-1, 1)
94
94
  labelChannels.append(signal)
95
95
 
96
- return np.concatenate(labelChannels, axis=1)
96
+ return np.concatenate(labelChannels, axis=1) if labelChannels else np.zeros((0, 0))
97
97
 
98
98
  @staticmethod
99
99
  def tailorToSleepScoring(recordSignal, events, useSPT=False, fillLastSleepstage=False, addOffset=None, cutToAnnotation=False):
@@ -169,10 +169,10 @@ class RecordProcessor:
169
169
  try:
170
170
  recordSignal, events = self.recordLoader.loadRecord(recordId)
171
171
 
172
- targetSignals = self.preProcessingConfig["targetChannels"]
173
- targetFrequency = self.preProcessingConfig["targetFrequency"]
174
- labelFrequency = self.preProcessingConfig["labelFrequency"]
175
- featureChannels = self.preProcessingConfig["featureChannels"]
172
+ 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 []
175
+ featureChannels = self.preProcessingConfig["featureChannels"] if "featureChannels" in self.preProcessingConfig else []
176
176
  recordSignal.targetFrequency = targetFrequency
177
177
 
178
178
  # tailor to targetsignals
@@ -233,7 +233,7 @@ class Extract(Phase):
233
233
  preProcessingConfig=preprocessingConfig,
234
234
  signalProcessing=SignalPreprocessing.getInstance(preprocessingConfig),
235
235
  eventManager=PSGEventManager(),
236
- labelChannels=self.getConfig("labelChannels"),
236
+ labelChannels=self.getConfig("labelChannels", []),
237
237
  featureExtraction=featureExtraction,
238
238
  preDataManipulation=preDataManipulation,
239
239
  project=self.project,
@@ -243,7 +243,7 @@ class Extract(Phase):
243
243
  def extractRecord(self, recordId):
244
244
 
245
245
  processRecord = self.getRecordProcessor()
246
- result = processRecord(recordId)
246
+ result = processRecord(recordId)
247
247
  if result is None:
248
248
  raise ChannelsNotPresent(processRecord.channelNotPresent)
249
249
  return result
@@ -98,7 +98,7 @@ class Predict(Phase):
98
98
  # Apply channel mapping if provided
99
99
  if channelMapping:
100
100
  recordLoader.chanelNameAliasMap = {v: k for k, v in channelMapping.items()}
101
-
101
+ recordLoader.targetSignals = expectedChannels
102
102
  try:
103
103
  recordSignal = recordLoader.loadSignal(inputFile)
104
104
  except ChannelsNotPresent as e:
@@ -170,26 +170,22 @@ class Predict(Phase):
170
170
  def predictRecordSignal(self, recordSignal, dummyY=None):
171
171
  processedSignal = self.preprocessSignal(recordSignal)
172
172
 
173
- # # Prepare input for model
174
- # if len(processedSignal.shape) == 2:
175
- # processedSignal = processedSignal.reshape([1] + list(processedSignal.shape))
176
-
177
- # Apply segment manipulation for evaluation
178
- manipulationSteps = self.getConfig("segmentManipulationEval", [])
179
- da = DataManipulation.getInstance(manipulationSteps, "test", self.project.config)
180
- dummyY = np.array([0]).reshape(1, -1, 1) if dummyY is None else dummyY
181
- processedSignal, dummyY = da((processedSignal, dummyY)) # Dummy label
182
-
183
173
  with self.project:
184
174
  overwriteConfig = self.getConfig("predict", {})
185
175
  self.project.updateConfig(overwriteConfig)
186
176
 
177
+ # Apply segment manipulation for evaluation
178
+ manipulationSteps = self.getConfig("segmentManipulationEval", [])
179
+ da = DataManipulation.getInstance(manipulationSteps, "test", self.project.config)
180
+ dummyY = np.array([0]).reshape(1, -1, 1) if dummyY is None else dummyY
181
+ processedSignal, dummyY = da((processedSignal, dummyY)) # Dummy label
182
+
187
183
  # Load model with optional custom weights
188
184
  weightsPath = self.getConfig("predict.weights", False)
189
185
  model = self.getModel(weightsPath)
190
186
 
191
187
  # Run prediction
192
- prediction = model.predict(processedSignal, returnNumpy=True)
188
+ prediction = model.predict(processedSignal, returnNumpy=False)
193
189
 
194
190
  # Apply post-prediction manipulation if configured
195
191
  manipulationAfterPredict = self.getConfig("manipulationAfterPredict", False)
@@ -71,7 +71,7 @@ class Setup(Phase):
71
71
 
72
72
  # create non manual splits, if not specified
73
73
  valSplit = self.getConfig("validationSplit", 0.2)
74
- testSplit = self.getConfig("testSplit", 0.2)
74
+ testSplit = self.getConfig("testSplit", 0)
75
75
 
76
76
  dm = DataversionManager(groupedRecords, seed=seed)
77
77
 
@@ -104,7 +104,7 @@ class Setup(Phase):
104
104
  dm.addSplitByRemaining("validation", valSplit) # , remainingSplit=trainingSlice
105
105
  dm.addSplitByRemaining("training", 1)
106
106
 
107
- if "test" not in dm.splits:
107
+ if "test" not in dm.splits and testSplit > 0:
108
108
  dm.addSplitByRemaining("test", testSplit)
109
109
 
110
110
  if "validation" not in dm.splits:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SleePyPhases
3
- Version: 0.6.2
3
+ Version: 0.7.0
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
@@ -12,8 +12,10 @@ SleePyPhases/PreManipulation.py
12
12
  SleePyPhases/RecordFeatureExtraction.py
13
13
  SleePyPhases/Reporter.py
14
14
  SleePyPhases/SignalPreprocessing.py
15
+ SleePyPhases/SleePyPhases.py
15
16
  SleePyPhases/SleepMetaData.py
16
17
  SleePyPhases/__init__.py
18
+ SleePyPhases/config.yaml
17
19
  SleePyPhases.egg-info/PKG-INFO
18
20
  SleePyPhases.egg-info/SOURCES.txt
19
21
  SleePyPhases.egg-info/dependency_links.txt
@@ -46,6 +48,7 @@ tests/conftest.py
46
48
  tests/unit/__init__.py
47
49
  tests/unit/test_BuildDataset.py
48
50
  tests/unit/test_BuildDatasetFolded.py
51
+ tests/unit/test_BuildDatasetMetadataSync.py
49
52
  tests/unit/test_BuildDatasetSegments.py
50
53
  tests/unit/test_DataManipulation.py
51
54
  tests/unit/test_Eval.py
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="SleePyPhases",
8
- version="v0.6.2"[1:],
8
+ version="v0.7.0"[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",
@@ -1,4 +1,15 @@
1
1
  # Setup project for unittest discovery
2
+ import sys
3
+ import os
4
+
5
+ # Ensure SleePyPhases is imported as a package before loadClass adds
6
+ # the inner directory to sys.path (which would shadow the package with SleePyPhases.py)
7
+ _cwd = os.getcwd()
8
+ _inner = os.path.join(_cwd, "SleePyPhases")
9
+ if _inner in sys.path:
10
+ sys.path.remove(_inner)
11
+ import SleePyPhases # noqa: E402,F401 — force package registration
12
+
2
13
  from phases.commands.test import Test as PhasesTest
3
14
  from pyPhases.test import TestCase
4
15
 
@@ -0,0 +1,261 @@
1
+ """
2
+ Acceptance tests: verify that recordMetadata stays in sync with the actual
3
+ data items throughout the full pyPhases pipeline – including datafold with
4
+ multiple datasets, shuffle, and deterministic re-loading.
5
+
6
+ Scenario
7
+ --------
8
+ - 3 datasets via ``datafold.datasets``, each contributing 5 recordings
9
+ (recordIds "d0-0"…"d0-4", "d1-0"…"d1-4", "d2-0"…"d2-4").
10
+ - Each recording's X data embeds the *flat record index* so we can tell
11
+ which record a data item came from.
12
+ - A custom DataManipulation subclass (injected via Swappable.setClass)
13
+ adds a ``checkMetadata`` step that, for every item, records what
14
+ ``self.recordMetadata[self.currentIndex]`` returns versus the recordId
15
+ derivable from the data payload.
16
+ - We pull data through ``project.getData("dataset-training")`` — the
17
+ same code path the real Training phase uses.
18
+ """
19
+
20
+ from unittest.mock import MagicMock
21
+
22
+ import numpy as np
23
+ import pandas as pd
24
+
25
+ from pyPhases.test.Mocks import OverwriteConfig
26
+ from pyPhases.test.TestCase import TestCase
27
+ from pyPhasesRecordloader import RecordLoader
28
+
29
+ from SleePyPhases.DataManipulation import DataManipulation
30
+ from SleePyPhases.phases.BuildDataset import BuildDataset
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Helpers
35
+ # ---------------------------------------------------------------------------
36
+
37
+ # Accumulates (index, metadataRecordId, dataValue) triples that the custom
38
+ # manipulation step records while the pipeline iterates.
39
+ _metadata_checks: list = []
40
+
41
+
42
+ class CheckingDataManipulation(DataManipulation):
43
+ """DataManipulation subclass with an extra ``checkMetadata`` step.
44
+
45
+ For every item it reads ``self.recordMetadata[self.currentIndex]`` and
46
+ compares the ``recordId`` to the flat-index value embedded in the X
47
+ data (after ``addBatchDimension`` + ``fixedSize`` centering to pos 5).
48
+ """
49
+
50
+ def checkMetadata(self, X, Y):
51
+ meta = self.recordMetadata[self.currentIndex]
52
+ metadataRecordId = meta["recordId"]
53
+ dataValue = int(X[0, 5, 0]) # flat index encoded in the signal
54
+ _metadata_checks.append(
55
+ (self.currentIndex, metadataRecordId, dataValue)
56
+ )
57
+ return X, Y
58
+
59
+
60
+ # Per-dataset record ids: 3 datasets × 5 records
61
+ ALL_IDS = [[f"d{d}-{r}" for r in range(7)] for d in range(3)]
62
+ FLAT_IDS = [rid for ds in ALL_IDS for rid in ds] # 15 ids in flat order
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Test class
67
+ # ---------------------------------------------------------------------------
68
+
69
+ class TestBuildDatasetMetadataSync(TestCase):
70
+ phase = BuildDataset()
71
+
72
+ def config(self):
73
+ self.project.setConfig("dataversion.split", {})
74
+ return {
75
+ "useLoader": "test",
76
+ "dataversion": {
77
+ "seed": 2023,
78
+ "recordIds": FLAT_IDS,
79
+ "split": {"training": ["0:15"]},
80
+ "groupBy": None,
81
+ },
82
+ "segmentManipulation": [
83
+ {"name": "addBatchDimension"},
84
+ {"name": "fixedSize", "position": "center", "size": 10},
85
+ {"name": "checkMetadata"},
86
+ ],
87
+ "segmentManipulationEval": [
88
+ {"name": "addBatchDimension"},
89
+ {"name": "fixedSize", "position": "center", "size": 10},
90
+ {"name": "checkMetadata"},
91
+ ],
92
+ "evalOn": None,
93
+ "datafold": {
94
+ "shuffle": False,
95
+ "seed": 42,
96
+ "datasets": [
97
+ {"config": {"dataversion": {"recordIds": ALL_IDS[0], "seed": 1, "split": {"training": ["0:5"], "validation": ["5:7"]}, "groupBy": None}}},
98
+ {"config": {"dataversion": {"recordIds": ALL_IDS[1], "seed": 2, "split": {"training": ["0:5"], "validation": ["5:7"]}, "groupBy": None}}},
99
+ {"config": {"dataversion": {"recordIds": ALL_IDS[2], "seed": 3, "split": {"training": ["0:5"], "validation": ["5:7"]}, "groupBy": None}}},
100
+ ],
101
+ },
102
+ }
103
+
104
+ def setUp(self):
105
+ super().setUp()
106
+ DataManipulation.setClass(CheckingDataManipulation)
107
+
108
+ metadata = pd.DataFrame([{"recordId": rid} for rid in FLAT_IDS])
109
+
110
+ RecordLoader.getRecordList = MagicMock(return_value=FLAT_IDS)
111
+
112
+ # Register data for each datafold dataset context — mirrors what
113
+ # buildDataRecords() does with ``with self.project:`` + updateConfig.
114
+ # Each dataset gets its own data arrays (like separate memmaps in prod).
115
+ datasets = self.project.getConfig("datafold.datasets", [{"config": {}}])
116
+ for dsIdx, ds in enumerate(datasets):
117
+ overwriteConfig = ds["config"]
118
+ with self.project:
119
+ if "dataversion" in overwriteConfig:
120
+ self.project.setConfig("dataversion", {})
121
+ self.project.updateConfig(overwriteConfig)
122
+
123
+ dsRecordIds = self.project.getConfig("dataversion.recordIds")
124
+ # Each dataset's memmap only contains its own records.
125
+ # Values are the *global flat index* so we can trace which
126
+ # record a data item originally belonged to.
127
+ dsStart = dsIdx * 7
128
+ dsProcessed = np.arange(dsStart, dsStart + len(dsRecordIds)).reshape(-1, 1, 1)
129
+ dsFeatures = np.arange(dsStart, dsStart + len(dsRecordIds)).reshape(-1, 1, 1)
130
+
131
+ self.project.registerData("allDBRecordIds", {rid: [rid] for rid in dsRecordIds})
132
+ self.project.registerData("metadata", metadata)
133
+ self.project.registerData("data-processed", dsProcessed)
134
+ self.project.registerData("data-features", dsFeatures)
135
+
136
+ def tearDown(self):
137
+ DataManipulation.setClass(None)
138
+ super().tearDown()
139
+
140
+ # -- helpers ----------------------------------------------------------
141
+
142
+ def _drain(self, dataset):
143
+ """Iterate the full dataset, return list of (x, y)."""
144
+ return [item for item in dataset]
145
+
146
+ def _data_order(self, items):
147
+ """Return the list of flat-index values read from the X payloads."""
148
+ return [int(x[0, 5, 0]) for x, _ in items]
149
+
150
+ def _assert_metadata_matches_data(self, label=""):
151
+ """Assert every recorded (metadata, data) pair agrees."""
152
+ mismatches = []
153
+ for idx, metaId, dataVal in _metadata_checks:
154
+ expected_flat = FLAT_IDS.index(metaId)
155
+ if expected_flat != dataVal:
156
+ mismatches.append(
157
+ f" index {idx}: metadata={metaId} (flat {expected_flat}), data={dataVal}"
158
+ )
159
+ if mismatches:
160
+ self.fail(
161
+ f"{label}{len(mismatches)} metadata/data mismatches:\n"
162
+ + "\n".join(mismatches)
163
+ )
164
+
165
+ # =====================================================================
166
+ # 1) No shuffle — metadata must match data for every item
167
+ # =====================================================================
168
+
169
+ def testNoShuffle_MetadataMatchesData(self):
170
+ """Without shuffle the metadata recordId at each index must equal
171
+ the recordId derivable from the data payload."""
172
+ _metadata_checks.clear()
173
+ items = self._drain(self.getData("dataset-training"))
174
+ self.assertEqual(len(items), 15)
175
+ self._assert_metadata_matches_data("no-shuffle: ")
176
+
177
+ def testNoShuffle_MetadataMatchesDataValidation(self):
178
+ """Without shuffle the metadata recordId at each index must equal
179
+ the recordId derivable from the data payload."""
180
+ _metadata_checks.clear()
181
+ items = self._drain(self.getData("dataset-validation"))
182
+ self.assertEqual(len(items), 6)
183
+ self._assert_metadata_matches_data("no-shuffle: ")
184
+
185
+ # =====================================================================
186
+ # 2) No shuffle — deterministic across two loads
187
+ # =====================================================================
188
+
189
+ def testNoShuffle_DeterministicOrder(self):
190
+ """Two consecutive loads without shuffle must return data in
191
+ identical order."""
192
+ items1 = self._drain(self.getData("dataset-training"))
193
+ order1 = self._data_order(items1)
194
+
195
+ items2 = self._drain(self.getData("dataset-training"))
196
+ order2 = self._data_order(items2)
197
+
198
+ self.assertEqual(order1, order2)
199
+
200
+ def testNoShuffle_DeterministicOrderValidation(self):
201
+ """Two consecutive loads without shuffle must return validation data in
202
+ identical order."""
203
+ items1 = self._drain(self.getData("dataset-validation"))
204
+ order1 = self._data_order(items1)
205
+
206
+ items2 = self._drain(self.getData("dataset-validation"))
207
+ order2 = self._data_order(items2)
208
+
209
+ self.assertEqual(order1, order2)
210
+
211
+ # =====================================================================
212
+ # 3) With shuffle — load twice, verify deterministic order via seed
213
+ # =====================================================================
214
+
215
+ @OverwriteConfig({"datafold": {"shuffle": True, "seed": 42}})
216
+ def testShuffle_DeterministicOrder(self):
217
+ """Loading the dataset twice with the same seed must yield
218
+ identical item order both times."""
219
+ items1 = self._drain(self.getData("dataset-training"))
220
+ order1 = self._data_order(items1)
221
+
222
+ items2 = self._drain(self.getData("dataset-training"))
223
+ order2 = self._data_order(items2)
224
+
225
+ self.assertEqual(order1, order2,
226
+ "Same seed must produce the same item order")
227
+
228
+ @OverwriteConfig({"datafold": {"shuffle": True, "seed": 42}})
229
+ def testShuffle_DeterministicOrderValidation(self):
230
+ """Loading the validation dataset twice with the same seed must yield
231
+ identical item order both times."""
232
+ items1 = self._drain(self.getData("dataset-validation"))
233
+ order1 = self._data_order(items1)
234
+
235
+ items2 = self._drain(self.getData("dataset-validation"))
236
+ order2 = self._data_order(items2)
237
+
238
+ self.assertEqual(order1, order2,
239
+ "Same seed must produce the same item order")
240
+
241
+ # =====================================================================
242
+ # 4) With shuffle — metadata must still match data for every item
243
+ # =====================================================================
244
+
245
+ @OverwriteConfig({"datafold": {"shuffle": True, "seed": 42}})
246
+ def testShuffle_MetadataMatchesData(self):
247
+ """After shuffle, recordMetadata[i].recordId must describe the
248
+ record that data item i actually contains."""
249
+ _metadata_checks.clear()
250
+ items = self._drain(self.getData("dataset-training"))
251
+ self.assertEqual(len(items), 15)
252
+ self._assert_metadata_matches_data("shuffled: ")
253
+
254
+ @OverwriteConfig({"datafold": {"shuffle": True, "seed": 42}})
255
+ def testShuffle_MetadataMatchesDataValidation(self):
256
+ """After shuffle, recordMetadata[i].recordId must describe the
257
+ record that validation data item i actually contains."""
258
+ _metadata_checks.clear()
259
+ items = self._drain(self.getData("dataset-validation"))
260
+ self.assertEqual(len(items), 6)
261
+ self._assert_metadata_matches_data("shuffled-validation: ")
@@ -207,7 +207,7 @@ class TestDataManipulation(unittest.TestCase):
207
207
 
208
208
  def test_deleteIgnored(self):
209
209
  testY = np.array([1, 2, 3, -1, 2, 2, 3, -1, 2, -1, 2]).reshape(1, -1, 1)
210
- testX = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]).reshape(1, -1, 1)
210
+ testX = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]).reshape(1, -1, 1)
211
211
  newX, newY = self.getDA().deleteIgnored(testX, testY)
212
212
  npt.assert_equal(newY.reshape(-1), [1, 2, 3, 2, 2, 3, 2, 2])
213
213
  npt.assert_equal(newX.reshape(-1), [1, 2, 3, 5, 6, 7, 9, 11])
File without changes
File without changes
File without changes