sarc-asm 0.2.2__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sarc-asm might be problematic. Click here for more details.
- contraction_net/__init__.py +4 -0
- contraction_net/annotation.py +148 -0
- contraction_net/contraction_net.py +81 -0
- contraction_net/data.py +158 -0
- contraction_net/losses.py +97 -0
- contraction_net/prediction.py +41 -0
- contraction_net/training.py +171 -0
- contraction_net/utils.py +248 -0
- sarc_asm-0.2.2.dist-info/LICENSE +27 -0
- sarc_asm-0.2.2.dist-info/METADATA +18 -0
- sarc_asm-0.2.2.dist-info/RECORD +64 -0
- sarc_asm-0.2.2.dist-info/WHEEL +4 -0
- sarcasm/.DS_Store +0 -0
- sarcasm/__init__.py +25 -0
- sarcasm/_version.py +20 -0
- sarcasm/core.py +624 -0
- sarcasm/core_omezarr.py +801 -0
- sarcasm/exceptions.py +17 -0
- sarcasm/export.py +464 -0
- sarcasm/feature_dict.py +743 -0
- sarcasm/ioutils.py +112 -0
- sarcasm/meta_data_handler.py +120 -0
- sarcasm/meta_data_handler_omezarr.py +243 -0
- sarcasm/models/model_ContractionNet.pt +0 -0
- sarcasm/models/model_sarcomeres_generalist.pt +0 -0
- sarcasm/models/model_z_bands_unet3d.pt +0 -0
- sarcasm/motion.py +938 -0
- sarcasm/plot_utils.py +302 -0
- sarcasm/plots.py +1615 -0
- sarcasm/py.typed +0 -0
- sarcasm/structure.py +2668 -0
- sarcasm/structure_xarray.py +2899 -0
- sarcasm/training_data_generation.py +1041 -0
- sarcasm/type_utils.py +34 -0
- sarcasm/utils.py +1276 -0
- sarcasm_app/__init__.py +256 -0
- sarcasm_app/__main__.py +24 -0
- sarcasm_app/control/__init__.py +17 -0
- sarcasm_app/control/application_control.py +579 -0
- sarcasm_app/control/batch_processing_control.py +495 -0
- sarcasm_app/control/chain_execution.py +89 -0
- sarcasm_app/control/file_selection_control.py +322 -0
- sarcasm_app/control/loi_analysis_control.py +202 -0
- sarcasm_app/control/motion_analysis_control.py +320 -0
- sarcasm_app/control/popup_export.py +295 -0
- sarcasm_app/control/structure_analysis_control.py +505 -0
- sarcasm_app/icons/sarcasm.icns +0 -0
- sarcasm_app/icons/sarcasm.ico +0 -0
- sarcasm_app/model/__init__.py +356 -0
- sarcasm_app/model/parameter.py +148 -0
- sarcasm_app/model/parameters.py +60 -0
- sarcasm_app/py.typed +0 -0
- sarcasm_app/ui-files/file_selection.ui +212 -0
- sarcasm_app/ui-files/parameters_batch_processing.ui +276 -0
- sarcasm_app/ui-files/parameters_loi_analysis.ui +680 -0
- sarcasm_app/ui-files/parameters_motion_analysis.ui +740 -0
- sarcasm_app/ui-files/parameters_structure_analysis.ui +1199 -0
- sarcasm_app/ui-files/update_py_files.bat +5 -0
- sarcasm_app/view/__init__.py +0 -0
- sarcasm_app/view/file_selection.py +146 -0
- sarcasm_app/view/parameters_batch_processing.py +174 -0
- sarcasm_app/view/parameters_loi_analysis.py +371 -0
- sarcasm_app/view/parameters_motion_analysis.py +404 -0
- sarcasm_app/view/parameters_structure_analysis.py +643 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import os
|
|
3
|
+
import numpy as np
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
from matplotlib.widgets import Button
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TimeSeriesAnnotator:
|
|
9
|
+
"""
|
|
10
|
+
Class to annotate contraction intervals in multiple time-series files and save the results.
|
|
11
|
+
|
|
12
|
+
This class allows users to interactively annotate the start and end of contractions
|
|
13
|
+
in time-series data by clicking on the plot. The user can press the left mouse button
|
|
14
|
+
at the start of a contraction interval and release at the end of the contraction interval.
|
|
15
|
+
The results are saved to a specified output directory.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
file_list : list of str
|
|
20
|
+
List of file paths to the time-series data files to be annotated.
|
|
21
|
+
output_dir : str
|
|
22
|
+
Directory where the output files with annotated contraction intervals will be saved.
|
|
23
|
+
figsize : tuple, optional
|
|
24
|
+
Figure size in inches.
|
|
25
|
+
|
|
26
|
+
Methods
|
|
27
|
+
-------
|
|
28
|
+
load_next_file()
|
|
29
|
+
Load the next time-series file for annotation.
|
|
30
|
+
press_callback(event)
|
|
31
|
+
Record the x-coordinate (time) where the mouse button is pressed.
|
|
32
|
+
release_callback(event)
|
|
33
|
+
Record the x-coordinate (time) where the mouse button is released.
|
|
34
|
+
save_and_load_next(event)
|
|
35
|
+
Save the current annotations and load the next time-series file.
|
|
36
|
+
reset_annotations(event)
|
|
37
|
+
Reset the current annotations for the current time-series file.
|
|
38
|
+
|
|
39
|
+
Notes
|
|
40
|
+
-----
|
|
41
|
+
In an interactive Jupyter notebook, add the following before running the TimeSeriesAnnotator:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
matplotlib.use('nbagg')
|
|
45
|
+
%matplotlib notebook
|
|
46
|
+
```
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, file_list, output_dir, figsize=(11, 3.5)):
|
|
50
|
+
self.file_list = [f for f in file_list if not f.endswith('_contr.txt')]
|
|
51
|
+
self.output_dir = output_dir
|
|
52
|
+
self.current_file_index = 0
|
|
53
|
+
self.start_contraction = []
|
|
54
|
+
self.end_contraction = []
|
|
55
|
+
self.fig, self.ax = plt.subplots(figsize=figsize, constrained_layout=True)
|
|
56
|
+
self.load_next_file()
|
|
57
|
+
|
|
58
|
+
self.fig.canvas.mpl_connect('button_press_event', self.press_callback)
|
|
59
|
+
self.fig.canvas.mpl_connect('button_release_event', self.release_callback)
|
|
60
|
+
|
|
61
|
+
save_ax = self.fig.add_axes([0.85, 0.01, 0.1, 0.075])
|
|
62
|
+
self.save_button = Button(save_ax, 'Save & Next', color='lightgoldenrodyellow', hovercolor='0.975')
|
|
63
|
+
self.save_button.on_clicked(self.save_and_load_next)
|
|
64
|
+
|
|
65
|
+
reset_ax = self.fig.add_axes([0.75, 0.01, 0.1, 0.075])
|
|
66
|
+
self.reset_button = Button(reset_ax, 'Reset', color='lightcoral', hovercolor='0.975')
|
|
67
|
+
self.reset_button.on_clicked(self.reset_annotations)
|
|
68
|
+
|
|
69
|
+
plt.show()
|
|
70
|
+
|
|
71
|
+
def load_next_file(self):
|
|
72
|
+
"""Load the next time-series file for annotation."""
|
|
73
|
+
if self.current_file_index < len(self.file_list):
|
|
74
|
+
file_path = self.file_list[self.current_file_index]
|
|
75
|
+
self.data = np.loadtxt(file_path)
|
|
76
|
+
self.ax.clear()
|
|
77
|
+
self.ax.plot(self.data, c='k')
|
|
78
|
+
self.ax.set_title(f'Annotating {os.path.basename(file_path)}')
|
|
79
|
+
self.start_contraction = []
|
|
80
|
+
self.end_contraction = []
|
|
81
|
+
plt.draw()
|
|
82
|
+
else:
|
|
83
|
+
plt.close(self.fig)
|
|
84
|
+
print('All files annotated.')
|
|
85
|
+
|
|
86
|
+
def press_callback(self, event):
|
|
87
|
+
"""Record the x-coordinate (time) where the mouse button is pressed."""
|
|
88
|
+
self.ax.axvline(event.xdata, c='r', linestyle=':')
|
|
89
|
+
self.start_contraction.append(event.xdata)
|
|
90
|
+
|
|
91
|
+
def release_callback(self, event):
|
|
92
|
+
"""Record the x-coordinate (time) where the mouse button is released."""
|
|
93
|
+
self.ax.axvline(event.xdata, c='r', linestyle=':')
|
|
94
|
+
self.ax.axvspan(self.start_contraction[-1], event.xdata, alpha=0.5, color='red')
|
|
95
|
+
self.end_contraction.append(event.xdata)
|
|
96
|
+
|
|
97
|
+
def save_and_load_next(self, event):
|
|
98
|
+
"""Save the current annotations and load the next time-series file."""
|
|
99
|
+
if self.current_file_index < len(self.file_list):
|
|
100
|
+
file_path = self.file_list[self.current_file_index]
|
|
101
|
+
output_file = os.path.join(self.output_dir, f'{os.path.basename(file_path).split(".")[0]}_contr.txt')
|
|
102
|
+
start_end_contraction = np.asarray([self.start_contraction, self.end_contraction])
|
|
103
|
+
np.savetxt(output_file, start_end_contraction.T, header='start end', comments='', fmt='%f')
|
|
104
|
+
print(f'Saved contractions for {os.path.basename(file_path)} to {output_file}')
|
|
105
|
+
|
|
106
|
+
self.current_file_index += 1
|
|
107
|
+
self.load_next_file()
|
|
108
|
+
|
|
109
|
+
def reset_annotations(self, event):
|
|
110
|
+
"""Reset the current annotations for the current time-series file."""
|
|
111
|
+
self.start_contraction = []
|
|
112
|
+
self.end_contraction = []
|
|
113
|
+
self.ax.clear()
|
|
114
|
+
self.ax.plot(self.data, c='k')
|
|
115
|
+
self.ax.set_title(f'Annotating {os.path.basename(self.file_list[self.current_file_index])}')
|
|
116
|
+
plt.draw()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def split_trace(filepath, output_dir, chunk_size=512, p=1):
|
|
120
|
+
"""
|
|
121
|
+
Split a time-series trace into smaller chunks and save them to separate files.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
filepath : str
|
|
126
|
+
Path to the input time-series data file.
|
|
127
|
+
output_dir : str
|
|
128
|
+
Directory where the output chunk files will be saved.
|
|
129
|
+
chunk_size : int, optional
|
|
130
|
+
The size of each chunk, by default 512.
|
|
131
|
+
p : float, optional
|
|
132
|
+
Probability of saving each chunk, by default 1 (save all chunks).
|
|
133
|
+
"""
|
|
134
|
+
# Read the time-series data
|
|
135
|
+
data = np.loadtxt(filepath)
|
|
136
|
+
|
|
137
|
+
# Create the output directory if it doesn't exist
|
|
138
|
+
if not os.path.exists(output_dir):
|
|
139
|
+
os.makedirs(output_dir)
|
|
140
|
+
|
|
141
|
+
# Calculate the number of chunks
|
|
142
|
+
num_chunks = len(data) // chunk_size + (len(data) % chunk_size > 0)
|
|
143
|
+
|
|
144
|
+
# Split the data into chunks of size `chunk_size` and save each chunk to a separate file
|
|
145
|
+
for i in range(num_chunks):
|
|
146
|
+
if random.random() < p:
|
|
147
|
+
chunk = data[i * chunk_size:(i + 1) * chunk_size]
|
|
148
|
+
np.savetxt(os.path.join(output_dir, f'{os.path.basename(filepath).split(".")[0]}_{i}.txt'), chunk)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
import torch.nn.functional as F
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ContractionNet(nn.Module):
|
|
7
|
+
"""
|
|
8
|
+
ContractionNet model for detecting contraction intervals from time-series data of individual Z-band positions
|
|
9
|
+
and sarcomere lengths of beating cardiomyocytes.
|
|
10
|
+
|
|
11
|
+
This neural network is designed to handle noisy data and distinguish between contracting and non-contracting intervals.
|
|
12
|
+
The network first extracts various features from a single input time-series by two convolutional layers with kernel size 5, followed by a dilated convolution in the third layer
|
|
13
|
+
to capture broader temporal patterns. Each convolution is followed by instance normalization and ReLU activation.
|
|
14
|
+
A self-attention layer enhances focus on salient features. The processed signal then undergoes two further
|
|
15
|
+
convolutions before being outputted through a sigmoid activation function.
|
|
16
|
+
|
|
17
|
+
Methods
|
|
18
|
+
-------
|
|
19
|
+
forward(x)
|
|
20
|
+
Forward pass through the network.
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self, n_filter=64, in_channels=1, out_channels=2, dropout_rate=0.5):
|
|
23
|
+
"""
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
n_filter : int, optional
|
|
27
|
+
Number of filters in the convolutional layers (default is 64).
|
|
28
|
+
in_channels : int, optional
|
|
29
|
+
Number of input channels (default is 1).
|
|
30
|
+
out_channels : int, optional
|
|
31
|
+
Number of output channels (default is 2).
|
|
32
|
+
dropout_rate : float, optional
|
|
33
|
+
Dropout rate (default is 0.5).
|
|
34
|
+
"""
|
|
35
|
+
super(ContractionNet, self).__init__()
|
|
36
|
+
self.conv1 = nn.Conv1d(in_channels=in_channels, out_channels=n_filter, kernel_size=5, padding=2)
|
|
37
|
+
self.in1 = nn.InstanceNorm1d(n_filter)
|
|
38
|
+
self.conv2 = nn.Conv1d(in_channels=n_filter, out_channels=n_filter * 2, kernel_size=5, padding=2)
|
|
39
|
+
self.bn2 = nn.InstanceNorm1d(n_filter * 2)
|
|
40
|
+
self.conv3 = nn.Conv1d(in_channels=n_filter * 2, out_channels=n_filter * 4, kernel_size=5, padding=4,
|
|
41
|
+
dilation=2)
|
|
42
|
+
self.bn3 = nn.InstanceNorm1d(n_filter * 4)
|
|
43
|
+
self.attention = nn.MultiheadAttention(embed_dim=n_filter * 4, num_heads=4, batch_first=True)
|
|
44
|
+
self.norm1 = nn.LayerNorm(n_filter * 4)
|
|
45
|
+
self.dropout_attention = nn.Dropout(dropout_rate)
|
|
46
|
+
self.conv4 = nn.Conv1d(in_channels=n_filter * 4, out_channels=n_filter * 2, kernel_size=5, padding=2,
|
|
47
|
+
dilation=1)
|
|
48
|
+
self.bn4 = nn.InstanceNorm1d(n_filter * 2)
|
|
49
|
+
self.dropout_pre_output = nn.Dropout(dropout_rate)
|
|
50
|
+
self.conv_out = nn.Conv1d(in_channels=n_filter * 2, out_channels=out_channels, kernel_size=1)
|
|
51
|
+
|
|
52
|
+
def forward(self, x):
|
|
53
|
+
"""
|
|
54
|
+
Forward pass through the network.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
x : torch.Tensor
|
|
59
|
+
Input tensor of shape (batch_size, in_channels, sequence_length).
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
torch.Tensor
|
|
64
|
+
Output tensor of shape (batch_size, out_channels, sequence_length) after sigmoid activation.
|
|
65
|
+
torch.Tensor
|
|
66
|
+
Raw output tensor of shape (batch_size, out_channels, sequence_length).
|
|
67
|
+
"""
|
|
68
|
+
x = F.relu(self.in1(self.conv1(x)))
|
|
69
|
+
x = F.relu(self.bn2(self.conv2(x)))
|
|
70
|
+
x = F.relu(self.bn3(self.conv3(x)))
|
|
71
|
+
x = x.transpose(1, 2)
|
|
72
|
+
residual = x
|
|
73
|
+
x, attention_weights = self.attention(x, x, x)
|
|
74
|
+
x = x + residual
|
|
75
|
+
x = self.norm1(x)
|
|
76
|
+
x = self.dropout_attention(x)
|
|
77
|
+
x = x.transpose(1, 2)
|
|
78
|
+
x = F.relu(self.bn4(self.conv4(x)))
|
|
79
|
+
x = self.dropout_pre_output(x)
|
|
80
|
+
x = self.conv_out(x)
|
|
81
|
+
return torch.sigmoid(x), x
|
contraction_net/data.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import torch
|
|
6
|
+
from torch.utils.data import Dataset
|
|
7
|
+
|
|
8
|
+
from .utils import distance_transform
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DataProcess(Dataset):
|
|
12
|
+
"""
|
|
13
|
+
A Dataset class for creating training data objects for ContractionNet training.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
source_dir : Tuple[str, str]
|
|
18
|
+
Tuple containing paths to the directories of training data [images, labels]. Images should be in .tif format.
|
|
19
|
+
input_len : int, optional
|
|
20
|
+
Length of the input sequences (default is 512).
|
|
21
|
+
normalize : bool, optional
|
|
22
|
+
Whether to normalize each time-series (default is False).
|
|
23
|
+
aug_factor : int, optional
|
|
24
|
+
Factor for image augmentation (default is 10).
|
|
25
|
+
val_split : float, optional
|
|
26
|
+
Validation split for training (default is 0.2).
|
|
27
|
+
noise_amp : float, optional
|
|
28
|
+
Amplitude of Gaussian noise for image augmentation (default is 0.2).
|
|
29
|
+
aug_p : float, optional
|
|
30
|
+
Probability of applying augmentation (default is 0.5).
|
|
31
|
+
random_offset : float, optional
|
|
32
|
+
Amplitude of random offset applied to the input sequences (default is 0.25).
|
|
33
|
+
random_outlier : float, optional
|
|
34
|
+
Amplitude of random outliers added to the input sequences (default is 0.5).
|
|
35
|
+
random_drift : Tuple[float, float], optional
|
|
36
|
+
Parameters for random drift: (frequency, amplitude) (default is (0.01, 0.2)).
|
|
37
|
+
random_swap : float, optional
|
|
38
|
+
Probability of randomly swapping the sign of the input sequences (default is 0.5).
|
|
39
|
+
random_subsampling : Tuple[int, int], optional
|
|
40
|
+
Range for random subsampling intervals (default is None).
|
|
41
|
+
|
|
42
|
+
Methods
|
|
43
|
+
-------
|
|
44
|
+
__len__()
|
|
45
|
+
Returns the total number of samples.
|
|
46
|
+
__getitem__(idx)
|
|
47
|
+
Generates one sample of data.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
def __init__(self, source_dir, input_len=512, normalize=False, val_split=0.2, aug_factor=10, aug_p=0.5,
|
|
51
|
+
noise_amp=0.2, random_offset=0.25, random_outlier=0.5, random_drift=(0.01, 0.2), random_swap=0.5,
|
|
52
|
+
random_subsampling=None):
|
|
53
|
+
self.source_dir = source_dir
|
|
54
|
+
self.data = []
|
|
55
|
+
self.is_real = []
|
|
56
|
+
self.input_len = input_len
|
|
57
|
+
self.val_split = val_split
|
|
58
|
+
self.normalize = normalize
|
|
59
|
+
self.aug_factor = aug_factor
|
|
60
|
+
self.aug_p = aug_p
|
|
61
|
+
self.noise_amp = noise_amp
|
|
62
|
+
self.random_offset = random_offset
|
|
63
|
+
self.random_drift = random_drift
|
|
64
|
+
self.random_outlier = random_outlier
|
|
65
|
+
self.random_subsampling = random_subsampling
|
|
66
|
+
self.random_swap = random_swap
|
|
67
|
+
self.mode = 'train'
|
|
68
|
+
self.__load_and_edit()
|
|
69
|
+
if self.aug_factor is not None:
|
|
70
|
+
self.__augment()
|
|
71
|
+
|
|
72
|
+
def __load_and_edit(self):
|
|
73
|
+
"""
|
|
74
|
+
Loads and preprocesses the input data files from the source directory.
|
|
75
|
+
"""
|
|
76
|
+
files = glob.glob(self.source_dir + '*.txt')
|
|
77
|
+
print(f'{len(files)} files found')
|
|
78
|
+
files_input = [f for f in files if 'peaks' not in f and 'contr' not in os.path.basename(f)]
|
|
79
|
+
for file_i in files_input:
|
|
80
|
+
input_i = np.loadtxt(file_i)[: self.input_len]
|
|
81
|
+
if self.normalize:
|
|
82
|
+
input_i -= np.mean(input_i)
|
|
83
|
+
input_i /= np.std(input_i)
|
|
84
|
+
start_end_systole_i = np.loadtxt(file_i[:-4] + '_contr.txt')[: self.input_len]
|
|
85
|
+
if len(start_end_systole_i) == len(input_i):
|
|
86
|
+
contr_i = start_end_systole_i
|
|
87
|
+
else:
|
|
88
|
+
contr_i = np.zeros_like(input_i)
|
|
89
|
+
start_end_systole_i = np.clip(start_end_systole_i, a_min=0, a_max=self.input_len)
|
|
90
|
+
if start_end_systole_i.size != 0:
|
|
91
|
+
if len(start_end_systole_i.shape) == 1:
|
|
92
|
+
contr_i[int(start_end_systole_i[0]): int(start_end_systole_i[1])] = 1
|
|
93
|
+
elif len(start_end_systole_i.shape) == 2:
|
|
94
|
+
for start, end in start_end_systole_i.T:
|
|
95
|
+
contr_i[int(start): int(end)] = 1
|
|
96
|
+
if 'sim' in file_i or 'noise' in file_i:
|
|
97
|
+
is_real_i = False
|
|
98
|
+
else:
|
|
99
|
+
is_real_i = True
|
|
100
|
+
self.data.append([input_i, contr_i, distance_transform(input_i, contr_i)])
|
|
101
|
+
self.is_real.append(is_real_i)
|
|
102
|
+
self.data = np.asarray(self.data)
|
|
103
|
+
|
|
104
|
+
def __augment(self):
|
|
105
|
+
"""
|
|
106
|
+
Applies data augmentation techniques to the loaded data.
|
|
107
|
+
"""
|
|
108
|
+
_data = self.data.copy()
|
|
109
|
+
self.data = []
|
|
110
|
+
for i, d_i in enumerate(_data):
|
|
111
|
+
for j in range(self.aug_factor):
|
|
112
|
+
d_ij = d_i.copy()
|
|
113
|
+
std_ij = np.std(d_ij[0])
|
|
114
|
+
d_ij[0] = d_ij[0] / std_ij
|
|
115
|
+
d_ij[0] = np.random.choice([-1, 1], p=(1-self.random_swap, self.random_swap)) * d_ij[0]
|
|
116
|
+
if self.random_subsampling is not None and np.random.binomial(1, self.aug_p):
|
|
117
|
+
d_ij = np.zeros_like(d_ij)
|
|
118
|
+
d_ij_short = np.delete(d_i, np.arange(0, 512, np.random.randint(self.random_subsampling[0],
|
|
119
|
+
self.random_subsampling[1])),
|
|
120
|
+
axis=1)
|
|
121
|
+
d_ij[:, :d_ij_short.shape[1]] = d_ij_short
|
|
122
|
+
if not self.is_real[i] and np.random.binomial(1, self.aug_p):
|
|
123
|
+
d_ij[0] += np.random.normal(0, self.noise_amp, size=d_i.shape[1])
|
|
124
|
+
if np.random.binomial(1, self.aug_p):
|
|
125
|
+
d_ij[0] += np.random.normal(0, self.random_offset, size=1)
|
|
126
|
+
if np.random.binomial(1, self.aug_p):
|
|
127
|
+
n_outliers = np.random.randint(0, 10)
|
|
128
|
+
t_outliers = np.random.randint(0, self.input_len, n_outliers)
|
|
129
|
+
d_ij[0, t_outliers] += np.random.normal(0, self.random_outlier, n_outliers)
|
|
130
|
+
if np.random.binomial(1, self.aug_p):
|
|
131
|
+
d_ij[0] += self.random_drift[1] * np.cos(self.random_drift[0] * np.arange(self.input_len))
|
|
132
|
+
d_ij[0] = d_ij[0] * std_ij
|
|
133
|
+
self.data.append(d_ij)
|
|
134
|
+
self.data = np.asarray(self.data)
|
|
135
|
+
|
|
136
|
+
def __len__(self):
|
|
137
|
+
"""
|
|
138
|
+
Returns the total number of samples.
|
|
139
|
+
"""
|
|
140
|
+
return len(self.data)
|
|
141
|
+
|
|
142
|
+
def __getitem__(self, idx):
|
|
143
|
+
"""
|
|
144
|
+
Generates one sample of data.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
idx : int
|
|
149
|
+
Index of the sample to retrieve.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
sample : dict
|
|
154
|
+
Dictionary containing 'input', 'target', and 'distance' tensors.
|
|
155
|
+
"""
|
|
156
|
+
data_torch = torch.from_numpy(self.data[idx]).float()
|
|
157
|
+
sample = {'input': data_torch[0], 'target': data_torch[1], 'distance': data_torch[2]}
|
|
158
|
+
return sample
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from torch import nn
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BCEDiceLoss(nn.Module):
|
|
6
|
+
"""
|
|
7
|
+
A combination of Binary Cross Entropy (BCE) and Dice Loss for binary segmentation tasks.
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
loss_params : tuple, optional
|
|
12
|
+
A tuple containing the weights for BCE and Dice losses respectively. Default is (1, 1).
|
|
13
|
+
|
|
14
|
+
Methods
|
|
15
|
+
-------
|
|
16
|
+
dice_loss(inputs, targets, epsilon=1e-6)
|
|
17
|
+
Computes the Dice loss.
|
|
18
|
+
|
|
19
|
+
forward(inputs, targets)
|
|
20
|
+
Computes the combined BCE and Dice loss.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, loss_params=(1, 1)):
|
|
24
|
+
super(BCEDiceLoss, self).__init__()
|
|
25
|
+
self.bce_loss = nn.BCEWithLogitsLoss()
|
|
26
|
+
self.loss_params = loss_params
|
|
27
|
+
|
|
28
|
+
def dice_loss(self, inputs, targets, epsilon=1e-6):
|
|
29
|
+
inputs = torch.sigmoid(inputs)
|
|
30
|
+
intersection = (inputs * targets).sum()
|
|
31
|
+
dice_coeff = (2. * intersection + epsilon) / (inputs.sum() + targets.sum() + epsilon)
|
|
32
|
+
return 1 - dice_coeff
|
|
33
|
+
|
|
34
|
+
def forward(self, inputs, targets):
|
|
35
|
+
bce = self.bce_loss(inputs, targets)
|
|
36
|
+
dice = self.dice_loss(inputs, targets)
|
|
37
|
+
return self.loss_params[0] * bce + self.loss_params[1] * dice
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SmoothnessLoss(nn.Module):
|
|
41
|
+
"""
|
|
42
|
+
Computes the smoothness loss for a sequence of predictions.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
alpha : float, optional
|
|
47
|
+
Weight of the smoothness loss component. Default is 10.
|
|
48
|
+
|
|
49
|
+
Methods
|
|
50
|
+
-------
|
|
51
|
+
forward(predictions)
|
|
52
|
+
Computes the smoothness loss for a sequence of predictions.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, alpha=10):
|
|
56
|
+
super(SmoothnessLoss, self).__init__()
|
|
57
|
+
self.alpha = alpha
|
|
58
|
+
|
|
59
|
+
def forward(self, predictions):
|
|
60
|
+
if predictions.dim() < 3:
|
|
61
|
+
raise ValueError("The input tensor must be 3-dimensional.")
|
|
62
|
+
diffs = predictions[:, :, 1:] - predictions[:, :, :-1]
|
|
63
|
+
loss = torch.sum(diffs ** 2) / predictions.size(0)
|
|
64
|
+
return self.alpha * loss
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def f1_score(logits, true_labels, threshold=0.5, epsilon=1e-7):
|
|
68
|
+
"""
|
|
69
|
+
Computes the F1 score for binary classification.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
logits : torch.Tensor
|
|
74
|
+
The raw output from the model (before applying sigmoid).
|
|
75
|
+
true_labels : torch.Tensor
|
|
76
|
+
The ground truth binary labels.
|
|
77
|
+
threshold : float, optional
|
|
78
|
+
The threshold to convert probabilities to binary predictions. Default is 0.5.
|
|
79
|
+
epsilon : float, optional
|
|
80
|
+
A small value to avoid division by zero. Default is 1e-7.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
float
|
|
85
|
+
The computed F1 score.
|
|
86
|
+
"""
|
|
87
|
+
probabilities = torch.sigmoid(logits)
|
|
88
|
+
predictions = probabilities > threshold
|
|
89
|
+
predictions = predictions.float()
|
|
90
|
+
true_labels = true_labels.float()
|
|
91
|
+
tp = (predictions * true_labels).sum().item()
|
|
92
|
+
fp = ((1 - true_labels) * predictions).sum().item()
|
|
93
|
+
fn = (true_labels * (1 - predictions)).sum().item()
|
|
94
|
+
precision = tp / (tp + fp + epsilon)
|
|
95
|
+
recall = tp / (tp + fn + epsilon)
|
|
96
|
+
f1_score = 2 * (precision * recall) / (precision + recall + epsilon)
|
|
97
|
+
return f1_score
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import torch
|
|
3
|
+
|
|
4
|
+
from .contraction_net import ContractionNet
|
|
5
|
+
from .utils import get_device
|
|
6
|
+
|
|
7
|
+
# select device
|
|
8
|
+
device = get_device()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def predict_contractions(data, model, network=ContractionNet):
|
|
12
|
+
"""predict contraction intervals time-series with neural network
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
data : ndarray
|
|
17
|
+
1D array with time-series of contraction
|
|
18
|
+
model : str
|
|
19
|
+
trained model weights (.pt file)
|
|
20
|
+
network : nn.Module
|
|
21
|
+
Network to predict contractions from time-series
|
|
22
|
+
standard_normalizer : bool, ndarray
|
|
23
|
+
If False, each data is normalized by its mean and std. If True, the mean and std from the training data set are
|
|
24
|
+
applied. If ndarray, the data is normalized with the entered args [[input_mean, input_std], [vel_mean, vel_std]]
|
|
25
|
+
"""
|
|
26
|
+
# load model state
|
|
27
|
+
state_dict = torch.load(model, map_location=device)
|
|
28
|
+
# initiate model
|
|
29
|
+
model = network(state_dict['n_filter'], in_channels=state_dict['in_channels'],
|
|
30
|
+
out_channels=state_dict['out_channels']).to(device)
|
|
31
|
+
model.load_state_dict(state_dict['state_dict'])
|
|
32
|
+
# resize data
|
|
33
|
+
len_data = data.shape[0]
|
|
34
|
+
contr = np.pad(data, (0, 32 - np.mod(len_data, 32)), mode='reflect')
|
|
35
|
+
# convert to torch
|
|
36
|
+
data = torch.from_numpy(contr.astype('float32')).view(1, 1, -1).to(device)
|
|
37
|
+
# predict data
|
|
38
|
+
res = model(data)[0][0]
|
|
39
|
+
# resize data and convert to numpy
|
|
40
|
+
res = res.detach().cpu().numpy()
|
|
41
|
+
return res[:, :len_data]
|