├── data ├── datasets │ └── .gitkeep ├── records │ └── .gitkeep └── metadata.csv ├── examples ├── dl │ ├── log │ │ └── .gitkeep │ ├── config.py │ ├── create_dataset.py │ ├── model.py │ ├── predict.ipynb │ └── train_model.py └── ml │ ├── config.py │ └── create_dataset.py ├── requirements.txt ├── iridia_af ├── hyperparameters.py └── record.py ├── README.md └── LICENSE /data/datasets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/records/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/dl/log/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/ml/config.py: -------------------------------------------------------------------------------- 1 | WINDOW_SIZE = 300 2 | TRAINING_STEP = 100 3 | PREDICTION_STEP = 10 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | h5py 2 | numpy 3 | pandas 4 | matplotlib 5 | scipy 6 | xgboost 7 | scikit-learn 8 | torch 9 | tqdm -------------------------------------------------------------------------------- /examples/dl/config.py: -------------------------------------------------------------------------------- 1 | WINDOW_SIZE = 8192 2 | TRAINING_STEP = 4096 3 | TESTING_STEP = 8192 4 | RANDOM_SEED = 42 5 | 6 | EPOCH = 100 7 | PATIENCE = 5 8 | BATCH_SIZE = 32 9 | LEARNING_RATE = 0.0001 10 | 11 | NUM_PROC_WORKERS_DATA = 4 12 | 13 | def get_dict(): 14 | return { 15 | "WINDOW_SIZE": WINDOW_SIZE, 16 | "TRAINING_STEP": TRAINING_STEP, 17 | "TESTING_STEP": TESTING_STEP, 18 | "RANDOM_SEED": RANDOM_SEED, 19 | "EPOCH": EPOCH, 20 | "PATIENCE": PATIENCE, 21 | "BATCH_SIZE": BATCH_SIZE, 22 | "LEARNING_RATE": LEARNING_RATE, 23 | "NUM_PROC_WORKERS": NUM_PROC_WORKERS_DATA 24 | } -------------------------------------------------------------------------------- /iridia_af/hyperparameters.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | from pathlib import Path 3 | 4 | ROOT_PATH = Path(__file__).resolve().parent.parent.resolve() 5 | DATA_PATH = Path(ROOT_PATH, "data") 6 | DATASET_PATH = Path(DATA_PATH, "datasets") 7 | 8 | LOG_DL_PATH = Path(ROOT_PATH, "examples", "dl", "log") 9 | 10 | RECORDS_PATH = Path(DATA_PATH, "records") 11 | assert RECORDS_PATH.exists(), f"RECORDS_PATH does not exist: {RECORDS_PATH}" 12 | METADATA_PATH = Path(DATA_PATH, "metadata.csv") 13 | assert METADATA_PATH.exists(), f"METADATA_PATH does not exist: {METADATA_PATH}" 14 | 15 | NUM_PROC = multiprocessing.cpu_count() - 2 16 | 17 | SAMPLE_RATE = 200 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IRIDIA-AF 2 | 3 | A large paroxysmal atrial fibrillation long-term electrocardiogram monitoring database 4 | ## Authors 5 | 6 | - Cédric GILON (1) 7 | - Jean-Marie GRÉGOIRE (1,2) 8 | - Marianne MATHIEU (1) 9 | - Stéphane CARLIER (2) 10 | - Hugues BERSINI (1) 11 | 12 | 1. IRIDIA, Université libre de Bruxelles, Belgium 13 | 2. Cardiology departement, Université de Mons, Belgium 14 | 15 | ## Abstract 16 | 17 | Atrial fibrillation (AF) is the most common sustained heart arrhythmia in adults. 18 | Holter monitoring, a long-term 2-lead electrocardiogram (ECG), is a key tool available to cardiologists for AF diagnosis. 19 | Machine learning (ML) and deep learning (DL) models have shown great capacity to automatically detect AF in ECG and their use as medical decision support tool is growing. 20 | Training these models rely on a few open and annotated databases. 21 | We present a new Holter monitoring database from patients with paroxysmal AF with 167 records from 152 patients, acquired from an outpatient cardiology clinic from 2006 to 2017 in Belgium. 22 | AF episodes were manually annotated and reviewed by an expert cardiologist and a specialist cardiac nurse. 23 | Records last from 19 hours up to 95 hours, divided into 24-hour files. 24 | In total, it represents 24 million seconds of annotated Holter monitoring, sampled at 200 Hz. 25 | This dataset aims at expanding the available options for researchers and offers a valuable resource for advancing ML and DL use in the field of cardiac arrhythmia diagnosis. 26 | 27 | ## Data 28 | 29 | The data is available on Zenodo: https://zenodo.org/records/8405941. 30 | -------------------------------------------------------------------------------- /examples/dl/create_dataset.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import h5py 4 | import numpy as np 5 | import pandas as pd 6 | import torch 7 | from torch.utils.data import Dataset 8 | from tqdm import tqdm 9 | 10 | import config as cfg 11 | import iridia_af.hyperparameters as hp 12 | from iridia_af.record import create_record 13 | 14 | 15 | def create_dataset_csv(): 16 | metadata_df = pd.read_csv(hp.METADATA_PATH) 17 | list_windows = [] 18 | for record_id in tqdm(metadata_df["record_id"].unique()): 19 | record = create_record(record_id, metadata_df, hp.RECORDS_PATH) 20 | record.load_ecg() 21 | for day_index in range(record.metadata.record_n_files): 22 | len_day = record.ecg[day_index].shape[0] 23 | for i in range(0, len_day - cfg.WINDOW_SIZE, cfg.TRAINING_STEP): 24 | label = 1 if np.sum(record.ecg_labels[day_index][i:i + cfg.WINDOW_SIZE]) > 0 else 0 25 | detection_window = { 26 | "patient_id": record.metadata.patient_id, 27 | "file": record.ecg_files[day_index], 28 | "start_index": i, 29 | "end_index": i + cfg.WINDOW_SIZE, 30 | "label": label 31 | } 32 | list_windows.append(detection_window) 33 | 34 | new_df = pd.DataFrame(list_windows) 35 | new_df_path = Path(hp.DATASET_PATH, f"dataset_detection_ecg_{cfg.WINDOW_SIZE}.csv") 36 | new_df.to_csv(Path(new_df_path, index=False)) 37 | print(f"Saved dataset to {Path(hp.DATASET_PATH, f'dataset_detection_ecg_{cfg.WINDOW_SIZE}.csv')}") 38 | 39 | 40 | class DetectionDataset(Dataset): 41 | def __init__(self, df): 42 | self.df = df 43 | 44 | def __len__(self): 45 | return len(self.df) 46 | 47 | def __getitem__(self, idx): 48 | dw = self.df.iloc[idx] 49 | with h5py.File(dw.file, "r") as f: 50 | key = list(f.keys())[0] 51 | ecg_data = f[key][dw.start_index:dw.end_index, 0] 52 | ecg_data = torch.tensor(ecg_data, dtype=torch.float32) 53 | ecg_data = ecg_data.unsqueeze(0) 54 | label = torch.tensor(dw.label, dtype=torch.float32) 55 | label = label.unsqueeze(0) 56 | return ecg_data, label 57 | 58 | 59 | if __name__ == "__main__": 60 | create_dataset_csv() 61 | -------------------------------------------------------------------------------- /examples/ml/create_dataset.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | from itertools import repeat 3 | from pathlib import Path 4 | 5 | import hrvanalysis as hrv 6 | import numpy as np 7 | import pandas as pd 8 | 9 | import iridia_af.hyperparameters as hp 10 | import config as cfg 11 | from iridia_af.record import Record 12 | 13 | 14 | def main(): 15 | metadata_df = pd.read_csv(hp.METADATA_PATH) 16 | list_record_path = metadata_df["record_id"].values 17 | with multiprocessing.Pool(hp.NUM_PROC) as pool: 18 | all_windows = pool.starmap(get_record_windows, zip(list_record_path, repeat(metadata_df))) 19 | 20 | new_all_windows = [] 21 | for windows in all_windows: 22 | new_all_windows.extend(windows) 23 | 24 | df_features = pd.DataFrame(new_all_windows) 25 | new_df_path = Path(hp.DATASET_PATH, f"dataset_hrv_{cfg.WINDOW_SIZE}_{cfg.TRAINING_STEP}.csv") 26 | df_features.to_csv(new_df_path, index=False) 27 | print(f"Saved dataset to {new_df_path}") 28 | 29 | 30 | def get_record_windows(record_id, metadata_df): 31 | metadata_record = metadata_df[metadata_df["record_id"] == record_id] 32 | metadata_record = metadata_record.values[0] 33 | record_path = Path(hp.RECORDS_PATH, record_id) 34 | record = Record(record_path, metadata_record) 35 | record.load_rr_record() 36 | record_windows = [] 37 | for day_index in range(record.num_days): 38 | for i in range(0, len(record.rr[day_index]) - cfg.WINDOW_SIZE, cfg.TRAINING_STEP): 39 | rr_window = record.rr[day_index][i:i + cfg.WINDOW_SIZE] 40 | label_window = record.rr_labels[day_index][i:i + cfg.WINDOW_SIZE] 41 | if np.sum(label_window) == 0: 42 | label = 0 43 | else: 44 | label = 1 45 | features = get_hrv_metrics(rr_window) 46 | features["patient"] = record.metadata.patient_id 47 | features["record"] = record.metadata.record_id 48 | features["label"] = label 49 | record_windows.append(features) 50 | print(f"Finished record {record_id}") 51 | return record_windows 52 | 53 | 54 | def get_hrv_metrics(rr_window: np.ndarray): 55 | time_domain_features = hrv.get_time_domain_features(rr_window) 56 | frequency_domain_features = hrv.get_frequency_domain_features(rr_window) 57 | geometrical_features = hrv.get_geometrical_features(rr_window) 58 | poincare_features = hrv.get_poincare_plot_features(rr_window) 59 | 60 | all_features = {**time_domain_features, **frequency_domain_features, **geometrical_features, **poincare_features} 61 | # remove tinn 62 | del all_features["tinn"] 63 | return all_features 64 | 65 | 66 | if __name__ == '__main__': 67 | main() 68 | -------------------------------------------------------------------------------- /examples/dl/model.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import torch 3 | import torch.nn as nn 4 | import torch.nn.functional as F 5 | 6 | 7 | @dataclasses.dataclass 8 | class CNNModelConfig: 9 | input_size: int 10 | 11 | 12 | class CNNModel(nn.Module): 13 | def __init__(self, config: CNNModelConfig): 14 | super().__init__() 15 | self.config = config 16 | 17 | # input 2 leads with 4096 samples (= 20 seconds) 18 | self.block1 = ResidualBlock(in_channels=1, out_channels=16, kernel_size=7, padding=3) 19 | self.block2 = ResidualBlock(in_channels=16, out_channels=16, kernel_size=7, padding=3) 20 | self.block3 = ResidualBlock(in_channels=16, out_channels=16, kernel_size=7, padding=3) 21 | 22 | self.dropout = nn.Dropout(p=0.2) 23 | 24 | self.block4 = ResidualBlock(in_channels=16, out_channels=32, kernel_size=5, padding=2) 25 | self.block5 = ResidualBlock(in_channels=32, out_channels=32, kernel_size=5, padding=2) 26 | self.block6 = ResidualBlock(in_channels=32, out_channels=32, kernel_size=5, padding=2) 27 | 28 | self.block7 = ResidualBlock(in_channels=32, out_channels=64, kernel_size=3, padding=1) 29 | self.block8 = ResidualBlock(in_channels=64, out_channels=64, kernel_size=3, padding=1) 30 | self.block9 = ResidualBlock(in_channels=64, out_channels=64, kernel_size=3, padding=1) 31 | 32 | # invert dimension for convolution on leads (2) 33 | self.conv1 = nn.Conv1d(in_channels=8, out_channels=128, kernel_size=8, stride=2, padding=1) 34 | self.bn1 = nn.BatchNorm1d(128) 35 | 36 | size = int(config.input_size / 8) 37 | 38 | self.fc1 = nn.Linear(size, 32) 39 | self.fc2 = nn.Linear(32, 1) 40 | 41 | def forward(self, x): 42 | # block 1 43 | x = self.block1(x) 44 | x = self.block2(x) 45 | x = self.block3(x) 46 | 47 | x = self.dropout(x) 48 | 49 | x = self.block4(x) 50 | x = self.block5(x) 51 | x = self.block6(x) 52 | 53 | x = self.dropout(x) 54 | 55 | x = self.block7(x) 56 | x = self.block8(x) 57 | x = self.block9(x) 58 | 59 | x = self.dropout(x) 60 | 61 | x = x.view(x.shape[0], -1) 62 | x = self.fc1(x) 63 | x = F.relu(x) 64 | x = self.fc2(x) 65 | x = torch.sigmoid(x) 66 | 67 | return x 68 | 69 | def configure_optimizers(self): 70 | optimizer = torch.optim.AdamW(self.parameters(), lr=1e-3) 71 | return optimizer 72 | 73 | 74 | class ResidualBlock(nn.Module): 75 | def __init__(self, in_channels, out_channels, kernel_size, padding): 76 | super().__init__() 77 | self.conv1 = nn.Conv1d(in_channels=in_channels, out_channels=out_channels, 78 | kernel_size=kernel_size, stride=1, padding=padding) 79 | self.bn1 = nn.BatchNorm1d(out_channels) 80 | 81 | self.conv2 = nn.Conv1d(in_channels=out_channels, out_channels=out_channels, 82 | kernel_size=kernel_size, stride=2, padding=padding) 83 | self.bn2 = nn.BatchNorm1d(out_channels) 84 | 85 | self.conv_shortcut = nn.Conv1d(in_channels=in_channels, out_channels=out_channels, 86 | kernel_size=1, stride=1, padding=0) 87 | self.maxpool = nn.MaxPool1d(kernel_size=2) 88 | 89 | def forward(self, x_in): 90 | x = self.conv1(x_in) 91 | x = F.relu(x) 92 | x = self.bn1(x) 93 | 94 | x = self.conv2(x) 95 | x = F.relu(x) 96 | x = self.bn2(x) 97 | 98 | shortcut = self.conv_shortcut(x_in) 99 | shortcut = self.maxpool(shortcut) 100 | 101 | x += shortcut 102 | 103 | return x 104 | -------------------------------------------------------------------------------- /examples/dl/predict.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "outputs": [], 7 | "source": [ 8 | "%load_ext autoreload\n", 9 | "%autoreload 2" 10 | ], 11 | "metadata": { 12 | "collapsed": false 13 | } 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "outputs": [], 19 | "source": [ 20 | "from pathlib import Path\n", 21 | "\n", 22 | "import matplotlib.pyplot as plt\n", 23 | "\n", 24 | "plt.rcParams['figure.dpi'] = 300\n", 25 | "import numpy as np\n", 26 | "import pandas as pd\n", 27 | "import torch\n", 28 | "from tqdm.notebook import tqdm\n", 29 | "\n", 30 | "import iridia_af.hyperparameters as hp\n", 31 | "import config as cfg\n", 32 | "from model import CNNModel, CNNModelConfig\n", 33 | "from iridia_af.record import create_record" 34 | ], 35 | "metadata": { 36 | "collapsed": false 37 | } 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "outputs": [], 43 | "source": [ 44 | "# folder_name = \"20230701-161104\"\n", 45 | "folder_name = \"20230720-164047\"\n", 46 | "folder_path = Path(hp.LOG_DL_PATH, folder_name)\n", 47 | "assert folder_path.exists()\n", 48 | "\n", 49 | "model_path = Path(folder_path, \"model.pt\")\n", 50 | "assert model_path.exists()\n", 51 | "#\n", 52 | "config = CNNModelConfig(cfg.WINDOW_SIZE)\n", 53 | "model = CNNModel(config)\n", 54 | "model.load_state_dict(torch.load(model_path))\n", 55 | "model.float()\n", 56 | "model.eval()\n", 57 | "\n", 58 | "# folder path\n", 59 | "folder_path.absolute()" 60 | ], 61 | "metadata": { 62 | "collapsed": false 63 | } 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "outputs": [], 69 | "source": [ 70 | "metadata_df = pd.read_csv(hp.METADATA_PATH)\n", 71 | "record = create_record(\"record_104\", metadata_df, hp.RECORDS_PATH)\n", 72 | "record.load_ecg(clean_front=True)" 73 | ], 74 | "metadata": { 75 | "collapsed": false 76 | } 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "outputs": [], 82 | "source": [ 83 | "all_y_pred = [[] for _ in range(len(record.ecg[0]))]\n", 84 | "for i in tqdm(range(0, len(record.ecg[0]) - cfg.WINDOW_SIZE, cfg.TESTING_STEP)):\n", 85 | " x = torch.tensor(record.ecg[0][i:i + cfg.WINDOW_SIZE, 0].copy()).float()\n", 86 | " x = x.unsqueeze(0).unsqueeze(0)\n", 87 | " y_pred = model(x)\n", 88 | " for j in range(i, i + cfg.WINDOW_SIZE):\n", 89 | " all_y_pred[j].append(y_pred.detach().numpy()[0][0])" 90 | ], 91 | "metadata": { 92 | "collapsed": false 93 | } 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "outputs": [], 99 | "source": [ 100 | "y_pred = []\n", 101 | "for i in tqdm(all_y_pred):\n", 102 | " if len(i) == 0:\n", 103 | " y_pred.append(0)\n", 104 | " else:\n", 105 | " y_pred.append(np.mean(i))" 106 | ], 107 | "metadata": { 108 | "collapsed": false 109 | } 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": null, 114 | "outputs": [], 115 | "source": [ 116 | "fig, ax = plt.subplots(3, 1, figsize=(20, 10), sharex=True)\n", 117 | "\n", 118 | "ax_index = 0\n", 119 | "ax[ax_index].plot(record.ecg[0][:, 0])\n", 120 | "ax[ax_index].set_ylabel(\"ECG Lead I (mV)\")\n", 121 | "ax_index += 1\n", 122 | "\n", 123 | "ax[ax_index].plot(record.ecg_labels[0])\n", 124 | "ax[ax_index].set_ylim(-0.1, 1.1)\n", 125 | "ax[ax_index].set_yticks([0, 1])\n", 126 | "ax[ax_index].set_yticklabels([\"NSR\", \"AF\"])\n", 127 | "ax[ax_index].set_ylabel(\"Annotation\")\n", 128 | "ax_index += 1\n", 129 | "\n", 130 | "ax[ax_index].plot(y_pred)\n", 131 | "ax[ax_index].set_ylim(-0.1, 1.1)\n", 132 | "ax[ax_index].set_yticks([0, 1])\n", 133 | "ax[ax_index].set_yticklabels([\"NSR\", \"AF\"])\n", 134 | "ax[ax_index].set_ylabel(\"Prediction\")\n", 135 | "ax_index += 1" 136 | ], 137 | "metadata": { 138 | "collapsed": false 139 | } 140 | } 141 | ], 142 | "metadata": { 143 | "kernelspec": { 144 | "display_name": "Python 3", 145 | "language": "python", 146 | "name": "python3" 147 | }, 148 | "language_info": { 149 | "codemirror_mode": { 150 | "name": "ipython", 151 | "version": 2 152 | }, 153 | "file_extension": ".py", 154 | "mimetype": "text/x-python", 155 | "name": "python", 156 | "nbconvert_exporter": "python", 157 | "pygments_lexer": "ipython2", 158 | "version": "2.7.6" 159 | } 160 | }, 161 | "nbformat": 4, 162 | "nbformat_minor": 0 163 | } 164 | -------------------------------------------------------------------------------- /examples/dl/train_model.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from pathlib import Path 3 | 4 | import numpy as np 5 | import pandas as pd 6 | import torch 7 | import torch.nn as nn 8 | from sklearn.metrics import roc_auc_score, confusion_matrix 9 | from sklearn.model_selection import train_test_split 10 | from tqdm import tqdm 11 | 12 | import config as cfg 13 | import iridia_af.hyperparameters as hp 14 | from create_dataset import DetectionDataset 15 | from model import CNNModel, CNNModelConfig 16 | 17 | torch.manual_seed(cfg.RANDOM_SEED) 18 | torch.backends.cuda.matmul.allow_tf32 = True # allow tf32 on matmul 19 | torch.backends.cudnn.allow_tf32 = True # allow tf32 on cudnn 20 | 21 | 22 | def train_model(): 23 | print("Loading data") 24 | train_dataset, val_dataset, test_dataset, list_patients = create_train_val_test_split() 25 | 26 | device = get_device() 27 | print(f"Using device: {device}") 28 | 29 | print(cfg.get_dict()) 30 | 31 | config = CNNModelConfig(input_size=cfg.WINDOW_SIZE) 32 | model = CNNModel(config) 33 | 34 | # summary(model, (1, hp.WINDOW_SIZE)) 35 | 36 | model = model.to(device) 37 | optimizer = configure_optimizers(model) 38 | criterion = nn.BCELoss() 39 | 40 | min_val_loss = 1000 41 | min_val_loss_epoch = 0 42 | best_model = None 43 | 44 | for i in range(cfg.EPOCH): 45 | model.train() 46 | train_losses = [] 47 | list_y_true = [] 48 | list_y_pred = [] 49 | for batch_idx, (x, y) in enumerate(tqdm(train_dataset)): 50 | x = x.to(device) 51 | y = y.to(device) 52 | optimizer.zero_grad() 53 | y_pred = model(x) 54 | loss = criterion(y_pred, y) 55 | 56 | train_losses.append(loss.item()) 57 | list_y_true.extend(y.tolist()) 58 | list_y_pred.extend(y_pred.tolist()) 59 | 60 | loss.backward() 61 | optimizer.step() 62 | 63 | total = len(list_y_true) 64 | list_y_pred_round = np.round(list_y_pred) 65 | correct = sum([1 if y_true == y_pred else 0 for y_true, y_pred in zip(list_y_true, list_y_pred_round)]) 66 | 67 | train_accuracy = correct / total 68 | 69 | train_loss = np.mean(train_losses) 70 | val_loss = estimate_loss(model, device, val_dataset, criterion) 71 | metrics = estimate_metrics(model, val_dataset, device) 72 | print(f"step {i + 1}: train loss {train_loss}, val loss {val_loss}") 73 | print(f"step {i + 1}: train accuracy {train_accuracy}, val accuracy {metrics['accuracy']}") 74 | print(f"step {i + 1}: train roc_auc {metrics['roc_auc']}, val roc_auc {metrics['roc_auc']}") 75 | 76 | # early stopping 77 | if val_loss < min_val_loss: 78 | min_val_loss = val_loss 79 | min_val_loss_epoch = 0 80 | best_model = model.state_dict() 81 | else: 82 | min_val_loss_epoch += 1 83 | 84 | if min_val_loss_epoch >= cfg.PATIENCE: 85 | print(f"Early stopping at epoch {i + 1}") 86 | model.load_state_dict(best_model) 87 | break 88 | 89 | test_loss = estimate_loss(model, device, test_dataset, criterion) 90 | metrics = estimate_metrics(model, test_dataset, device) 91 | print(f"test loss {test_loss}") 92 | print(f"test roc_auc {metrics['roc_auc']}") 93 | print(f"test accuracy {metrics['accuracy']}") 94 | print(f"test sensitivity {metrics['sensitivity']}") 95 | print(f"test specificity {metrics['specificity']}") 96 | print(f"test f1_score {metrics['f1_score']}") 97 | 98 | # save model 99 | timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") 100 | folder = Path(hp.LOG_DL_PATH, f"{timestamp}") 101 | print(f"Saving model to {folder.absolute()}") 102 | folder.mkdir(parents=True, exist_ok=True) 103 | torch.save(model.state_dict(), Path(folder, "model.pt")) 104 | pd.DataFrame(list_patients).to_csv(Path(folder, "list_patients.csv")) 105 | # save hyperparameters 106 | df_hp = pd.DataFrame(cfg.get_dict(), index=[0]) 107 | df_hp.to_csv(Path(folder, "hyperparameters.csv")) 108 | # save metrics 109 | df_metrics = pd.DataFrame(metrics, index=[0]) 110 | df_metrics.to_csv(Path(folder, "metrics.csv")) 111 | 112 | 113 | @torch.no_grad() 114 | def estimate_loss(model, device, dataset, criterion): 115 | model.eval() 116 | losses = [] 117 | for batch_idx, (x, y) in enumerate(dataset): 118 | x = x.to(device) 119 | y = y.to(device) 120 | y_pred = model(x) 121 | loss = criterion(y, y_pred) 122 | losses.append(loss.item()) 123 | 124 | # if batch_idx > 10: 125 | # break 126 | 127 | return np.mean(losses) 128 | 129 | 130 | @torch.no_grad() 131 | def estimate_metrics(model, dataset, device, threshold=0.5): 132 | model.eval() 133 | list_y_true = [] 134 | list_y_pred = [] 135 | for batch_idx, (x, y) in enumerate(dataset): 136 | x = x.to(device) 137 | y = y.to(device) 138 | list_y_true.extend(y.tolist()) 139 | y_pred = model(x) 140 | list_y_pred.extend(y_pred.tolist()) 141 | 142 | roc_auc = roc_auc_score(list_y_true, list_y_pred) 143 | 144 | list_y_pred_round = np.where(np.array(list_y_pred) > threshold, 1, 0) 145 | cm = confusion_matrix(list_y_true, list_y_pred_round) 146 | accuracy = (cm[0, 0] + cm[1, 1]) / (cm[0, 0] + cm[1, 1] + cm[1, 0] + cm[0, 1]) 147 | sensitivity = cm[1, 1] / (cm[1, 1] + cm[1, 0]) 148 | specificity = cm[0, 0] / (cm[0, 0] + cm[0, 1]) 149 | f1_score = 2 * (sensitivity * specificity) / (sensitivity + specificity) 150 | 151 | return {"roc_auc": roc_auc, 152 | "accuracy": accuracy, 153 | "sensitivity": sensitivity, 154 | "specificity": specificity, 155 | "f1_score": f1_score} 156 | 157 | 158 | def get_device(): 159 | if torch.cuda.is_available(): 160 | return torch.device("cuda") 161 | elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): 162 | return torch.device("mps") 163 | else: 164 | return torch.device("cpu") 165 | 166 | 167 | def configure_optimizers(model): 168 | optimizer = torch.optim.AdamW(model.parameters(), lr=cfg.LEARNING_RATE) 169 | return optimizer 170 | 171 | 172 | def create_train_val_test_split(): 173 | dataset_path = Path(hp.DATASET_PATH, f"dataset_detection_ecg_{cfg.WINDOW_SIZE}.csv") 174 | df = pd.read_csv(dataset_path) 175 | # crop to the first 20000 rows 176 | df = df[:20000] 177 | patients = df["patient_id"].unique() 178 | 179 | train_val_patients, test_patients = train_test_split(patients, test_size=0.2, random_state=cfg.RANDOM_SEED) 180 | train_patients, val_patients = train_test_split(train_val_patients, test_size=0.2, random_state=cfg.RANDOM_SEED) 181 | 182 | train_df = df[df["patient_id"].isin(train_patients)] 183 | train_dataset = DetectionDataset(train_df) 184 | train_dataset_loader = torch.utils.data.DataLoader(train_dataset, 185 | batch_size=cfg.BATCH_SIZE, 186 | shuffle=True, 187 | num_workers=cfg.NUM_PROC_WORKERS_DATA, 188 | pin_memory=True) 189 | 190 | val_df = df[df["patient_id"].isin(val_patients)] 191 | val_dataset = DetectionDataset(val_df) 192 | val_dataset_loader = torch.utils.data.DataLoader(val_dataset, 193 | batch_size=cfg.BATCH_SIZE, 194 | shuffle=False, 195 | num_workers=cfg.NUM_PROC_WORKERS_DATA, 196 | pin_memory=True) 197 | 198 | test_df = df[df["patient_id"].isin(test_patients)] 199 | test_dataset = DetectionDataset(test_df) 200 | test_dataset_loader = torch.utils.data.DataLoader(test_dataset, 201 | batch_size=cfg.BATCH_SIZE, 202 | shuffle=False, 203 | num_workers=cfg.NUM_PROC_WORKERS_DATA, 204 | pin_memory=True) 205 | 206 | return train_dataset_loader, val_dataset_loader, test_dataset_loader, [train_patients, val_patients, test_patients] 207 | 208 | 209 | if __name__ == "__main__": 210 | train_model() 211 | -------------------------------------------------------------------------------- /iridia_af/record.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from pathlib import Path 3 | 4 | import h5py 5 | import hrvanalysis as hrv 6 | import numpy as np 7 | import pandas as pd 8 | from matplotlib import pyplot as plt 9 | 10 | 11 | def create_record(record_id, metadata_df, record_path): 12 | metadata_record = (metadata_df[metadata_df["record_id"] == record_id]) 13 | assert len(metadata_record) == 1 14 | metadata_record = metadata_record.values[0] 15 | record_path = Path(record_path, record_id) 16 | record = Record(record_path, metadata_record) 17 | return record 18 | 19 | 20 | class Record: 21 | def __init__(self, record_folder, metadata_record): 22 | # self.metadata = self.__get_metadata(record_id) 23 | self.metadata = RecordMetadata(*metadata_record) 24 | 25 | self.record_folder = record_folder 26 | self.num_days = len(list(self.record_folder.glob("*ecg_*.h5"))) 27 | 28 | self.rr_files = sorted(self.record_folder.glob("*rr_*.h5")) 29 | assert len(self.rr_files) == self.num_days 30 | self.rr = None 31 | self.rr_labels_df = None 32 | self.rr_labels = None 33 | 34 | self.ecg_files = sorted(self.record_folder.glob("*ecg_*.h5")) 35 | assert len(self.ecg_files) == self.num_days 36 | self.ecg = None 37 | self.ecg_labels_df = None 38 | self.ecg_labels = None 39 | 40 | def load_rr_record(self): 41 | self.rr = [self.__read_rr_file(rr_file) for rr_file in self.rr_files] 42 | self.__create_rr_labels() 43 | 44 | def __read_rr_file(self, rr_file: Path, clean_rr=True) -> np.ndarray: 45 | with h5py.File(rr_file, "r") as f: 46 | rr = f["rr"][:] 47 | if clean_rr: 48 | rr = self.__clean_rr(rr) 49 | return rr 50 | 51 | def __clean_rr(self, rr_list, remove_invalid=True, low_rr=200, high_rr=4000, interpolation_method="linear", 52 | remove_ecto=True) -> np.ndarray: 53 | 54 | if remove_invalid: 55 | rr_list = [rr if high_rr >= rr >= low_rr else np.nan for rr in rr_list] 56 | rr_list = pd.Series(rr_list).interpolate(method=interpolation_method).tolist() 57 | if remove_ecto: 58 | rr_list = hrv.remove_ectopic_beats(rr_list, 59 | method='custom', 60 | custom_removing_rule=0.3, 61 | verbose=False) 62 | rr_list = pd.Series(rr_list) \ 63 | .interpolate(method=interpolation_method) \ 64 | .interpolate(limit_direction='both').tolist() 65 | return np.array(rr_list) 66 | 67 | def __read_rr_label(self) -> pd.DataFrame: 68 | rr_labels = sorted(self.record_folder.glob("*rr_labels.csv")) 69 | df_rr_labels = pd.read_csv(rr_labels[0]) 70 | return df_rr_labels 71 | 72 | def __create_rr_labels(self): 73 | self.rr_labels_df = self.__read_rr_label() 74 | len_rr = [len(rr) for rr in self.rr] 75 | 76 | start_day = self.rr_labels_df["start_file_index"].unique() 77 | end_day = self.rr_labels_df["end_file_index"].unique() 78 | days = np.unique(np.concatenate([start_day, end_day])) 79 | assert len(days) <= len(len_rr) 80 | 81 | labels = [np.zeros(len_day_rr) for len_day_rr in len_rr] 82 | 83 | for i, row in self.rr_labels_df.iterrows(): 84 | rr_event = RREvent(**row) 85 | if rr_event.start_file_index == rr_event.end_file_index: 86 | labels[rr_event.start_file_index][rr_event.start_rr_index:rr_event.end_rr_index] = 1 87 | else: 88 | labels[rr_event.start_file_index][rr_event.start_rr_index:] = 1 89 | labels[rr_event.end_file_index][:rr_event.end_rr_index] = 1 90 | if rr_event.end_file_index - rr_event.end_file_index > 1: 91 | for day in range(rr_event.start_file_index + 1, rr_event.end_file_index): 92 | labels[day][:] = 1 93 | self.rr_labels = labels 94 | 95 | def plot_rr(self, has_day_ticks=True, has_abnormal_color=False): 96 | all_rr = np.concatenate(self.rr) 97 | all_rr_labels = np.concatenate(self.rr_labels) 98 | 99 | fig, ax = plt.subplots(2, 1, figsize=(10, 8), sharex=True) 100 | 101 | ax[0].plot(all_rr) 102 | ax[0].set_ylabel("RR (ms)") 103 | 104 | ax[1].plot(all_rr_labels) 105 | ax[1].set_ylim(-0.1, 1.1) 106 | ax[1].set_yticks([0, 1]) 107 | ax[1].set_yticklabels(["NSR", "AF"]) 108 | ax[1].set_xlabel("RR index", labelpad=10) 109 | ax[1].set_ylabel("Label") 110 | 111 | ax[0].set_title(f"Record {self.metadata.record_id}") 112 | 113 | if has_day_ticks: 114 | # add vertical lines at the end of each day 115 | len_rr = [len(rr) for rr in self.rr] 116 | index_len_rr = np.cumsum(len_rr) 117 | index_len_rr = np.insert(index_len_rr, 0, 0) 118 | for i in index_len_rr: 119 | ax[0].axvline(i, color="k", linestyle="--", alpha=0.5) 120 | ax[1].axvline(i, color="k", linestyle="--", alpha=0.5) 121 | 122 | if has_abnormal_color: 123 | # color the background of the abnormal regions 124 | abnormal_regions_start = np.where(np.diff(all_rr_labels) == 1)[0] 125 | abnormal_regions_end = np.where(np.diff(all_rr_labels) == -1)[0] 126 | if len(abnormal_regions_start) > len(abnormal_regions_end): 127 | abnormal_regions_end = np.append(abnormal_regions_end, len(all_rr_labels)) 128 | elif len(abnormal_regions_start) < len(abnormal_regions_end): 129 | abnormal_regions_start = np.insert(abnormal_regions_start, 0, 0) 130 | for start, end in zip(abnormal_regions_start, abnormal_regions_end): 131 | ax[0].axvspan(start, end, alpha=0.3, color="red") 132 | ax[1].axvspan(start, end, alpha=0.3, color="red") 133 | 134 | plt.show() 135 | 136 | def load_ecg(self, clean_front=False): 137 | ecg_files = sorted(self.record_folder.glob("*_ecg_*.h5")) 138 | self.ecg = [self.__read_ecg_file(ecg_file, clean_front) for ecg_file in ecg_files] 139 | self.__create_ecg_labels(clean_front) 140 | 141 | def __read_ecg_file(self, ecg_file: Path, clean_front=False) -> np.ndarray: 142 | with h5py.File(ecg_file, "r") as f: 143 | key = list(f.keys())[0] 144 | ecg = f[key][:] 145 | if clean_front: 146 | ecg = ecg[6000:] 147 | return ecg 148 | 149 | def __read_ecg_labels(self) -> pd.DataFrame: 150 | ecg_labels = sorted(self.record_folder.glob("*ecg_labels.csv")) 151 | df_ecg_labels = pd.read_csv(ecg_labels[0]) 152 | return df_ecg_labels 153 | 154 | def __create_ecg_labels(self, clean_front=False): 155 | self.ecg_labels_df = self.__read_ecg_labels() 156 | len_ecg = [len(ecg) for ecg in self.ecg] 157 | 158 | start_day = self.ecg_labels_df["start_file_index"].unique() 159 | end_day = self.ecg_labels_df["end_file_index"].unique() 160 | days = np.unique(np.concatenate([start_day, end_day])) 161 | assert len(days) <= len(len_ecg) 162 | 163 | labels = [np.zeros(len_day_ecg) for len_day_ecg in len_ecg] 164 | 165 | for i, row in self.ecg_labels_df.iterrows(): 166 | if row.start_file_index == row.end_file_index: 167 | labels[row.start_file_index][row.start_qrs_index:row.end_qrs_index] = 1 168 | else: 169 | labels[row.start_file_index][row.start_qrs_index:] = 1 170 | labels[row.end_file_index][:row.end_qrs_index] = 1 171 | if row.end_file_index - row.end_file_index > 1: 172 | for day in range(row.start_file_index + 1, row.end_file_index): 173 | labels[day][:] = 1 174 | if clean_front: 175 | labels = [label[6000:] for label in labels] 176 | self.ecg_labels = labels 177 | 178 | def plot_ecg(self, has_day_ticks=True): 179 | all_ecg = np.concatenate(self.ecg) 180 | all_ecg_labels = np.concatenate(self.ecg_labels) 181 | 182 | # set font size 183 | plt.rcParams.update({"font.size": 18}) 184 | 185 | fig, ax = plt.subplots(3, 1, figsize=(10, 8), sharex=True) 186 | 187 | ax[0].plot(all_ecg[:, 0]) 188 | ax[0].set_ylabel("ECG I (mV)") 189 | 190 | ax[1].plot(all_ecg[:, 1]) 191 | ax[1].set_ylabel("ECG II (mV)") 192 | 193 | ax[2].plot(all_ecg_labels) 194 | ax[2].set_ylim(-0.1, 1.1) 195 | ax[2].set_yticks([0, 1]) 196 | ax[2].set_yticklabels(["NSR", "AF"]) 197 | ax[2].set_xlabel("ECG index", labelpad=10) 198 | ax[2].set_ylabel("Label") 199 | 200 | if has_day_ticks: 201 | # add vertical lines at the end of each day 202 | len_ecg = [len(ecg) for ecg in self.ecg] 203 | index_len_ecg = np.cumsum(len_ecg) 204 | index_len_ecg = np.insert(index_len_ecg, 0, 0) 205 | for i in index_len_ecg: 206 | ax[0].axvline(i, color="k", linestyle="--", alpha=0.5) 207 | ax[1].axvline(i, color="k", linestyle="--", alpha=0.5) 208 | ax[2].axvline(i, color="k", linestyle="--", alpha=0.5) 209 | 210 | plt.show() 211 | 212 | def number_of_episodes(self): 213 | rr_labels = sorted(self.record_folder.glob("*rr_labels.csv")) 214 | df_rr_labels = pd.read_csv(rr_labels[0]) 215 | num_episodes_rr = len(df_rr_labels) 216 | 217 | ecg_labels = sorted(self.record_folder.glob("*ecg_labels.csv")) 218 | df_ecg_labels = pd.read_csv(ecg_labels[0]) 219 | num_episodes_ecg = len(df_ecg_labels) 220 | 221 | assert num_episodes_rr == num_episodes_ecg 222 | return num_episodes_rr 223 | 224 | 225 | @dataclass 226 | class RecordMetadata: 227 | patient_id: str 228 | patient_sex: str 229 | patient_age: int 230 | record_id: str 231 | record_date: str 232 | record_start_time: str 233 | record_end_time: str 234 | record_timedelta: str 235 | record_n_files: int 236 | record_n_seconds: int 237 | record_n_samples: int 238 | 239 | 240 | @dataclass 241 | class ECGEvent: 242 | # start event 243 | start_datetime: str 244 | start_file_index: int 245 | start_qrs_index: int 246 | # end event 247 | end_datetime: str 248 | end_file_index: int 249 | end_qrs_index: int 250 | # duration 251 | af_duration: int 252 | nsr_duration: int 253 | 254 | 255 | @dataclass 256 | class RREvent: 257 | # start event 258 | start_file_index: int 259 | start_rr_index: int 260 | # end event 261 | end_file_index: int 262 | end_rr_index: int 263 | -------------------------------------------------------------------------------- /data/metadata.csv: -------------------------------------------------------------------------------- 1 | patient_id,patient_sex,patient_age,record_id,record_date,record_start_time,record_end_time,record_timedelta,record_files,record_seconds,record_samples 2 | patient_000,female,86,record_000,2012-10-02,2012-10-02T10:50:11,2012-10-03T10:50:02,86391,1,86391,17278200 3 | patient_001,female,72,record_001,2011-08-19,2011-08-19T11:19:55,2011-08-21T11:19:54,172799,2,172798,34559600 4 | patient_002,male,73,record_002,2012-01-16,2012-01-16T11:29:38,2012-01-17T09:34:22,79484,1,79484,15896800 5 | patient_003,female,71,record_003,2017-04-14,2017-04-14T10:18:10,2017-04-15T08:23:04,79494,1,79488,15897790 6 | patient_004,female,71,record_004,2008-08-06,2008-08-06T13:01:10,2008-08-07T11:05:49,79479,1,79479,15895800 7 | patient_005,female,74,record_005,2016-12-29,2016-12-29T11:32:24,2016-12-30T11:32:23,86399,1,86399,17279800 8 | patient_006,female,79,record_006,2010-04-24,2010-04-24T13:51:45,2010-04-28T12:54:09,342144,4,342112,68422592 9 | patient_007,female,74,record_007,2010-12-15,2010-12-15T10:07:32,2010-12-18T19:05:51,291499,4,291474,58294883 10 | patient_008,female,58,record_008,2012-08-27,2012-08-27T11:31:10,2012-08-29T11:31:09,172799,2,172798,34559600 11 | patient_009,male,41,record_009,2012-03-19,2012-03-19T11:02:35,2012-03-23T11:00:09,345454,4,326368,65273661 12 | patient_010,female,80,record_010,2013-11-01,2013-11-01T11:21:07,2013-11-02T11:21:00,86393,1,86393,17278600 13 | patient_011,male,76,record_011,2008-11-07,2008-11-07T09:20:53,2008-11-09T09:20:52,172799,2,172798,34559600 14 | patient_012,male,68,record_012,2016-06-28,2016-06-28T10:01:51,2016-06-29T10:01:44,86393,1,86393,17278600 15 | patient_013,male,59,record_013,2013-11-16,2013-11-16T14:23:53,2013-11-19T14:23:52,259199,3,259197,51839400 16 | patient_014,male,65,record_014,2013-11-18,2013-11-18T12:02:28,2013-11-19T12:02:18,86390,1,86390,17278000 17 | patient_015,female,87,record_015,2012-04-30,2012-04-30T14:43:20,2012-05-01T14:43:19,86399,1,86399,17279800 18 | patient_016,male,86,record_016,2010-03-07,2010-03-07T12:09:01,2010-03-08T12:08:51,86390,1,86390,17278000 19 | patient_017,female,68,record_017,2009-05-30,2009-05-30T11:15:48,2009-05-31T11:15:39,86391,1,86391,17278200 20 | patient_018,female,66,record_018,2017-07-08,2017-07-08T09:06:01,2017-07-09T09:06:00,86399,1,86399,17279800 21 | patient_019,female,99,record_019,2009-07-17,2009-07-17T09:59:47,2009-07-18T09:05:36,83149,1,83142,16628530 22 | patient_020,male,81,record_020,2007-10-28,2007-10-28T12:18:29,2007-11-01T07:28:31,328202,4,327996,65599266 23 | patient_021,female,82,record_021,2012-05-29,2012-05-29T10:01:55,2012-05-30T10:01:54,86399,1,86399,17279800 24 | patient_022,male,78,record_022,2014-05-07,2014-05-07T14:10:51,2014-05-08T10:46:15,74124,1,74121,14824250 25 | patient_023,male,63,record_023,2008-07-02,2008-07-02T15:24:34,2008-07-04T15:24:33,172799,2,172798,34559600 26 | patient_024,male,72,record_024,2011-06-22,2011-06-22T10:16:33,2011-06-24T10:16:32,172799,2,172798,34559600 27 | patient_025,female,64,record_025,2012-06-04,2012-06-04T10:00:41,2012-06-08T10:00:40,345599,4,345596,69119200 28 | patient_026,male,47,record_026,2012-02-20,2012-02-20T14:04:26,2012-02-23T14:04:25,259199,3,259197,51839400 29 | patient_027,male,73,record_027,2009-06-02,2009-06-02T09:43:48,2009-06-03T09:43:45,86397,1,86397,17279400 30 | patient_028,female,79,record_028,2013-06-30,2013-06-30T15:01:50,2013-07-04T09:57:35,327345,4,288005,57601192 31 | patient_029,male,68,record_029,2013-06-23,2013-06-23T14:29:07,2013-06-24T14:28:59,86392,1,86392,17278400 32 | patient_030,male,59,record_030,2015-12-23,2015-12-23T09:44:35,2015-12-24T09:44:34,86399,1,86399,17279800 33 | patient_031,male,77,record_031,2011-04-25,2011-04-25T11:59:55,2011-04-27T11:59:54,172799,2,172798,34559600 34 | patient_032,female,60,record_032,2009-10-28,2009-10-28T13:16:24,2009-10-30T11:19:30,165786,2,165606,33121269 35 | patient_033,female,83,record_033,2011-11-28,2011-11-28T09:51:34,2011-11-29T09:51:33,86399,1,86399,17279800 36 | patient_034,male,67,record_034,2011-12-06,2011-12-06T08:25:16,2011-12-09T08:25:15,259199,3,259197,51839400 37 | patient_035,female,59,record_035,2010-03-26,2010-03-26T16:55:11,2010-03-30T08:51:30,316579,4,316566,63313231 38 | patient_035,female,61,record_036,2011-11-11,2011-11-11T10:19:44,2011-11-14T10:31:52,259928,4,259850,51970146 39 | patient_035,female,66,record_037,2016-07-28,2016-07-28T11:31:21,2016-07-29T11:31:20,86399,1,86399,17279800 40 | patient_035,female,66,record_038,2017-02-17,2017-02-17T11:41:30,2017-02-18T11:41:29,86399,1,86399,17279800 41 | patient_036,male,90,record_039,2013-04-12,2013-04-12T11:12:06,2013-04-13T11:11:58,86392,1,86392,17278400 42 | patient_037,female,93,record_040,2014-08-21,2014-08-21T08:58:07,2014-08-22T08:57:59,86392,1,86392,17278400 43 | patient_038,female,89,record_041,2014-01-18,2014-01-18T12:01:35,2014-01-19T12:01:34,86399,1,83662,16732523 44 | patient_039,female,65,record_042,2011-03-18,2011-03-18T10:04:57,2011-03-20T10:04:56,172799,2,172798,34559600 45 | patient_040,female,89,record_043,2009-12-07,2009-12-07T12:39:04,2009-12-08T12:38:56,86392,1,86392,17278400 46 | patient_041,male,83,record_044,2013-08-28,2013-08-28T09:55:06,2013-08-29T09:55:03,86397,1,86397,17279400 47 | patient_042,female,66,record_045,2013-04-21,2013-04-21T10:41:33,2013-04-22T10:41:32,86399,1,86257,17251582 48 | patient_043,male,75,record_046,2017-04-01,2017-04-01T11:12:42,2017-04-04T11:12:41,259199,3,259197,51839400 49 | patient_044,female,66,record_047,2014-12-06,2014-12-06T10:15:34,2014-12-08T10:15:33,172799,2,172798,34559600 50 | patient_045,male,76,record_048,2013-03-19,2013-03-19T10:09:02,2013-03-20T10:08:56,86394,1,86394,17278800 51 | patient_045,male,80,record_049,2017-03-12,2017-03-12T12:07:17,2017-03-13T12:07:10,86393,1,86393,17278600 52 | patient_046,female,66,record_050,2016-02-05,2016-02-05T11:54:00,2016-02-06T09:58:32,79472,1,79472,15894400 53 | patient_047,male,71,record_051,2013-10-18,2013-10-18T10:24:54,2013-10-21T10:24:53,259199,3,259197,51839400 54 | patient_048,female,70,record_052,2008-06-02,2008-06-02T09:50:40,2008-06-04T09:50:39,172799,2,172798,34559600 55 | patient_049,female,80,record_053,2013-05-17,2013-05-17T07:24:03,2013-05-18T07:23:56,86393,1,86393,17278600 56 | patient_050,female,75,record_054,2013-11-30,2013-11-30T14:07:18,2013-12-01T14:07:17,86399,1,86394,17278800 57 | patient_051,male,68,record_055,2008-03-23,2008-03-23T15:35:17,2008-03-24T11:29:12,71635,1,71408,14281713 58 | patient_052,male,87,record_056,2010-09-30,2010-09-30T10:20:53,2010-10-01T08:24:46,79433,1,73356,14671273 59 | patient_053,female,65,record_057,2013-03-18,2013-03-18T12:35:31,2013-03-19T12:26:27,85856,1,85856,17171200 60 | patient_054,male,79,record_058,2010-04-08,2010-04-08T10:12:25,2010-04-09T10:12:15,86390,1,86390,17278000 61 | patient_055,male,95,record_059,2017-10-06,2017-10-06T08:22:30,2017-10-07T08:22:21,86391,1,86391,17278200 62 | patient_056,female,76,record_060,2015-08-31,2015-08-31T15:34:04,2015-09-03T15:34:03,259199,3,259197,51839400 63 | patient_056,female,77,record_061,2017-04-20,2017-04-20T13:38:22,2017-04-21T13:38:21,86399,1,86399,17279800 64 | patient_057,male,60,record_062,2012-11-06,2012-11-06T15:56:42,2012-11-08T15:56:41,172799,2,172798,34559600 65 | patient_058,male,78,record_063,2013-03-10,2013-03-10T11:05:16,2013-03-14T11:05:04,345588,4,345596,69119200 66 | patient_059,female,55,record_064,2013-01-20,2013-01-20T12:22:19,2013-01-22T12:08:05,171946,2,169937,33987570 67 | patient_060,male,55,record_065,2006-03-15,2006-03-15T00:07:46,2006-03-17T00:07:45,172799,2,172798,34559600 68 | patient_061,male,89,record_066,2014-03-25,2014-03-25T09:39:41,2014-03-26T09:39:37,86396,1,82744,16548800 69 | patient_062,female,75,record_067,2008-10-17,2008-10-17T08:58:46,2008-10-18T08:58:42,86396,1,86396,17279200 70 | patient_062,female,76,record_068,2009-03-03,2009-03-03T12:26:15,2009-03-07T12:09:57,344622,4,344346,68869292 71 | patient_063,male,81,record_069,2017-12-26,2017-12-26T13:57:09,2017-12-29T13:57:08,259199,3,259197,51839400 72 | patient_064,male,60,record_070,2009-03-07,2009-03-07T16:01:48,2009-03-08T16:01:38,86390,1,86390,17278000 73 | patient_064,male,66,record_071,2014-09-09,2014-09-09T11:59:54,2014-09-12T11:59:53,259199,3,259197,51839400 74 | patient_065,female,61,record_072,2015-09-25,2015-09-25T15:41:07,2015-09-29T15:41:06,345599,4,342112,68422592 75 | patient_066,male,77,record_073,2010-10-26,2010-10-26T10:02:06,2010-10-27T10:02:05,86399,1,83579,16715991 76 | patient_067,male,72,record_074,2011-12-11,2011-12-11T11:08:09,2011-12-13T11:08:08,172799,2,172798,34559600 77 | patient_068,female,94,record_075,2015-12-19,2015-12-19T14:52:18,2015-12-20T11:44:58,75160,1,73619,14723862 78 | patient_069,male,84,record_076,2009-06-07,2009-06-07T10:58:14,2009-06-08T10:58:05,86391,1,86391,17278200 79 | patient_070,male,69,record_077,2008-01-13,2008-01-13T08:48:02,2008-01-14T08:47:56,86394,1,86394,17278800 80 | patient_071,male,87,record_078,2014-06-25,2014-06-25T15:19:29,2014-06-26T15:19:28,86399,1,76635,15327055 81 | patient_072,female,83,record_079,2008-01-01,2008-01-01T13:00:42,2008-01-02T13:00:41,86399,1,79745,15949118 82 | patient_073,female,83,record_080,2011-10-30,2011-10-30T11:46:22,2011-10-31T09:54:16,79674,1,77044,15408949 83 | patient_074,male,78,record_081,2017-02-05,2017-02-05T08:59:06,2017-02-06T08:59:05,86399,1,83224,16644979 84 | patient_075,male,68,record_082,2013-12-07,2013-12-07T15:07:09,2013-12-08T14:49:58,85369,1,85192,17038407 85 | patient_076,male,69,record_083,2015-04-06,2015-04-06T15:00:19,2015-04-08T14:56:55,172596,2,172539,34507817 86 | patient_077,female,93,record_084,2008-03-22,2008-03-22T15:12:35,2008-03-24T15:12:34,172799,2,100456,20091273 87 | patient_078,male,61,record_085,2012-08-27,2012-08-27T10:08:17,2012-08-30T10:08:16,259199,3,258803,51760788 88 | patient_079,female,70,record_086,2009-07-25,2009-07-25T00:09:11,2009-07-28T05:34:12,278701,4,278659,55731937 89 | patient_080,male,67,record_087,2014-03-13,2014-03-13T11:02:34,2014-03-17T11:02:33,345599,4,345596,69119200 90 | patient_080,male,69,record_088,2016-01-28,2016-01-28T10:19:08,2016-01-31T10:19:07,259199,3,259197,51839400 91 | patient_081,female,91,record_089,2015-01-22,2015-01-22T11:06:01,2015-01-23T11:06:00,86399,1,86399,17279800 92 | patient_082,male,85,record_090,2010-04-26,2010-04-26T09:36:59,2010-04-27T09:36:49,86390,1,86390,17278000 93 | patient_082,male,87,record_091,2011-07-25,2011-07-25T16:31:11,2011-07-29T15:28:08,341817,4,341779,68355956 94 | patient_083,male,63,record_092,2013-06-29,2013-06-29T14:38:42,2013-07-03T14:38:27,345585,4,345596,69119200 95 | patient_084,male,61,record_093,2016-07-24,2016-07-24T15:05:47,2016-07-25T15:05:36,86389,1,86389,17277800 96 | patient_085,male,66,record_094,2016-01-10,2016-01-10T14:21:14,2016-01-11T14:21:13,86399,1,86399,17279800 97 | patient_086,male,60,record_095,2011-11-12,2011-11-12T10:34:39,2011-11-13T20:39:53,122714,2,122469,24493807 98 | patient_087,male,68,record_096,2010-01-24,2010-01-24T09:50:25,2010-01-25T09:50:24,86399,1,86399,17279800 99 | patient_088,female,79,record_097,2012-01-03,2012-01-03T10:02:09,2012-01-04T10:02:08,86399,1,86399,17279800 100 | patient_089,female,87,record_098,2013-09-28,2013-09-28T16:14:12,2013-09-29T16:14:11,86399,1,86399,17279800 101 | patient_090,female,81,record_099,2012-04-26,2012-04-26T11:11:41,2012-04-28T11:11:40,172799,2,172798,34559600 102 | patient_091,female,69,record_100,2016-09-17,2016-09-17T10:24:25,2016-09-18T10:24:24,86399,1,86399,17279800 103 | patient_092,female,63,record_101,2018-01-04,2018-01-04T03:48:12,2018-01-07T03:48:11,259199,3,259197,51839400 104 | patient_093,female,59,record_102,2015-02-04,2015-02-04T10:44:08,2015-02-05T10:43:57,86389,1,86389,17277800 105 | patient_094,female,78,record_103,2013-11-11,2013-11-11T09:19:34,2013-11-12T09:19:33,86399,1,86399,17279800 106 | patient_095,female,76,record_104,2011-12-24,2011-12-24T11:03:39,2011-12-25T11:03:38,86399,1,86399,17279800 107 | patient_096,female,86,record_105,2012-12-11,2012-12-11T11:38:19,2012-12-12T11:38:13,86394,1,86394,17278800 108 | patient_097,male,67,record_106,2011-12-21,2011-12-21T08:36:27,2011-12-22T08:36:22,86395,1,86395,17279000 109 | patient_098,male,91,record_107,2010-08-13,2010-08-13T08:57:55,2010-08-14T08:57:54,86399,1,85228,17045635 110 | patient_099,female,90,record_108,2016-01-01,2016-01-01T16:41:01,2016-01-02T16:41:00,86399,1,86399,17279800 111 | patient_100,male,70,record_109,2012-05-18,2012-05-18T11:03:03,2012-05-22T11:03:02,345599,4,345596,69119200 112 | patient_101,male,67,record_110,2009-09-13,2009-09-13T10:57:17,2009-09-16T10:57:16,259199,3,259197,51839400 113 | patient_101,male,67,record_111,2009-12-17,2009-12-17T13:37:28,2009-12-20T10:48:46,249078,3,240931,48186260 114 | patient_102,female,90,record_112,2013-01-11,2013-01-11T09:01:29,2013-01-12T09:01:28,86399,1,86399,17279800 115 | patient_103,female,76,record_113,2010-03-16,2010-03-16T13:02:43,2010-03-17T13:02:40,86397,1,86397,17279400 116 | patient_104,male,76,record_114,2012-04-03,2012-04-03T10:03:04,2012-04-04T10:03:03,86399,1,86399,17279800 117 | patient_105,male,78,record_115,2010-01-17,2010-01-17T10:45:15,2010-01-18T10:45:09,86394,1,86394,17278800 118 | patient_106,male,56,record_116,2008-09-28,2008-09-28T14:03:05,2008-10-02T14:02:54,345589,4,345596,69119200 119 | patient_107,female,63,record_117,2012-02-27,2012-02-27T14:34:04,2012-02-28T14:34:03,86399,1,86399,17279800 120 | patient_107,female,64,record_118,2013-02-20,2013-02-20T11:39:39,2013-02-21T11:39:38,86399,1,86399,17279800 121 | patient_108,female,98,record_119,2011-05-27,2011-05-27T09:02:24,2011-05-28T09:02:17,86393,1,86393,17278600 122 | patient_109,male,54,record_120,2010-11-28,2010-11-28T11:13:37,2010-11-30T08:26:55,162798,2,162802,32560566 123 | patient_109,male,55,record_121,2012-04-03,2012-04-03T11:42:11,2012-04-07T11:41:59,345588,4,345596,69119200 124 | patient_110,male,58,record_122,2009-11-19,2009-11-19T15:36:31,2009-11-20T15:36:23,86392,1,86392,17278400 125 | patient_111,male,62,record_123,2009-06-19,2009-06-19T11:20:03,2009-06-20T09:24:31,79468,1,79468,15893600 126 | patient_112,male,66,record_124,2014-05-18,2014-05-18T09:55:44,2014-05-22T09:55:43,345599,4,337048,67409740 127 | patient_113,male,58,record_125,2011-10-02,2011-10-02T11:39:57,2011-10-05T11:39:56,259199,3,259197,51839400 128 | patient_113,male,61,record_126,2014-08-03,2014-08-03T14:42:06,2014-08-04T14:42:00,86394,1,86394,17278800 129 | patient_114,male,78,record_127,2009-04-02,2009-04-02T10:34:22,2009-04-03T08:39:03,79481,1,79481,15896200 130 | patient_114,male,79,record_128,2009-09-23,2009-09-23T14:01:58,2009-09-24T11:54:30,78752,1,78206,15641266 131 | patient_115,female,73,record_129,2011-09-09,2011-09-09T09:50:55,2011-09-10T09:50:54,86399,1,86399,17279800 132 | patient_116,male,89,record_130,2013-09-25,2013-09-25T14:32:27,2013-09-26T14:32:18,86391,1,86391,17278200 133 | patient_117,female,79,record_131,2015-06-09,2015-06-09T11:31:29,2015-06-10T11:31:21,86392,1,86392,17278400 134 | patient_118,female,78,record_132,2008-05-20,2008-05-20T15:10:47,2008-05-21T11:15:33,72286,1,72286,14457200 135 | patient_119,male,60,record_133,2009-09-14,2009-09-14T16:53:12,2009-09-17T16:53:11,259199,3,259197,51839400 136 | patient_120,male,79,record_134,2013-03-15,2013-03-15T12:42:59,2013-03-16T12:42:54,86395,1,86395,17279000 137 | patient_121,male,54,record_135,2011-11-23,2011-11-23T09:46:26,2011-11-24T09:46:20,86394,1,86394,17278800 138 | patient_122,female,62,record_136,2014-02-07,2014-02-07T15:33:05,2014-02-10T12:06:15,246790,3,246800,49360082 139 | patient_123,female,75,record_137,2008-11-04,2008-11-04T09:14:18,2008-11-05T09:09:55,86137,1,83238,16647712 140 | patient_124,male,69,record_138,2007-09-04,2007-09-04T10:27:46,2007-09-05T10:27:37,86391,1,86391,17278200 141 | patient_125,female,66,record_139,2016-08-14,2016-08-14T11:32:29,2016-08-15T09:37:26,79497,1,79497,15899400 142 | patient_126,male,71,record_140,2011-04-22,2011-04-22T10:33:38,2011-04-23T08:38:15,79477,1,79477,15895400 143 | patient_127,male,82,record_141,2011-10-06,2011-10-06T11:04:12,2011-10-07T11:04:11,86399,1,86399,17279800 144 | patient_128,male,63,record_142,2011-01-26,2011-01-26T09:32:15,2011-01-29T09:32:14,259199,2,124730,24946157 145 | patient_129,male,86,record_143,2013-08-11,2013-08-11T03:33:30,2013-08-13T03:33:29,172799,2,172798,34559600 146 | patient_130,male,78,record_144,2010-04-28,2010-04-28T09:15:03,2010-04-29T09:14:53,86390,1,86390,17278000 147 | patient_131,female,83,record_145,2013-01-10,2013-01-10T16:46:55,2013-01-11T16:46:54,86399,1,86399,17279800 148 | patient_132,female,82,record_146,2012-12-27,2012-12-27T10:43:15,2012-12-28T10:43:14,86399,1,86399,17279800 149 | patient_133,male,86,record_147,2011-05-10,2011-05-10T11:01:51,2011-05-11T11:01:50,86399,1,86399,17279800 150 | patient_134,female,56,record_148,2016-01-01,2016-01-01T10:15:38,2016-01-03T10:15:37,172799,2,172798,34559600 151 | patient_135,male,59,record_149,2015-11-06,2015-11-06T11:14:15,2015-11-07T11:14:14,86399,1,86399,17279800 152 | patient_136,female,51,record_150,2012-11-02,2012-11-02T11:40:03,2012-11-05T13:10:52,264649,4,264590,52918181 153 | patient_137,male,78,record_151,2011-08-30,2011-08-30T12:51:51,2011-08-31T12:39:30,85659,1,85659,17131800 154 | patient_138,female,87,record_152,2014-08-24,2014-08-24T10:02:35,2014-08-25T10:02:34,86399,1,79849,15969917 155 | patient_139,male,82,record_153,2012-09-29,2012-09-29T15:18:56,2012-09-30T15:09:43,85847,1,85847,17169400 156 | patient_140,male,68,record_154,2012-08-16,2012-08-16T10:25:08,2012-08-17T10:25:07,86399,1,77714,15542925 157 | patient_141,female,66,record_155,2010-03-02,2010-03-02T11:27:43,2010-03-04T11:25:34,172671,2,172485,34497040 158 | patient_142,female,75,record_156,2011-12-02,2011-12-02T10:35:27,2011-12-03T10:35:26,86399,1,84042,16808453 159 | patient_143,male,72,record_157,2010-08-28,2010-08-28T17:38:51,2010-08-30T17:38:50,172799,2,172798,34559600 160 | patient_144,female,59,record_158,2002-02-19,2002-02-19T18:01:36,2002-02-20T17:48:08,85592,1,83797,16759546 161 | patient_144,female,71,record_159,2013-09-16,2013-09-16T15:17:18,2013-09-17T15:17:17,86399,1,86399,17279800 162 | patient_145,female,87,record_160,2012-07-26,2012-07-26T10:05:56,2012-07-28T09:46:40,171644,2,162972,32594541 163 | patient_146,male,81,record_161,2010-08-19,2010-08-19T13:42:11,2010-08-20T13:42:06,86395,1,86395,17279000 164 | patient_147,male,80,record_162,2016-01-06,2016-01-06T09:32:44,2016-01-07T09:32:43,86399,1,86399,17279800 165 | patient_148,female,82,record_163,2009-04-21,2009-04-21T11:55:03,2009-04-25T11:55:02,345599,4,345596,69119200 166 | patient_149,female,75,record_164,2010-11-03,2010-11-03T14:11:05,2010-11-04T14:10:56,86391,1,86391,17278200 167 | patient_150,male,82,record_165,2010-09-11,2010-09-11T11:20:54,2010-09-13T11:20:53,172799,2,172798,34559600 168 | patient_151,male,43,record_166,2009-10-19,2009-10-19T13:27:13,2009-10-21T08:08:12,153659,2,153390,30678001 169 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------