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.
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/PKG-INFO +1 -1
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/EventScorer.py +2 -2
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/FeatureExtraction.py +1 -1
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/Plugin.py +3 -2
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/SleePyPhases.py +2 -2
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/SleepMetaData.py +7 -3
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/Eval.py +6 -15
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/EvalReport.py +21 -12
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/Extract.py +30 -6
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/ExtractFeatures.py +100 -12
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/Predict.py +8 -5
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/Setup.py +25 -2
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/ThresholdOptimisation.py +6 -5
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/Training.py +33 -5
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/Validation.py +1 -1
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases.egg-info/PKG-INFO +1 -1
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/setup.py +1 -1
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_Extract.py +1 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/LICENSE +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/README.md +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/DataManipulation.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/MultiScorer.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/Plot.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/PreManipulation.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/RecordFeatureExtraction.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/Reporter.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/SignalPreprocessing.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/__init__.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/config.yaml +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/BuildDataset.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/DataAnalysis.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/DataReport.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/EvalPlotExamples.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/ExportMetadataToMeticalDB.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/ExtractEvents.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/ExtractRecordFeatures.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/GatherMetadata.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/LogCleanup.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/LogOverview.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/TestRun.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/VisualizeConfig.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases/phases/__init__.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases.egg-info/SOURCES.txt +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases.egg-info/dependency_links.txt +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases.egg-info/requires.txt +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/SleePyPhases.egg-info/top_level.txt +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/pyproject.toml +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/setup.cfg +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/__init__.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/conftest.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/__init__.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_BuildDataset.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_BuildDatasetFolded.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_BuildDatasetMetadataSync.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_BuildDatasetSegments.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_DataManipulation.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_Eval.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_EventScorer.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_PreManipulation.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_RecordFeatureExtraction.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_RecordProcessor.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_Setup.py +0 -0
- {sleepyphases-0.7.0 → sleepyphases-0.8.2}/tests/unit/test_SignalPreprocessing.py +0 -0
|
@@ -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,
|
|
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
|
-
|
|
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.
|
|
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",
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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,
|
|
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("
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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().
|
|
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,
|
|
24
|
+
p = model.predict(x, returnNumpy=True)
|
|
25
25
|
scorer.score(t, p, trace=True)
|
|
26
26
|
|
|
27
27
|
res = scorer.scoreAllRecords()
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
|
|
|
5
5
|
|
|
6
6
|
setuptools.setup(
|
|
7
7
|
name="SleePyPhases",
|
|
8
|
-
version="v0.
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|