diff --git a/.clang-format b/.clang-format deleted file mode 100644 index 1878c25f..00000000 --- a/.clang-format +++ /dev/null @@ -1,7 +0,0 @@ ---- -# We'll use defaults from the LLVM style, but with 4 columns indentation. -BasedOnStyle: LLVM -IndentWidth: 4 -AlwaysBreakAfterReturnType: All -IndentPPDirectives: BeforeHash -ColumnLimit: 140 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index eabe44ce..776ec936 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,14 +7,6 @@ "ppa": true, "version": "latest" }, - "ghcr.io/devcontainers/features/nvidia-cuda:1": { - "installCudnn": true, - "installCudnnDev": true, - "installNvtx": true, - "installToolkit": true, - "cudaVersion": "12.2", - "cudnnVersion": "8.9.5.29" - }, "ghcr.io/devcontainers-contrib/features/pipenv:2": { "version": "latest" } @@ -31,10 +23,11 @@ "forwardPorts": [6006], - "postCreateCommand": "./.devcontainer/postCreateCommand.sh", + "postCreateCommand": "./.devcontainer/install.sh", "remoteEnv": { "LD_LIBRARY_PATH": "${containerEnv:LD_LIBRARY_PATH}:/usr/local/cuda/lib64", + "PATH": "${containerEnv:PATH}:/usr/local/cuda/bin", "TF_FORCE_GPU_ALLOW_GROWTH": "true" } } diff --git a/.devcontainer/install.sh b/.devcontainer/install.sh new file mode 100755 index 00000000..c5232499 --- /dev/null +++ b/.devcontainer/install.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +export DEBIAN_FRONTEND=noninteractive + +sudo apt update +sudo apt install -y libopenblas-dev libyaml-dev ffmpeg wget ca-certificates + +# Install CUDA and cuDNN if not already installed +if ! command -v nvcc &> /dev/null; then + + CUDA_VERSION="12.3" + CUDNN_VERSION="8.9.7.29-1+cuda12.2" # Not sure why no 12.3 + + NVIDIA_REPO_URL="https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64" + KEYRING_PACKAGE="cuda-keyring_1.1-1_all.deb" + KEYRING_PACKAGE_URL="$NVIDIA_REPO_URL/$KEYRING_PACKAGE" + KEYRING_PACKAGE_PATH="$(mktemp -d)" + KEYRING_PACKAGE_FILE="$KEYRING_PACKAGE_PATH/$KEYRING_PACKAGE" + wget -O "$KEYRING_PACKAGE_FILE" "$KEYRING_PACKAGE_URL" + sudo apt install -yq "$KEYRING_PACKAGE_FILE" + sudo apt update -yq + + # Install CUDA libraries + cuda_pkg="cuda-libraries-${CUDA_VERSION/./-}" + sudo apt install -yq "$cuda_pkg" + + # Install cuDNN + cudnn_pkg="libcudnn8=${CUDNN_VERSION}" + sudo apt install -yq "$cudnn_pkg_version" + + # Install cuDNN dev + cudnn_dev_pkg="libcudnn8-dev=${CUDNN_VERSION}" + sudo apt install -yq "$cudnn_dev_pkg" + + # Install NVTX + nvtx_pkg="cuda-nvtx-${CUDA_VERSION/./-}" + sudo apt install -yq "$nvtx_pkg" + + # Install CUDA Toolkit + toolkit_pkg="cuda-toolkit-${CUDA_VERSION/./-}" + sudo apt install -yq "$toolkit_pkg" + + export PATH=/usr/local/cuda/bin${PATH:+:${PATH}} + export LD_LIBRARY_PATH=/usr/local/cuda-${CUDA_VERSION}/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} + + # Clean up + sudo rm -rf /var/lib/apt/lists/* +fi + +# Install poetry +pipx install poetry --pip-args '--no-cache-dir --force-reinstall' + +# Install project dependencies +poetry install diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh deleted file mode 100755 index cf770a4f..00000000 --- a/.devcontainer/postCreateCommand.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -sudo apt update -sudo apt install -y libopenblas-dev libyaml-dev ffmpeg - -# Install poetry -pipx install poetry --pip-args '--no-cache-dir --force-reinstall' - -# Install project dependencies -poetry install diff --git a/configs/arr-2-eff-sm.json b/configs/arr-2-eff-sm.json index 0d8df9a3..8593ab16 100644 --- a/configs/arr-2-eff-sm.json +++ b/configs/arr-2-eff-sm.json @@ -2,18 +2,18 @@ "name": "arr-2-eff-sm", "project": "hk-rhythm-2", "job_dir": "./results/arr-2-eff-sm", + "verbose": 2, + "dataset_weights": [0.32, 0.68], "datasets": [{ - "name": "icentia11k", - "path": "./datasets/icentia11k", - "params": {} - }, { "name": "ptbxl", - "path": "./datasets/ptbxl", - "params": {} + "params": { + "path": "./datasets/ptbxl" + } }, { "name": "lsad", - "path": "./datasets/lsad", - "params": {} + "params": { + "path": "./datasets/lsad" + } }], "num_classes": 2, "class_map": { @@ -24,16 +24,14 @@ "class_names": [ "NORMAL", "AFIB/AFL" ], + "class_weights": "balanced", "sampling_rate": 100, "frame_size": 512, - "model_file": "model.keras", - "use_logits": false, "samples_per_patient": [10, 10], - "val_samples_per_patient": [10, 10], - "val_file": "./results/${task}-class-2-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", - "test_file": "./results/${task}-class-2-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", + "val_samples_per_patient": [5, 5], + "test_samples_per_patient": [5, 5], "val_patients": 0.20, - "test_samples_per_patient": [10, 10], + "val_size": 40000, "test_size": 40000, "batch_size": 256, "buffer_size": 50000, @@ -42,9 +40,8 @@ "val_metric": "loss", "lr_rate": 1e-3, "lr_cycles": 1, - "class_weights": "balanced", "threshold": 0.75, - "val_acc_threshold": 0.98, + "val_metric_threshold": 0.98, "tflm_var_name": "g_rhythm_model", "tflm_file": "rhythm_model_buffer.h", "backend": "pc", @@ -59,23 +56,17 @@ }, "preprocesses": [ { - "name": "filter", - "params": { - "lowcut": 1.0, - "highcut": 30, - "order": 3, - "forward_backward": true, - "axis": 0 - } - }, - { - "name": "znorm", + "name": "layer_norm", "params": { - "eps": 0.01, - "axis": null + "epsilon": 0.01, + "name": "znorm" } } ], + "augmentations": [ + ], + "model_file": "model.keras", + "use_logits": false, "architecture": { "name": "efficientnetv2", "params": { diff --git a/configs/arr-4-eff-lg.json b/configs/arr-4-eff-lg.json index df1d9f8a..ac661f6b 100644 --- a/configs/arr-4-eff-lg.json +++ b/configs/arr-4-eff-lg.json @@ -2,10 +2,11 @@ "name": "arr-4-eff-lg", "project": "hk-rhythm-4", "job_dir": "./results/arr-4-eff-lg", + "verbose": 2, "datasets": [{ "name": "lsad", - "path": "./datasets/lsad", "params": { + "path": "./datasets/lsad" } }], "num_classes": 4, @@ -20,27 +21,24 @@ "class_names": [ "SR", "SB", "AFIB", "GSVT" ], + "class_weights": "balanced", "sampling_rate": 100, "frame_size": 800, - "model_file": "model.keras", - "use_logits": true, "samples_per_patient": [5, 5, 5, 10], - "val_file": "./results/${task}-class-4-${dataset}-${sampling_rate}fs-${frame_size}sz-noaug.pkl", - "test_file": "./results/${task}-class-4-${dataset}-${sampling_rate}fs-${frame_size}sz-noaug.pkl", "val_samples_per_patient": [5, 5, 5, 10], - "val_patients": 0.20, "test_samples_per_patient": [5, 5, 5, 10], + "val_patients": 0.20, + "val_size": 40000, "test_size": 50000, "batch_size": 256, "buffer_size": 50000, - "epochs": 100, + "epochs": 150, "steps_per_epoch": 50, "val_metric": "loss", "lr_rate": 1e-3, "lr_cycles": 1, - "class_weights": "balanced", "threshold": 0.5, - "val_acc_threshold": 0.98, + "val_metric_threshold": 0.98, "tflm_var_name": "g_rhythm_model", "tflm_file": "rhythm_model_buffer.h", "backend": "pc", @@ -55,23 +53,58 @@ }, "preprocesses": [ { - "name": "filter", + "name": "layer_norm", "params": { - "lowcut": 1.0, - "highcut": 30, - "order": 3, - "forward_backward": true, - "axis": 0 - } - }, - { - "name": "znorm", - "params": { - "eps": 0.01, - "axis": null + "epsilon": 0.01, + "name": "znorm" } } ], + "augmentations-dis": [{ + "name": "random_noise_distortion", + "params": { + "amplitude": [0.01, 0.5], + "frequency": [0.5, 1.5], + "name": "baseline_wander" + } + },{ + "name": "random_sine_wave", + "params": { + "amplitude": [0.01, 0.05], + "frequency": [45, 50], + "auto_vectorize": false, + "name": "powerline_noise" + } + },{ + "name": "amplitude_warp", + "params": { + "amplitude": [0.99, 1.01], + "frequency": [0.5, 1.5], + "name": "amplitude_warp" + } + }, { + "name": "random_noise", + "params": { + "factor": [0.005, 0.05], + "name": "random_noise" + } + }, { + "name": "random_background_noise", + "params": { + "amplitude": [0.005, 0.1], + "num_noises": 1, + "name": "nstdb" + } + },{ + "name": "random_cutout", + "params": { + "cutouts": 2, + "factor": [0.005, 0.01], + "name": "cutout" + } + }], + "model_file": "model.keras", + "use_logits": true, "architecture": { "name": "efficientnetv2", "params": { @@ -89,43 +122,5 @@ "include_top": true, "use_logits": true } - }, - "augmentations-dis": [ - { - "name": "baseline_wander", - "params": { - "amplitude": [0.0, 0.2], - "frequency": [0.5, 1.5] - } - }, - { - "name": "powerline_noise", - "params": { - "amplitude": [0.0, 0.15], - "frequency": [45, 50] - } - }, - { - "name": "burst_noise", - "params": { - "burst_number": [0, 4], - "amplitude": [0.0, 0.1], - "frequency": [20, 49] - } - }, - { - "name": "noise_sources", - "params": { - "num_sources": [1, 2], - "amplitude": [0.0, 0.1], - "frequency": [10, 40] - } - }, - { - "name": "lead_noise", - "params": { - "scale": [0.05, 0.2] - } - } - ] + } } diff --git a/configs/arr-4-eff-sm.json b/configs/arr-4-eff-sm.json index 72ba7792..17fd7345 100644 --- a/configs/arr-4-eff-sm.json +++ b/configs/arr-4-eff-sm.json @@ -2,10 +2,11 @@ "name": "arr-4-eff-sm", "project": "hk-rhythm-4", "job_dir": "./results/arr-4-eff-sm", + "verbose": 2, "datasets": [{ "name": "lsad", - "path": "./datasets/lsad", "params": { + "path": "./datasets/lsad" } }], "num_classes": 4, @@ -20,27 +21,24 @@ "class_names": [ "SR", "SB", "AFIB", "GSVT" ], + "class_weights": "balanced", "sampling_rate": 100, "frame_size": 800, - "model_file": "model.keras", - "use_logits": false, "samples_per_patient": [5, 5, 5, 10], - "val_file": "./results/${task}-class-4-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", - "test_file": "./results/${task}-class-4-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", "val_samples_per_patient": [5, 5, 5, 10], - "val_patients": 0.20, "test_samples_per_patient": [5, 5, 5, 10], + "val_patients": 0.20, + "val_size": 40000, "test_size": 50000, "batch_size": 256, "buffer_size": 50000, - "epochs": 100, + "epochs": 200, "steps_per_epoch": 50, "val_metric": "loss", "lr_rate": 1e-3, "lr_cycles": 1, - "class_weights": "balanced", "threshold": 0.5, - "val_acc_threshold": 0.98, + "val_metric_threshold": 0.98, "tflm_var_name": "g_rhythm_model", "tflm_file": "rhythm_model_buffer.h", "backend": "pc", @@ -55,23 +53,58 @@ }, "preprocesses": [ { - "name": "filter", + "name": "layer_norm", "params": { - "lowcut": 1.0, - "highcut": 30, - "order": 3, - "forward_backward": true, - "axis": 0 - } - }, - { - "name": "znorm", - "params": { - "eps": 0.01, - "axis": null + "epsilon": 0.01, + "name": "znorm" } } ], + "augmentations-dis": [{ + "name": "random_noise_distortion", + "params": { + "amplitude": [0.01, 0.5], + "frequency": [0.5, 1.5], + "name": "baseline_wander" + } + },{ + "name": "random_sine_wave", + "params": { + "amplitude": [0.01, 0.05], + "frequency": [45, 50], + "auto_vectorize": false, + "name": "powerline_noise" + } + },{ + "name": "amplitude_warp", + "params": { + "amplitude": [0.99, 1.01], + "frequency": [0.5, 1.5], + "name": "amplitude_warp" + } + }, { + "name": "random_noise", + "params": { + "factor": [0.005, 0.05], + "name": "random_noise" + } + }, { + "name": "random_background_noise", + "params": { + "amplitude": [0.005, 0.1], + "num_noises": 1, + "name": "nstdb" + } + },{ + "name": "random_cutout", + "params": { + "cutouts": 2, + "factor": [0.005, 0.01], + "name": "cutout" + } + }], + "model_file": "model.keras", + "use_logits": false, "architecture": { "name": "efficientnetv2", "params": { @@ -89,43 +122,5 @@ "include_top": true, "use_logits": true } - }, - "augmentations": [ - { - "name": "baseline_wander", - "params": { - "amplitude": [0.0, 0.2], - "frequency": [0.5, 1.5] - } - }, - { - "name": "powerline_noise", - "params": { - "amplitude": [0.0, 0.15], - "frequency": [45, 50] - } - }, - { - "name": "burst_noise", - "params": { - "burst_number": [0, 4], - "amplitude": [0.0, 0.1], - "frequency": [20, 49] - } - }, - { - "name": "noise_sources", - "params": { - "num_sources": [1, 2], - "amplitude": [0.0, 0.1], - "frequency": [10, 40] - } - }, - { - "name": "lead_noise", - "params": { - "scale": [0.05, 0.2] - } - } - ] + } } diff --git a/configs/beat-2-eff-sm.json b/configs/beat-2-eff-sm.json index 619fec18..c95e130a 100644 --- a/configs/beat-2-eff-sm.json +++ b/configs/beat-2-eff-sm.json @@ -1,11 +1,13 @@ { "name": "beat-2-eff-sm", "project": "hk-beat-2", + "verbose": 2, "job_dir": "./results/beat-2-eff-sm", "datasets": [{ "name": "icentia11k", - "path": "./datasets/icentia11k", - "params": {} + "params": { + "path": "./datasets/icentia11k" + } }], "num_classes": 2, "class_map": { @@ -16,33 +18,29 @@ "class_names": [ "QRS", "PAC/PVC" ], + "class_weights": "balanced", "sampling_rate": 100, "frame_size": 512, - "model_file": "model.keras", - "use_logits": false, "samples_per_patient": [20, 20], "val_samples_per_patient": [20, 20], - "val_file": "./results/${task}-class-2-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", - "test_file": "./results/${task}-class-2-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", - "val_patients": 0.20, - "val_size": 24000, "test_samples_per_patient": [20, 20], + "val_patients": 0.20, + "val_size": 30000, "test_size": 30000, "batch_size": 256, - "buffer_size": 80000, + "buffer_size": 10000, "epochs": 150, "steps_per_epoch": 50, "val_metric": "loss", "lr_rate": 1e-3, "lr_cycles": 1, - "class_weights": "balanced", "threshold": 0.60, "val_acc_threshold": 0.98, "tflm_var_name": "g_beat_model", "tflm_file": "beat_model_buffer.h", "backend": "pc", "demo_size": 1024, - "display_report": false, + "display_report": true, "quantization": { "qat": false, "mode": "FP32", @@ -52,23 +50,15 @@ }, "preprocesses": [ { - "name": "filter", - "params": { - "lowcut": 1.0, - "highcut": 30, - "order": 3, - "forward_backward": true, - "axis": 0 - } - }, - { - "name": "znorm", + "name": "layer_norm", "params": { - "eps": 0.01, - "axis": null + "epsilon": 0.01, + "name": "znorm" } } ], + "model_file": "model.keras", + "use_logits": false, "architecture": { "name": "efficientnetv2", "params": { diff --git a/configs/beat-3-eff-sm.json b/configs/beat-3-eff-sm.json index 0df21dd8..56a9d4ed 100644 --- a/configs/beat-3-eff-sm.json +++ b/configs/beat-3-eff-sm.json @@ -1,11 +1,13 @@ { - "name": "beat-3-eff-lg-layer-att", + "name": "beat-3-eff-lg", "project": "hk-beat-3", - "job_dir": "./results/beat-3-eff-lg-layer-att", + "job_dir": "./results/beat-3-eff-lg", + "verbose": 2, "datasets": [{ "name": "icentia11k", - "path": "./datasets/icentia11k", - "params": {} + "params": { + "path": "./datasets/icentia11k" + } }], "num_classes": 3, "class_map": { @@ -16,28 +18,24 @@ "class_names": [ "QRS", "PAC", "PVC" ], + "class_weights": "balanced", "sampling_rate": 100, "frame_size": 512, - "model_file": "model.keras", - "use_logits": false, "samples_per_patient": [10, 40, 40], "val_samples_per_patient": [10, 40, 40], - "val_file": "./results/${task}-class-3-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", - "test_file": "./results/${task}-class-3-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", + "test_samples_per_patient": [10, 40, 40], "val_patients": 0.20, "val_size": 30000, - "test_samples_per_patient": [10, 40, 40], "test_size": 30000, "batch_size": 256, - "buffer_size": 80000, + "buffer_size": 50000, "epochs": 150, "steps_per_epoch": 50, "val_metric": "loss", + "val_metric_threshold": 0.98, "lr_rate": 1e-3, "lr_cycles": 1, - "class_weights": "balanced", "threshold": 0.5, - "val_acc_threshold": 0.98, "tflm_var_name": "g_beat_model", "tflm_file": "beat_model_buffer.h", "backend": "pc", @@ -52,23 +50,15 @@ }, "preprocesses": [ { - "name": "filter", - "params": { - "lowcut": 1.0, - "highcut": 30, - "order": 3, - "forward_backward": true, - "axis": 0 - } - }, - { - "name": "znorm", + "name": "layer_norm", "params": { - "eps": 0.01, - "axis": null + "epsilon": 0.01, + "name": "znorm" } } ], + "model_file": "model.keras", + "use_logits": false, "architecture": { "name": "efficientnetv2", "params": { @@ -79,9 +69,9 @@ {"filters": 32, "depth": 2, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, {"filters": 48, "depth": 2, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, {"filters": 56, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, - {"filters": 64, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer", "att_ratio": 16}, - {"filters": 72, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer", "att_ratio": 18}, - {"filters": 96, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer", "att_ratio": 24} + {"filters": 64, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, + {"filters": 72, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, + {"filters": 96, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"} ], "norm": "layer", "output_filters": 0, diff --git a/configs/den-ppg-tcn-lg.json b/configs/den-ppg-tcn-lg.json index 6a2900f8..bbfe4a92 100644 --- a/configs/den-ppg-tcn-lg.json +++ b/configs/den-ppg-tcn-lg.json @@ -1,12 +1,13 @@ { - "name": "den-ppg-tcn-xl", + "name": "den-ppg-tcn-lg", "project": "hk-denoise", - "job_dir": "./results/den-ppg-tcn-xl", + "job_dir": "./results/den-ppg-tcn-lg", + "signal_type": "PPG", + "verbose": 2, "datasets": [{ - "name": "syntheticppg", - "path": "./datasets/syntheticppg", + "name": "ppg-synthetic", "params": { - "num_pts": 20000, + "num_pts": 40000, "params": { "duration": 20, "sample_rate": 100, @@ -17,20 +18,17 @@ } } }], - "num_classes": 1, - "class_map": {}, - "class_names": ["CLEAN"], "sampling_rate": 100, "frame_size": 256, - "model_file": "model.keras", "samples_per_patient": 5, "val_samples_per_patient": 5, - "val_patients": 0.20, "test_samples_per_patient": 5, + "val_patients": 0.20, + "val_size": 10000, "test_size": 10000, "batch_size": 128, "buffer_size": 50000, - "epochs": 100, + "epochs": 200, "steps_per_epoch": 50, "val_metric": "loss", "lr_rate": 1e-3, @@ -39,7 +37,7 @@ "tflm_file": "ppg_denoise_flatbuffer.h", "backend": "pc", "demo_size": 768, - "display_report": true, + "display_report": false, "quantization": { "qat": false, "mode": "FP32", @@ -47,75 +45,68 @@ "concrete": true, "debug": false }, + "preprocesses": [ + { + "name": "layer_norm", + "params": { + "epsilon": 0.001, + "name": "znorm" + } + } + ], + "augmentations": [{ + "name": "random_noise_distortion", + "params": { + "amplitude": [0, 0.4], + "frequency": [0.5, 1.5], + "name": "baseline_wander" + } + },{ + "name": "random_sine_wave", + "params": { + "amplitude": [0, 0.2], + "frequency": [45, 50], + "name": "powerline_noise" + } + },{ + "name": "amplitude_warp", + "params": { + "amplitude": [0.8, 1.2], + "frequency": [0.5, 1.5], + "name": "amplitude_warp" + } + }, { + "name": "random_noise", + "params": { + "factor": [0, 0.5], + "name": "random_noise" + } + }, { + "name": "random_background_noise", + "params": { + "amplitude": [0, 0.5], + "num_noises": 1, + "name": "nstdb" + } + }], + "model_file": "model.keras", + "use_logits": true, "architecture": { "name": "tcn", "params": { "input_kernel": [1, 9], "input_norm": "batch", "blocks": [ - {"depth": 2, "branch": 1, "filters": 16, "kernel": [1, 9], "dilation": [1, 1], "dropout": 0, "ex_ratio": 1, "se_ratio": 0, "norm": "batch"}, - {"depth": 2, "branch": 1, "filters": 24, "kernel": [1, 9], "dilation": [1, 1], "dropout": 0, "ex_ratio": 1, "se_ratio": 4, "norm": "batch"}, - {"depth": 2, "branch": 1, "filters": 32, "kernel": [1, 9], "dilation": [1, 2], "dropout": 0, "ex_ratio": 1, "se_ratio": 4, "norm": "batch"}, - {"depth": 1, "branch": 1, "filters": 48, "kernel": [1, 9], "dilation": [1, 4], "dropout": 0, "ex_ratio": 1, "se_ratio": 4, "norm": "batch"}, - {"depth": 1, "branch": 1, "filters": 64, "kernel": [1, 9], "dilation": [1, 8], "dropout": 0, "ex_ratio": 1, "se_ratio": 4, "norm": "batch"} + {"depth": 1, "branch": 1, "filters": 16, "kernel": [1, 9], "dilation": [1, 1], "dropout": 0, "ex_ratio": 1, "se_ratio": 0, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 24, "kernel": [1, 9], "dilation": [1, 1], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 32, "kernel": [1, 9], "dilation": [1, 2], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 40, "kernel": [1, 9], "dilation": [1, 4], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 48, "kernel": [1, 9], "dilation": [1, 8], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"} ], "output_kernel": [1, 9], "include_top": true, "use_logits": true, "model_name": "tcn" } - }, - "preprocesses": [ - { - "name": "znorm", - "params": { - "eps": 0.001, - "axis": null - } - } - ], - "augmentations": [ - { - "name": "baseline_wander", - "params": { - "amplitude": [0.5, 2.0], - "frequency": [0.5, 1.5] - } - }, - { - "name": "powerline_noise", - "params": { - "amplitude": [0.05, 0.2], - "frequency": [45, 50] - } - }, - { - "name": "burst_noise", - "params": { - "burst_number": [0, 4], - "amplitude": [0.05, 0.2], - "frequency": [20, 49] - } - }, - { - "name": "noise_sources", - "params": { - "num_sources": [1, 2], - "amplitude": [0.05, 0.2], - "frequency": [10, 40] - } - }, - { - "name": "lead_noise", - "params": { - "scale": [0.05, 0.5] - } - }, - { - "name": "nstdb", - "params": { - "noise_level": [0.05, 0.5] - } - } - ] + } } diff --git a/configs/den-ppg-tcn-sm.json b/configs/den-ppg-tcn-sm.json index a15ba611..e7be6581 100644 --- a/configs/den-ppg-tcn-sm.json +++ b/configs/den-ppg-tcn-sm.json @@ -2,11 +2,12 @@ "name": "den-ppg-tcn-sm", "project": "hk-denoise", "job_dir": "./results/den-ppg-tcn-sm", + "signal_type": "PPG", + "verbose": 2, "datasets": [{ - "name": "syntheticppg", - "path": "./datasets/syntheticppg", + "name": "ppg-synthetic", "params": { - "num_pts": 20000, + "num_pts": 40000, "params": { "duration": 20, "sample_rate": 100, @@ -17,21 +18,17 @@ } } }], - "num_classes": 1, - "class_map": {}, - "class_names": ["CLEAN"], "sampling_rate": 100, "frame_size": 256, - "model_file": "model.keras", "samples_per_patient": 5, "val_samples_per_patient": 5, + "test_samples_per_patient": 5, "val_patients": 0.20, "val_size": 10000, - "test_samples_per_patient": 5, - "test_size": 5000, + "test_size": 10000, "batch_size": 128, "buffer_size": 50000, - "epochs": 100, + "epochs": 200, "steps_per_epoch": 50, "val_metric": "loss", "lr_rate": 1e-3, @@ -40,7 +37,7 @@ "tflm_file": "ppg_denoise_flatbuffer.h", "backend": "pc", "demo_size": 768, - "display_report": true, + "display_report": false, "quantization": { "qat": false, "mode": "FP32", @@ -48,74 +45,67 @@ "concrete": true, "debug": false }, + "preprocesses": [ + { + "name": "layer_norm", + "params": { + "epsilon": 0.001, + "name": "znorm" + } + } + ], + "augmentations": [{ + "name": "random_noise_distortion", + "params": { + "amplitude": [0, 0.4], + "frequency": [0.5, 1.5], + "name": "baseline_wander" + } + },{ + "name": "random_sine_wave", + "params": { + "amplitude": [0, 0.2], + "frequency": [45, 50], + "name": "powerline_noise" + } + },{ + "name": "amplitude_warp", + "params": { + "amplitude": [0.8, 1.2], + "frequency": [0.5, 1.5], + "name": "amplitude_warp" + } + }, { + "name": "random_noise", + "params": { + "factor": [0, 0.5], + "name": "random_noise" + } + }, { + "name": "random_background_noise", + "params": { + "amplitude": [0, 0.5], + "num_noises": 1, + "name": "nstdb" + } + }], + "model_file": "model.keras", + "use_logits": true, "architecture": { "name": "tcn", "params": { - "input_kernel": [1, 7], + "input_kernel": [1, 9], "input_norm": "batch", "blocks": [ - {"depth": 1, "branch": 1, "filters": 8, "kernel": [1, 7], "dilation": [1, 1], "dropout": 0, "ex_ratio": 1, "se_ratio": 0, "norm": "batch"}, - {"depth": 1, "branch": 1, "filters": 16, "kernel": [1, 7], "dilation": [1, 1], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, - {"depth": 1, "branch": 1, "filters": 24, "kernel": [1, 7], "dilation": [1, 2], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, - {"depth": 1, "branch": 1, "filters": 32, "kernel": [1, 7], "dilation": [1, 4], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"} + {"depth": 1, "branch": 1, "filters": 8, "kernel": [1, 9], "dilation": [1, 1], "dropout": 0, "ex_ratio": 1, "se_ratio": 0, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 16, "kernel": [1, 9], "dilation": [1, 2], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 24, "kernel": [1, 9], "dilation": [1, 4], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 32, "kernel": [1, 9], "dilation": [1, 8], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"} ], - "output_kernel": [1, 7], + "output_kernel": [1, 9], "include_top": true, "use_logits": true, "model_name": "tcn" } - }, - "preprocesses": [ - { - "name": "znorm", - "params": { - "eps": 0.001, - "axis": null - } - } - ], - "augmentations": [ - { - "name": "baseline_wander", - "params": { - "amplitude": [0.5, 2.0], - "frequency": [0.5, 1.5] - } - }, - { - "name": "powerline_noise", - "params": { - "amplitude": [0.05, 0.2], - "frequency": [45, 50] - } - }, - { - "name": "burst_noise", - "params": { - "burst_number": [0, 4], - "amplitude": [0.05, 0.2], - "frequency": [20, 49] - } - }, - { - "name": "noise_sources", - "params": { - "num_sources": [1, 2], - "amplitude": [0.05, 0.2], - "frequency": [10, 40] - } - }, - { - "name": "lead_noise", - "params": { - "scale": [0.05, 0.5] - } - }, - { - "name": "nstdb", - "params": { - "noise_level": [0.05, 0.5] - } - } - ] + } } diff --git a/configs/den-tcn-lg.json b/configs/den-tcn-lg.json index d776fed6..7ea88e6f 100644 --- a/configs/den-tcn-lg.json +++ b/configs/den-tcn-lg.json @@ -2,15 +2,16 @@ "name": "den-tcn-lg", "project": "hk-denoise", "job_dir": "./results/den-tcn-lg", + "verbose": 2, + "dataset_weights": [0.9, 0.1], "datasets": [{ - "name": "synthetic", - "path": "./datasets/synthetic", + "name": "ecg-synthetic", "params": { - "num_pts": 10000, + "num_pts": 20000, "params": { "presets": ["SR", "AFIB", "ant_STEMI", "LAHB", "LPHB", "high_take_off", "LBBB", "random_morphology"], - "preset_weights": [8, 4, 1, 1, 1, 1, 1, 1], - "duration": 20, + "preset_weights": [24, 8, 1, 1, 1, 1, 1, 0], + "duration": 10, "sample_rate": 100, "heart_rate": [40, 160], "impedance": [1, 2], @@ -22,25 +23,20 @@ } }, { "name": "ptbxl", - "path": "./datasets/ptbxl", "params": { + "path": "./datasets/ptbxl" } }], - "num_classes": 1, - "class_map": {}, - "class_names": ["CLEAN"], "sampling_rate": 100, "frame_size": 256, - "model_file": "model.keras", - "use_logits": false, - "samples_per_patient": 10, + "samples_per_patient": 5, "val_samples_per_patient": 10, + "test_samples_per_patient": 10, "val_patients": 0.20, "val_size": 10000, - "test_samples_per_patient": 10, - "test_size": 5000, - "batch_size": 128, - "buffer_size": 50000, + "test_size": 10000, + "batch_size": 256, + "buffer_size": 25000, "epochs": 150, "steps_per_epoch": 50, "val_metric": "loss", @@ -50,7 +46,7 @@ "tflm_file": "ecg_denoise_flatbuffer.h", "backend": "pc", "demo_size": 768, - "display_report": true, + "display_report": false, "quantization": { "qat": false, "mode": "FP32", @@ -58,6 +54,52 @@ "concrete": true, "debug": false }, + "preprocesses": [ + { + "name": "layer_norm", + "params": { + "epsilon": 0.01, + "name": "znorm" + } + } + ], + "augmentations": [{ + "name": "random_noise_distortion", + "params": { + "amplitude": [0, 0.5], + "frequency": [0.5, 1.5], + "name": "baseline_wander" + } + },{ + "name": "random_sine_wave", + "params": { + "amplitude": [0, 0.05], + "frequency": [45, 50], + "name": "powerline_noise" + } + },{ + "name": "amplitude_warp", + "params": { + "amplitude": [0.9, 1.1], + "frequency": [0.5, 1.5], + "name": "amplitude_warp" + } + }, { + "name": "random_noise", + "params": { + "factor": [0.05, 0.1], + "name": "random_noise" + } + }, { + "name": "random_background_noise", + "params": { + "amplitude": [0.05, 0.1], + "num_noises": 1, + "name": "nstdb" + } + }], + "model_file": "model.keras", + "use_logits": true, "architecture": { "name": "tcn", "params": { @@ -65,8 +107,9 @@ "input_norm": "batch", "blocks": [ {"depth": 1, "branch": 1, "filters": 16, "kernel": [1, 7], "dilation": [1, 1], "dropout": 0, "ex_ratio": 1, "se_ratio": 0, "norm": "batch"}, - {"depth": 1, "branch": 1, "filters": 24, "kernel": [1, 7], "dilation": [1, 2], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, - {"depth": 1, "branch": 1, "filters": 32, "kernel": [1, 7], "dilation": [1, 4], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 24, "kernel": [1, 7], "dilation": [1, 1], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 32, "kernel": [1, 7], "dilation": [1, 2], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 40, "kernel": [1, 7], "dilation": [1, 4], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, {"depth": 1, "branch": 1, "filters": 48, "kernel": [1, 7], "dilation": [1, 8], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"} ], "output_kernel": [1, 7], @@ -74,58 +117,5 @@ "use_logits": true, "model_name": "tcn" } - }, - "preprocesses": [ - { - "name": "znorm", - "params": { - "eps": 0.01, - "axis": null - } - } - ], - "augmentations": [ - { - "name": "baseline_wander", - "params": { - "amplitude": [0.0, 0.5], - "frequency": [0.5, 1.5] - } - }, - { - "name": "powerline_noise", - "params": { - "amplitude": [0.05, 0.15], - "frequency": [45, 50] - } - }, - { - "name": "burst_noise", - "params": { - "burst_number": [0, 4], - "amplitude": [0.05, 0.1], - "frequency": [20, 49] - } - }, - { - "name": "noise_sources", - "params": { - "num_sources": [1, 2], - "amplitude": [0.05, 0.1], - "frequency": [10, 40] - } - }, - { - "name": "lead_noise", - "params": { - "scale": [0.05, 0.2] - } - }, - { - "name": "nstdb", - "params": { - "noise_level": [0.2, 0.4] - } - } - ] + } } diff --git a/configs/den-tcn-sm.json b/configs/den-tcn-sm.json index 834dad8b..0afe83b7 100644 --- a/configs/den-tcn-sm.json +++ b/configs/den-tcn-sm.json @@ -2,15 +2,16 @@ "name": "den-tcn-sm", "project": "hk-denoise", "job_dir": "./results/den-tcn-sm", + "verbose": 2, + "dataset_weights": [0.9, 0.1], "datasets": [{ - "name": "synthetic", - "path": "./datasets/synthetic", + "name": "ecg-synthetic", "params": { - "num_pts": 10000, + "num_pts": 20000, "params": { "presets": ["SR", "AFIB", "ant_STEMI", "LAHB", "LPHB", "high_take_off", "LBBB", "random_morphology"], - "preset_weights": [8, 4, 1, 1, 1, 1, 1, 1], - "duration": 20, + "preset_weights": [24, 8, 1, 1, 1, 1, 1, 0], + "duration": 10, "sample_rate": 100, "heart_rate": [40, 160], "impedance": [1, 2], @@ -22,24 +23,20 @@ } }, { "name": "ptbxl", - "path": "./datasets/ptbxl", "params": { + "path": "./datasets/ptbxl" } }], - "num_classes": 1, - "class_map": {}, - "class_names": ["CLEAN"], "sampling_rate": 100, "frame_size": 256, - "model_file": "model.keras", - "samples_per_patient": 10, + "samples_per_patient": 5, "val_samples_per_patient": 10, - "val_patients": 0.20, - "val_size": 10000, "test_samples_per_patient": 10, - "test_size": 5000, - "batch_size": 128, - "buffer_size": 50000, + "val_patients": 0.20, + "val_size": 20000, + "test_size": 20000, + "batch_size": 256, + "buffer_size": 25000, "epochs": 150, "steps_per_epoch": 50, "val_metric": "loss", @@ -49,7 +46,7 @@ "tflm_file": "ecg_denoise_flatbuffer.h", "backend": "pc", "demo_size": 768, - "display_report": true, + "display_report": false, "quantization": { "qat": false, "mode": "FP32", @@ -57,6 +54,52 @@ "concrete": true, "debug": false }, + "preprocesses": [ + { + "name": "layer_norm", + "params": { + "epsilon": 0.01, + "name": "znorm" + } + } + ], + "augmentations": [{ + "name": "random_noise_distortion", + "params": { + "amplitude": [0, 0.5], + "frequency": [0.5, 1.5], + "name": "baseline_wander" + } + },{ + "name": "random_sine_wave", + "params": { + "amplitude": [0, 0.05], + "frequency": [45, 50], + "name": "powerline_noise" + } + },{ + "name": "amplitude_warp", + "params": { + "amplitude": [0.9, 1.1], + "frequency": [0.5, 1.5], + "name": "amplitude_warp" + } + }, { + "name": "random_noise", + "params": { + "factor": [0, 0.1], + "name": "random_noise" + } + }, { + "name": "random_background_noise", + "params": { + "amplitude": [0, 0.1], + "num_noises": 1, + "name": "nstdb" + } + }], + "model_file": "model.keras", + "use_logits": true, "architecture": { "name": "tcn", "params": { @@ -73,58 +116,5 @@ "use_logits": true, "model_name": "tcn" } - }, - "preprocesses": [ - { - "name": "znorm", - "params": { - "eps": 0.01, - "axis": null - } - } - ], - "augmentations": [ - { - "name": "baseline_wander", - "params": { - "amplitude": [0.0, 0.5], - "frequency": [0.5, 1.5] - } - }, - { - "name": "powerline_noise", - "params": { - "amplitude": [0.05, 0.15], - "frequency": [45, 50] - } - }, - { - "name": "burst_noise", - "params": { - "burst_number": [0, 4], - "amplitude": [0.05, 0.1], - "frequency": [20, 49] - } - }, - { - "name": "noise_sources", - "params": { - "num_sources": [1, 2], - "amplitude": [0.05, 0.1], - "frequency": [10, 40] - } - }, - { - "name": "lead_noise", - "params": { - "scale": [0.05, 0.2] - } - }, - { - "name": "nstdb", - "params": { - "noise_level": [0.2, 0.4] - } - } - ] + } } diff --git a/configs/download-datasets.json b/configs/download-datasets.json index 5760bb75..77bdef58 100644 --- a/configs/download-datasets.json +++ b/configs/download-datasets.json @@ -1,23 +1,29 @@ { - "ds_path": "./datasets", "datasets": [{ "name": "icentia11k", - "path": "./datasets/icentia11k" + "params": { + "path": "./datasets/icentia11k" + } }, { "name": "ludb", - "path": "./datasets/ludb" + "path": { + "path": "./datasets/ludb" + } }, { "name": "qtdb", - "path": "./datasets/qtdb" + "params": { + "path": "./datasets/qtdb" + } }, { "name": "ptbxl", - "path": "./datasets/ptbxl" + "params": { + "path": "./datasets/ptbxl" + } }, { "name": "lsad", - "path": "./datasets/lsad" - }, { - "name": "synthetic", - "path": "./datasets/synthetic" + "params": { + "path": "./datasets/lsad" + } }], "progress": true } diff --git a/configs/fnd-eff-lg.json b/configs/fnd-eff-lg.json index 1315db03..77d131cc 100644 --- a/configs/fnd-eff-lg.json +++ b/configs/fnd-eff-lg.json @@ -2,32 +2,32 @@ "name": "fnd-eff-lg", "project": "foundation", "job_dir": "./results/fnd-eff-lg", + "verbose": 2, "datasets": [{ "name": "lsad", - "path": "./datasets/lsad", "params": { + "path": "./datasets/lsad", + "leads": [0, 1, 2] } },{ "name": "ptbxl", - "path": "./datasets/ptbxl", "params": { + "path": "./datasets/ptbxl", + "leads": [0, 1, 2] } }], "num_classes": 128, - "temperature": 0.1, - "class_map": {}, - "class_names": ["FOUNDATION"], + "temperature": 1.0, "sampling_rate": 100, "frame_size": 800, - "model_file": "model.keras", "samples_per_patient": 1, - "val_file_dis": "./results/${task}-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", "val_samples_per_patient": 1, - "val_patients": 0.20, "test_samples_per_patient": 1, + "val_patients": 0.20, + "val_size": 10000, "test_size": 10000, - "batch_size": 2048, - "buffer_size": 30000, + "batch_size": 1024, + "buffer_size": 10000, "epochs": 200, "steps_per_epoch": 25, "val_metric": "loss", @@ -45,6 +45,60 @@ "concrete": true, "debug": false }, + "preprocesses": [ + { + "name": "layer_norm", + "params": { + "epsilon": 0.01, + "name": "znorm" + } + } + ], + "augmentations": [{ + "name": "random_noise_distortion", + "params": { + "amplitude": [0, 0.5], + "frequency": [0.5, 1.5], + "name": "baseline_wander" + } + },{ + "name": "random_sine_wave", + "params": { + "amplitude": [0, 0.05], + "frequency": [45, 50], + "auto_vectorize": false, + "name": "powerline_noise" + } + },{ + "name": "amplitude_warp", + "params": { + "amplitude": [0.9, 1.1], + "frequency": [0.5, 1.5], + "name": "amplitude_warp" + } + }, { + "name": "random_noise", + "params": { + "factor": [0, 0.05], + "name": "random_noise" + } + }, { + "name": "random_background_noise", + "params": { + "amplitude": [0, 0.05], + "num_noises": 1, + "name": "nstdb" + } + },{ + "name": "random_cutout", + "params": { + "cutouts": 2, + "factor": [0.005, 0.01], + "name": "cutout" + } + }], + "model_file": "model.keras", + "use_logits": true, "architecture": { "name": "efficientnetv2", "params": { @@ -62,71 +116,5 @@ "include_top": true, "norm": "layer" } - }, - "preprocesses": [ - { - "name": "filter", - "params": { - "lowcut": 1.0, - "highcut": 30, - "order": 3, - "forward_backward": true, - "axis": 0 - } - }, - { - "name": "znorm", - "params": { - "eps": 0.01, - "axis": null - } - } - ], - "augmentations": [ - { - "name": "baseline_wander", - "params": { - "amplitude": [0.0, 0.2], - "frequency": [0.5, 1.5] - } - }, - { - "name": "powerline_noise", - "params": { - "amplitude": [0.0, 0.1], - "frequency": [45, 50] - } - }, - { - "name": "burst_noise", - "params": { - "burst_number": [0, 4], - "amplitude": [0.0, 0.1], - "frequency": [20, 49] - } - }, - { - "name": "noise_sources", - "params": { - "num_sources": [1, 2], - "amplitude": [0.0, 0.1], - "frequency": [10, 40] - } - }, - { - "name": "lead_noise", - "params": { - "scale": [0.0, 0.1] - } - }, - { - "name": "cutout", - "params": { - "prob": [0.25, 0.50], - "amp": [0.05, 0.15], - "width": [0.05, 0.15], - "type": [0, 0] - } - } - ] + } } diff --git a/configs/fnd-eff-sm.json b/configs/fnd-eff-sm.json index d8d1d805..16ecd026 100644 --- a/configs/fnd-eff-sm.json +++ b/configs/fnd-eff-sm.json @@ -2,33 +2,33 @@ "name": "fnd-eff-sm", "project": "foundation", "job_dir": "./results/fnd-eff-sm", + "verbose": 2, "datasets": [{ "name": "lsad", - "path": "./datasets/lsad", "params": { + "path": "./datasets/lsad", + "leads": [0, 1, 2] } },{ "name": "ptbxl", - "path": "./datasets/ptbxl", "params": { + "path": "./datasets/ptbxl", + "leads": [0, 1, 2] } }], "num_classes": 128, - "temperature": 0.1, - "class_map": {}, - "class_names": ["FOUNDATION"], + "temperature": 1.0, "sampling_rate": 100, "frame_size": 800, - "model_file": "model.keras", "samples_per_patient": 1, - "val_file_dis": "./results/${task}-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", "val_samples_per_patient": 1, - "val_patients": 0.20, "test_samples_per_patient": 1, + "val_patients": 0.20, + "val_size": 10000, "test_size": 10000, - "batch_size": 2048, - "buffer_size": 30000, - "epochs": 200, + "batch_size": 1024, + "buffer_size": 10000, + "epochs": 150, "steps_per_epoch": 25, "val_metric": "loss", "lr_rate": 1e-3, @@ -37,7 +37,7 @@ "tflm_file": "ecg_foundation_flatbuffer.h", "backend": "pc", "demo_size": 800, - "display_report": true, + "display_report": false, "quantization": { "qat": false, "mode": "FP32", @@ -45,6 +45,60 @@ "concrete": true, "debug": false }, + "preprocesses": [ + { + "name": "layer_norm", + "params": { + "epsilon": 0.01, + "name": "znorm" + } + } + ], + "augmentations": [{ + "name": "random_noise_distortion", + "params": { + "amplitude": [0, 0.5], + "frequency": [0.5, 1.5], + "name": "baseline_wander" + } + },{ + "name": "random_sine_wave", + "params": { + "amplitude": [0, 0.05], + "frequency": [45, 50], + "auto_vectorize": false, + "name": "powerline_noise" + } + },{ + "name": "amplitude_warp", + "params": { + "amplitude": [0.9, 1.1], + "frequency": [0.5, 1.5], + "name": "amplitude_warp" + } + }, { + "name": "random_noise", + "params": { + "factor": [0, 0.05], + "name": "random_noise" + } + }, { + "name": "random_background_noise", + "params": { + "amplitude": [0, 0.05], + "num_noises": 1, + "name": "nstdb" + } + },{ + "name": "random_cutout", + "params": { + "cutouts": 2, + "factor": [0.005, 0.01], + "name": "cutout" + } + }], + "model_file": "model.keras", + "use_logits": true, "architecture": { "name": "efficientnetv2", "params": { @@ -52,9 +106,9 @@ "input_kernel_size": [1, 9], "input_strides": [1, 2], "blocks": [ - {"filters": 32, "depth": 2, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, - {"filters": 48, "depth": 2, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, - {"filters": 64, "depth": 2, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, + {"filters": 32, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, + {"filters": 48, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, + {"filters": 64, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, {"filters": 80, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, {"filters": 96, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"} ], @@ -62,71 +116,5 @@ "include_top": true, "norm": "layer" } - }, - "preprocesses": [ - { - "name": "filter", - "params": { - "lowcut": 1.0, - "highcut": 30, - "order": 3, - "forward_backward": true, - "axis": 0 - } - }, - { - "name": "znorm", - "params": { - "eps": 0.01, - "axis": null - } - } - ], - "augmentations": [ - { - "name": "baseline_wander", - "params": { - "amplitude": [0.0, 0.2], - "frequency": [0.5, 1.5] - } - }, - { - "name": "powerline_noise", - "params": { - "amplitude": [0.0, 0.1], - "frequency": [45, 50] - } - }, - { - "name": "burst_noise", - "params": { - "burst_number": [0, 4], - "amplitude": [0.0, 0.1], - "frequency": [20, 49] - } - }, - { - "name": "noise_sources", - "params": { - "num_sources": [1, 2], - "amplitude": [0.0, 0.1], - "frequency": [10, 40] - } - }, - { - "name": "lead_noise", - "params": { - "scale": [0.0, 0.1] - } - }, - { - "name": "cutout", - "params": { - "prob": [0.25, 0.50], - "amp": [0.05, 0.15], - "width": [0.05, 0.15], - "type": [0, 0] - } - } - ] + } } diff --git a/configs/ppg2ecg-tcn-sm.json b/configs/ppg2ecg-tcn-sm.json deleted file mode 100644 index 9ebc388f..00000000 --- a/configs/ppg2ecg-tcn-sm.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "name": "ppg2ecg-tcn-sm", - "project": "hk-transalte-ppg2ecg", - "job_dir": "./results/ppg2ecg-tcn-sm", - "datasets": [{ - "name": "bidmc", - "path": "./datasets/bidmc", - "params": { - } - }], - "num_classes": 1, - "class_map": {}, - "class_names": ["CLEAN"], - "sampling_rate": 100, - "frame_size": 800, - "model_file": "model.keras", - "samples_per_patient": 100, - "val_samples_per_patient": 100, - "val_patients": 0.20, - "val_size": 1000, - "test_samples_per_patient": 100, - "test_size": 1000, - "batch_size": 128, - "buffer_size": 3000, - "epochs": 100, - "steps_per_epoch": 25, - "val_metric": "loss", - "lr_rate": 1e-3, - "lr_cycles": 1, - "tflm_var_name": "ppg2ecg_translate_flatbuffer", - "tflm_file": "ppg2ecg_translate_flatbuffer.h", - "backend": "pc", - "demo_size": 1024, - "display_report": true, - "quantization": { - "qat": false, - "mode": "FP32", - "io_type": "float32", - "concrete": true, - "debug": false - }, - "architecture": { - "name": "efficientnetv2", - "params": { - "input_filters": 24, - "input_kernel_size": [1, 9], - "input_strides": [1, 2], - "blocks": [ - {"filters": 32, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, - {"filters": 48, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, - {"filters": 64, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, - {"filters": 80, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, - {"filters": 96, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 4, "norm": "layer"} - ], - "output_filters": 128, - "include_top": true, - "norm": "layer" - } - }, - "architecture-dis": { - "name": "tcn", - "params": { - "input_kernel": [1, 9], - "input_norm": "batch", - "blocks": [ - {"depth": 1, "branch": 1, "filters": 24, "kernel": [1, 9], "dilation": [1, 1], "dropout": 0, "ex_ratio": 1, "se_ratio": 0, "norm": "layer"}, - {"depth": 1, "branch": 1, "filters": 32, "kernel": [1, 9], "dilation": [1, 2], "dropout": 0, "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, - {"depth": 1, "branch": 1, "filters": 48, "kernel": [1, 9], "dilation": [1, 4], "dropout": 0, "ex_ratio": 1, "se_ratio": 4, "norm": "layer"}, - {"depth": 1, "branch": 1, "filters": 64, "kernel": [1, 9], "dilation": [1, 8], "dropout": 0, "ex_ratio": 1, "se_ratio": 4, "norm": "layer"} - ], - "output_kernel": [1, 9], - "include_top": true, - "use_logits": true, - "model_name": "tcn" - } - }, - "preprocesses": [ - { - "name": "filter", - "params": { - "lowcut": 1.0, - "highcut": 30, - "order": 3, - "forward_backward": true, - "axis": 0 - } - }, - { - "name": "znorm", - "params": { - "eps": 0.01, - "axis": null - } - } - ], - "augmentations-dis": [ - { - "name": "baseline_wander", - "params": { - "amplitude": [0.0, 1.0], - "frequency": [0.5, 1.5] - } - }, - { - "name": "powerline_noise", - "params": { - "amplitude": [0.05, 0.15], - "frequency": [45, 50] - } - }, - { - "name": "burst_noise", - "params": { - "burst_number": [0, 4], - "amplitude": [0.05, 0.15], - "frequency": [20, 49] - } - }, - { - "name": "noise_sources", - "params": { - "num_sources": [1, 2], - "amplitude": [0.05, 0.20], - "frequency": [10, 40] - } - }, - { - "name": "lead_noise", - "params": { - "scale": [0.05, 0.15] - } - }, - { - "name": "nstdb", - "params": { - "noise_level": [0.05, 0.15] - } - } - ] -} diff --git a/configs/seg-2-tcn-sm.json b/configs/seg-2-tcn-sm.json index 837443d2..9884677d 100644 --- a/configs/seg-2-tcn-sm.json +++ b/configs/seg-2-tcn-sm.json @@ -2,14 +2,18 @@ "name": "seg-2-tcn-sm", "project": "hk-segmentation-2", "job_dir": "./results/seg-2-tcn-sm", + "verbose": 2, + "dataset_weights": [0.32, 0.68], "datasets": [{ "name": "icentia11k", - "path": "./datasets/icentia11k", - "params": {} - }, { + "params": { + "path": "./datasets/icentia11k" + } + },{ "name": "ptbxl", - "path": "./datasets/ptbxl", - "params": {} + "params": { + "path": "./datasets/ptbxl" + } }], "num_classes": 2, "class_map": { @@ -22,28 +26,28 @@ "class_names": [ "NONE", "QRS" ], + "class_weights": "balanced", "sampling_rate": 100, "frame_size": 256, - "model_file": "model.keras", - "samples_per_patient": 10, - "val_file": "./results/${task}-class-2-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", - "test_file": "./results/${task}-class-2-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", - "val_samples_per_patient": 10, + "samples_per_patient": 5, + "val_samples_per_patient": 5, + "test_samples_per_patient": 5, "val_patients": 0.20, - "test_samples_per_patient": 10, - "test_size": 50000, + "val_size": 25000, + "test_size": 25000, "batch_size": 256, - "buffer_size": 100000, - "epochs": 125, - "steps_per_epoch": 50, + "buffer_size": 50000, + "epochs": 100, + "steps_per_epoch": 100, "val_metric": "loss", "lr_rate": 1e-3, "lr_cycles": 1, - "val_acc_threshold": 0.98, + "val_metric_threshold": 0.98, "tflm_var_name": "g_segmentation_model", "tflm_file": "segmentation_model_buffer.h", "backend": "pc", "demo_size": 900, + "display_report": false, "quantization": { "qat": false, "mode": "INT8", @@ -51,6 +55,17 @@ "concrete": true, "debug": false }, + "preprocesses": [ + { + "name": "layer_norm", + "params": { + "epsilon": 0.01, + "name": "znorm" + } + } + ], + "model_file": "model.keras", + "use_logits": true, "architecture": { "name": "tcn", "params": { @@ -68,24 +83,5 @@ "use_logits": true, "model_name": "tcn" } - }, - "preprocesses": [ - { - "name": "filter", - "params": { - "lowcut": 1.0, - "highcut": 30, - "order": 3, - "forward_backward": true, - "axis": 0 - } - }, - { - "name": "znorm", - "params": { - "eps": 0.01, - "axis": null - } - } - ] + } } diff --git a/configs/seg-4-tcn-lg.json b/configs/seg-4-tcn-lg.json index 1092b321..85bf6599 100644 --- a/configs/seg-4-tcn-lg.json +++ b/configs/seg-4-tcn-lg.json @@ -2,30 +2,30 @@ "name": "seg-4-tcn-lg", "project": "hk-segmentation-4", "job_dir": "./results/seg-4-tcn-lg", + "verbose": 2, + "dataset_weights": [0.20, 0.80], "datasets": [{ "name": "ludb", - "path": "./datasets/ludb", - "params": {}, - "weight": 0.10 + "params": { + "path": "./datasets/ludb" + } }, { - "name": "synthetic", - "path": "./datasets/synthetic", + "name": "ecg-synthetic", "params": { "num_pts": 10000, "params": { "presets": ["SR", "AFIB", "ant_STEMI", "LAHB", "LPHB", "high_take_off", "LBBB", "random_morphology"], - "preset_weights": [8, 4, 1, 1, 1, 1, 1, 1], - "duration": 20, + "preset_weights": [24, 8, 1, 1, 1, 1, 1, 0], + "duration": 10, "sample_rate": 100, "heart_rate": [40, 160], "impedance": [1, 2], "p_multiplier": [0.8, 1.2], "t_multiplier": [0.8, 1.2], - "noise_multiplier": [0.05, 0.15], + "noise_multiplier": [0.05, 0.1], "voltage_factor": [800, 1000] } - }, - "weight": 0.90 + } }], "num_classes": 4, "class_map": { @@ -39,30 +39,28 @@ "class_names": [ "NONE", "P-WAVE", "QRS", "T-WAVE" ], + "class_weights": "balanced", "sampling_rate": 100, "frame_size": 256, - "model_file": "model.keras", - "samples_per_patient": 25, - "val_file": "./results/${task}-class-4-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", - "test_file": "./results/${task}-class-4-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", - "val_samples_per_patient": 25, + "samples_per_patient": 5, + "val_samples_per_patient": 10, + "test_samples_per_patient": 10, "val_patients": 0.10, - "test_samples_per_patient": 25, - "test_size": 25000, - "batch_size": 128, - "buffer_size": 50000, - "epochs": 125, + "val_size": 20000, + "test_size": 20000, + "batch_size": 256, + "buffer_size": 25000, + "epochs": 150, "steps_per_epoch": 50, "val_metric": "loss", "lr_rate": 1e-3, "lr_cycles": 1, - "class_weights": "balanced", - "val_acc_threshold": 0.98, + "val_metric_threshold": 0.98, "tflm_var_name": "ecg_segmentation_flatbuffer", "tflm_file": "ecg_segmentation_flatbuffer.h", - "use_logits": false, "backend": "pc", "demo_size": 900, + "display_report": false, "quantization": { "qat": false, "mode": "INT8", @@ -70,6 +68,53 @@ "concrete": true, "debug": false }, + "preprocesses": [ + { + "name": "layer_norm", + "params": { + "epsilon": 0.01, + "name": "znorm" + } + } + ], + "augmentations": [{ + "name": "random_noise_distortion", + "params": { + "amplitude": [0, 0.5], + "frequency": [0.5, 1.5], + "name": "baseline_wander" + } + },{ + "name": "random_sine_wave", + "params": { + "amplitude": [0, 0.05], + "frequency": [45, 50], + "auto_vectorize": false, + "name": "powerline_noise" + } + },{ + "name": "amplitude_warp", + "params": { + "amplitude": [0.9, 1.1], + "frequency": [0.5, 1.5], + "name": "amplitude_warp" + } + }, { + "name": "random_noise", + "params": { + "factor": [0, 0.025], + "name": "random_noise" + } + }, { + "name": "random_background_noise", + "params": { + "amplitude": [0, 0.025], + "num_noises": 1, + "name": "nstdb" + } + }], + "model_file": "model.keras", + "use_logits": false, "architecture": { "name": "tcn", "params": { @@ -86,69 +131,5 @@ "use_logits": true, "model_name": "tcn" } - }, - "preprocesses": [ - { - "name": "filter", - "params": { - "lowcut": 1.0, - "highcut": 30, - "order": 3, - "forward_backward": true, - "axis": 0 - } - }, - { - "name": "znorm", - "params": { - "eps": 0.01, - "axis": null - } - } - ], - "augmentations": [ - { - "name": "baseline_wander", - "params": { - "amplitude": [0.0, 0.5], - "frequency": [0.5, 1.5] - } - }, - { - "name": "motion_noise", - "params": { - "amplitude": [0.0, 0.5], - "frequency": [1.0, 2.0] - } - }, - { - "name": "powerline_noise", - "params": { - "amplitude": [0.0, 0.15], - "frequency": [45, 50] - } - }, - { - "name": "burst_noise", - "params": { - "burst_number": [0, 4], - "amplitude": [0.0, 0.15], - "frequency": [20, 49] - } - }, - { - "name": "noise_sources", - "params": { - "num_sources": [1, 2], - "amplitude": [0.0, 0.15], - "frequency": [10, 40] - } - }, - { - "name": "lead_noise", - "params": { - "scale": [0.0, 0.15] - } - } - ] + } } diff --git a/configs/seg-4-tcn-sm.json b/configs/seg-4-tcn-sm.json index a24ed4a3..4d371aa7 100644 --- a/configs/seg-4-tcn-sm.json +++ b/configs/seg-4-tcn-sm.json @@ -2,30 +2,30 @@ "name": "seg-4-tcn-sm", "project": "hk-segmentation-4", "job_dir": "./results/seg-4-tcn-sm", + "verbose": 2, + "dataset_weights": [0.20, 0.80], "datasets": [{ "name": "ludb", - "path": "./datasets/ludb", - "params": {}, - "weight": 0.10 + "params": { + "path": "./datasets/ludb" + } }, { - "name": "synthetic", - "path": "./datasets/synthetic", + "name": "ecg-synthetic", "params": { "num_pts": 10000, "params": { "presets": ["SR", "AFIB", "ant_STEMI", "LAHB", "LPHB", "high_take_off", "LBBB", "random_morphology"], - "preset_weights": [8, 4, 1, 1, 1, 1, 1, 1], - "duration": 20, + "preset_weights": [24, 8, 1, 1, 1, 1, 1, 0], + "duration": 10, "sample_rate": 100, "heart_rate": [40, 160], "impedance": [1, 2], "p_multiplier": [0.8, 1.2], "t_multiplier": [0.8, 1.2], - "noise_multiplier": [0.05, 0.15], + "noise_multiplier": [0.05, 0.1], "voltage_factor": [800, 1000] } - }, - "weight": 0.90 + } }], "num_classes": 4, "class_map": { @@ -39,29 +39,28 @@ "class_names": [ "NONE", "P-WAVE", "QRS", "T-WAVE" ], + "class_weights": "balanced", "sampling_rate": 100, "frame_size": 256, - "model_file": "model.keras", - "samples_per_patient": 25, - "val_file": "./results/${task}-class-4-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", - "test_file": "./results/${task}-class-4-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", - "val_samples_per_patient": 25, + "samples_per_patient": 5, + "val_samples_per_patient": 10, + "test_samples_per_patient": 10, "val_patients": 0.10, - "test_samples_per_patient": 25, - "test_size": 25000, - "batch_size": 128, - "buffer_size": 50000, - "epochs": 125, + "val_size": 20000, + "test_size": 20000, + "batch_size": 256, + "buffer_size": 25000, + "epochs": 150, "steps_per_epoch": 50, "val_metric": "loss", "lr_rate": 1e-3, "lr_cycles": 1, - "val_acc_threshold": 0.98, + "val_metric_threshold": 0.98, "tflm_var_name": "ecg_segmentation_flatbuffer", "tflm_file": "ecg_segmentation_flatbuffer.h", - "use_logits": false, "backend": "pc", "demo_size": 900, + "display_report": false, "quantization": { "qat": false, "mode": "INT8", @@ -69,6 +68,53 @@ "concrete": true, "debug": false }, + "preprocesses": [ + { + "name": "layer_norm", + "params": { + "epsilon": 0.01, + "name": "znorm" + } + } + ], + "augmentations": [{ + "name": "random_noise_distortion", + "params": { + "amplitude": [0, 0.5], + "frequency": [0.5, 1.5], + "name": "baseline_wander" + } + },{ + "name": "random_sine_wave", + "params": { + "amplitude": [0, 0.05], + "frequency": [45, 50], + "auto_vectorize": false, + "name": "powerline_noise" + } + },{ + "name": "amplitude_warp", + "params": { + "amplitude": [0.9, 1.1], + "frequency": [0.5, 1.5], + "name": "amplitude_warp" + } + }, { + "name": "random_noise", + "params": { + "factor": [0, 0.025], + "name": "random_noise" + } + }, { + "name": "random_background_noise", + "params": { + "amplitude": [0, 0.025], + "num_noises": 1, + "name": "nstdb" + } + }], + "model_file": "model.keras", + "use_logits": false, "architecture": { "name": "tcn", "params": { @@ -85,75 +131,5 @@ "use_logits": true, "model_name": "tcn" } - }, - "preprocesses": [ - { - "name": "filter", - "params": { - "lowcut": 1.0, - "highcut": 30, - "order": 3, - "forward_backward": true, - "axis": 0 - } - }, - { - "name": "znorm", - "params": { - "eps": 0.01, - "axis": null - } - } - ], - "augmentations": [ - { - "name": "baseline_wander", - "params": { - "amplitude": [0.0, 0.5], - "frequency": [0.5, 1.5] - } - }, - { - "name": "motion_noise", - "params": { - "amplitude": [0.0, 0.5], - "frequency": [1.0, 2.0] - } - }, - { - "name": "powerline_noise", - "params": { - "amplitude": [0.0, 0.15], - "frequency": [45, 50] - } - }, - { - "name": "burst_noise", - "params": { - "burst_number": [0, 4], - "amplitude": [0.0, 0.15], - "frequency": [20, 49] - } - }, - { - "name": "noise_sources", - "params": { - "num_sources": [0, 4], - "amplitude": [0.0, 0.15], - "frequency": [10, 40] - } - }, - { - "name": "lead_noise", - "params": { - "scale": [0.0, 0.15] - } - }, - { - "name": "nstdb", - "params": { - "noise_level": [0.0, 0.15] - } - } - ] + } } diff --git a/configs/seg-ppg-2-tcn-sm.json b/configs/seg-ppg-2-tcn-sm.json index 6c13576e..f1c20359 100644 --- a/configs/seg-ppg-2-tcn-sm.json +++ b/configs/seg-ppg-2-tcn-sm.json @@ -2,9 +2,10 @@ "name": "seg-ppg-2-tcn-sm", "project": "hk-segmentation-2", "job_dir": "./results/seg-ppg-2-tcn-sm", + "verbose": 2, + "signal_type": "PPG", "datasets": [{ - "name": "syntheticppg", - "path": "./datasets/syntheticppg", + "name": "ppg-synthetic", "params": { "num_pts": 20000, "params": { @@ -13,7 +14,7 @@ "heart_rate": [40, 160], "frequency_modulation": [0.1, 0.4], "ibi_randomness": [0.05, 0.15], - "noise_multiplier": [0.05, 0.15] + "noise_multiplier": [0.0, 0.01] } } }], @@ -25,29 +26,27 @@ "class_names": [ "SYS", "DIA" ], - "signal_type": "PPG", "sampling_rate": 100, "frame_size": 256, - "model_file": "model.keras", "samples_per_patient": 5, - "val_file": "./results/${task}-ppg-class-2-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", - "test_file": "./results/${task}-ppg-class-2-${dataset}-${sampling_rate}fs-${frame_size}sz.pkl", - "val_samples_per_patient": 5, + "val_samples_per_patient": 10, + "test_samples_per_patient": 10, "val_patients": 0.20, - "test_samples_per_patient": 5, + "val_size": 20000, "test_size": 20000, + "buffer_size": 25000, "batch_size": 256, - "buffer_size": 50000, - "epochs": 125, + "epochs": 200, "steps_per_epoch": 50, "val_metric": "loss", "lr_rate": 1e-3, "lr_cycles": 1, - "val_acc_threshold": 0.98, + "val_metric_threshold": 0.98, "tflm_var_name": "g_segmentation_model", "tflm_file": "segmentation_model_buffer.h", "backend": "pc", "demo_size": 900, + "display_report": false, "quantization": { "qat": false, "mode": "INT8", @@ -55,92 +54,67 @@ "concrete": true, "debug": false }, + "preprocesses": [ + { + "name": "layer_norm", + "params": { + "epsilon": 0.001, + "name": "znorm" + } + } + ], + "augmentations": [{ + "name": "random_noise_distortion", + "params": { + "amplitude": [0, 1.0], + "frequency": [0.5, 1.5], + "name": "baseline_wander" + } + },{ + "name": "random_sine_wave", + "params": { + "amplitude": [0, 0.2], + "frequency": [45, 50], + "name": "powerline_noise" + } + },{ + "name": "amplitude_warp", + "params": { + "amplitude": [0.8, 1.2], + "frequency": [0.5, 1.5], + "name": "amplitude_warp" + } + }, { + "name": "random_noise", + "params": { + "factor": [0, 0.2], + "name": "random_noise" + } + }, { + "name": "random_background_noise", + "params": { + "amplitude": [0, 0.2], + "num_noises": 1, + "name": "nstdb" + } + }], + "model_file": "model.keras", "architecture": { "name": "tcn", "params": { - "input_kernel": [1, 7], + "input_kernel": [1, 9], "input_norm": "batch", "blocks": [ - {"depth": 1, "branch": 1, "filters": 8, "kernel": [1, 7], "dilation": [1, 1], "dropout": 0, "ex_ratio": 1, "se_ratio": 0, "norm": "batch"}, - {"depth": 1, "branch": 1, "filters": 12, "kernel": [1, 7], "dilation": [1, 1], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, - {"depth": 1, "branch": 1, "filters": 16, "kernel": [1, 7], "dilation": [1, 2], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, - {"depth": 1, "branch": 1, "filters": 24, "kernel": [1, 7], "dilation": [1, 4], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, - {"depth": 1, "branch": 1, "filters": 32, "kernel": [1, 7], "dilation": [1, 8], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"} + {"depth": 1, "branch": 1, "filters": 8, "kernel": [1, 9], "dilation": [1, 1], "dropout": 0, "ex_ratio": 1, "se_ratio": 0, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 12, "kernel": [1, 9], "dilation": [1, 1], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 16, "kernel": [1, 9], "dilation": [1, 2], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 24, "kernel": [1, 9], "dilation": [1, 4], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 32, "kernel": [1, 9], "dilation": [1, 8], "dropout": 0, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"} ], - "output_kernel": [1, 7], + "output_kernel": [1, 9], "include_top": true, "use_logits": true, "model_name": "tcn" } - }, - "preprocesses": [ - { - "name": "filter", - "params": { - "lowcut": 0.5, - "highcut": 10, - "order": 3, - "forward_backward": true, - "axis": 0 - } - }, - { - "name": "znorm", - "params": { - "eps": 0.001, - "axis": null - } - } - ], - "augmentations": [ - { - "name": "baseline_wander", - "params": { - "amplitude": [0.0, 1.0], - "frequency": [0.5, 1.5] - } - }, - { - "name": "motion_noise", - "params": { - "amplitude": [0.0, 0.5], - "frequency": [1.0, 2.0] - } - }, - { - "name": "powerline_noise", - "params": { - "amplitude": [0.0, 0.15], - "frequency": [45, 50] - } - }, - { - "name": "burst_noise", - "params": { - "burst_number": [0, 4], - "amplitude": [0.0, 0.15], - "frequency": [20, 49] - } - }, - { - "name": "noise_sources", - "params": { - "num_sources": [0, 4], - "amplitude": [0.0, 0.15], - "frequency": [10, 40] - } - }, - { - "name": "lead_noise", - "params": { - "scale": [0.0, 0.15] - } - }, - { - "name": "nstdb", - "params": { - "noise_level": [0.0, 0.15] - } - } - ] + } } diff --git a/docs/api/datasets.md b/docs/api/datasets.md deleted file mode 100644 index 22fc1056..00000000 --- a/docs/api/datasets.md +++ /dev/null @@ -1,35 +0,0 @@ -# Datasets - -See [Datasets](../datasets/index.md) for information about available datasets. - -::: heartkit.datasets.augmentation - -::: heartkit.datasets.bidmc - -::: heartkit.datasets.dataloader - -::: heartkit.datasets.dataset - -::: heartkit.datasets.defines - -::: heartkit.datasets.download - -::: heartkit.datasets.icentia11k - -::: heartkit.datasets.lsad - -::: heartkit.datasets.ludb - -::: heartkit.datasets.nstdb - -::: heartkit.datasets.preprocessing - -::: heartkit.datasets.ptbxl - -::: heartkit.datasets.qtdb - -::: heartkit.datasets.synthetic - -::: heartkit.datasets.syntheticppg - -::: heartkit.datasets.utils diff --git a/docs/api/datasets/augmentation.md b/docs/api/datasets/augmentation.md new file mode 100644 index 00000000..c0da118d --- /dev/null +++ b/docs/api/datasets/augmentation.md @@ -0,0 +1,9 @@ +# Augmentation API + +## hk.datasets.augmentation.create_augmentation_layer + +::: heartkit.datasets.augmentation.create_augmentation_layer + +## hk.datasets.augmentation.create_augmentation_pipeline + +::: heartkit.datasets.augmentation.create_augmentation_pipeline diff --git a/docs/api/datasets/dataloader.md b/docs/api/datasets/dataloader.md new file mode 100644 index 00000000..122448b3 --- /dev/null +++ b/docs/api/datasets/dataloader.md @@ -0,0 +1,5 @@ +# Dataloader API + +## hk.datasets.HKDataloader + +::: heartkit.datasets.dataloader.HKDataloader diff --git a/docs/api/datasets/dataset.md b/docs/api/datasets/dataset.md new file mode 100644 index 00000000..5b59eed0 --- /dev/null +++ b/docs/api/datasets/dataset.md @@ -0,0 +1,5 @@ +# Dataset API + +## hk.dataset.HKDataset + +::: heartkit.datasets.dataset.HKDataset diff --git a/docs/api/datasets/factory.md b/docs/api/datasets/factory.md new file mode 100644 index 00000000..2c4ff36a --- /dev/null +++ b/docs/api/datasets/factory.md @@ -0,0 +1,16 @@ +# Dataset Factory + +See [Datasets](../../datasets/index.md) for information about available datasets. + +To list all available dataset names and their corresponding classes: + +## hk.DatasetFactory + +```python +import heartkit as hk + +for dataset in hk.DatasetFactory.list(): + print(f"Dataset name: {dataset} - {hk.DatasetFactory.get(dataset)}") +``` + +::: neuralspot_edge.utils.ItemFactory diff --git a/docs/api/datasets/icentia11k.md b/docs/api/datasets/icentia11k.md new file mode 100644 index 00000000..76ba109c --- /dev/null +++ b/docs/api/datasets/icentia11k.md @@ -0,0 +1,13 @@ +# Icentia11K Dataset API + +## hk.datasets.icentia11k.IcentiaRhythm + +::: heartkit.datasets.icentia11k.IcentiaRhythm + +## hk.datasets.icentia11k.IcentiaBeat + +::: heartkit.datasets.icentia11k.IcentiaBeat + +## hk.datasets.icentia11k.IcentiaDataset + +::: heartkit.datasets.icentia11k.IcentiaDataset diff --git a/docs/api/datasets/lsad.md b/docs/api/datasets/lsad.md new file mode 100644 index 00000000..573bcbe9 --- /dev/null +++ b/docs/api/datasets/lsad.md @@ -0,0 +1,10 @@ +# LSAD Dataset API + +## hk.datasets.lsad.LsadScpCode + +::: heartkit.datasets.lsad.LsadScpCode + + +## hk.datasets.lsad.LsadDataset + +::: heartkit.datasets.lsad.LsadDataset diff --git a/docs/api/datasets/ludb.md b/docs/api/datasets/ludb.md new file mode 100644 index 00000000..d36df734 --- /dev/null +++ b/docs/api/datasets/ludb.md @@ -0,0 +1,5 @@ +# LUDB Dataset API + +## hk.datasets.ludb.LudbDataset + +::: heartkit.datasets.ludb.LudbDataset diff --git a/docs/api/datasets/ptbxl.md b/docs/api/datasets/ptbxl.md new file mode 100644 index 00000000..853d2337 --- /dev/null +++ b/docs/api/datasets/ptbxl.md @@ -0,0 +1,5 @@ +# PTB-XL Dataset API + +## hk.datasets.ptbxl.PtbxlDataset + +::: heartkit.datasets.ptbxl.PtbxlDataset diff --git a/docs/api/datasets/qtdb.md b/docs/api/datasets/qtdb.md new file mode 100644 index 00000000..7a264c26 --- /dev/null +++ b/docs/api/datasets/qtdb.md @@ -0,0 +1,5 @@ +# QTDB Dataset API + +## hk.datasets.qtdb.QtdbDataset + +::: heartkit.datasets.qtdb.QtdbDataset diff --git a/docs/api/datasets/synthetic.md b/docs/api/datasets/synthetic.md new file mode 100644 index 00000000..f367ae1f --- /dev/null +++ b/docs/api/datasets/synthetic.md @@ -0,0 +1,22 @@ +# Synthetic Datasets + +## ECG Synthetic + +### hk.datasets.ecg_synthetic.EcgSyntheticParams + +::: heartkit.datasets.ecg_synthetic.EcgSyntheticParams + +### hk.datasets.ecg_synthetic.EcgSyntheticDataset + +::: heartkit.datasets.ecg_synthetic.EcgSyntheticDataset + + +## PPG Synthetic + +### hk.datasets.ppg_synthetic.PpgSyntheticParams + +::: heartkit.datasets.ppg_synthetic.PpgSyntheticParams + +### hk.datasets.ppg_synthetic.PpgSyntheticDataset + +::: heartkit.datasets.ppg_synthetic.PpgSyntheticDataset diff --git a/docs/api/heartkit.md b/docs/api/heartkit.md index d74c39ae..07c31ee7 100644 --- a/docs/api/heartkit.md +++ b/docs/api/heartkit.md @@ -4,6 +4,6 @@ ::: heartkit.defines -::: heartkit.metrics - ::: heartkit.utils + +::: heartkit.utils.plotting diff --git a/heartkit/tasks/defines.py b/docs/api/index.md similarity index 100% rename from heartkit/tasks/defines.py rename to docs/api/index.md diff --git a/docs/api/models.md b/docs/api/models.md deleted file mode 100644 index 039c5033..00000000 --- a/docs/api/models.md +++ /dev/null @@ -1,5 +0,0 @@ -# Models - -A number of custom model architectures are provided in the `heartkit.models` module. These models are designed to be used with the `heartkit` package, but can be used independently as well. See [Models](../models/index.md) for information about available models. - -::: heartkit.models diff --git a/docs/api/models/factory.md b/docs/api/models/factory.md new file mode 100644 index 00000000..964af9ae --- /dev/null +++ b/docs/api/models/factory.md @@ -0,0 +1,12 @@ +# ModelFactory API + +See [Models](../../models/index.md) for information about available models. + +## hk.ModelFactory + +```python +import heartkit as hk + +for model in hk.ModelFactory.list(): + print(f"Model name: {model} - {hk.ModelFactory.get(model)}") +``` diff --git a/docs/api/models/model.md b/docs/api/models/model.md new file mode 100644 index 00000000..10c0ad9f --- /dev/null +++ b/docs/api/models/model.md @@ -0,0 +1,9 @@ +# Model API + +HeartKit leverages [neuralspot-edge](https://ambiqai.github.io/neuralspot-edge/) for customizable model architectures. Currently, the models are built using Keras functional model API to allow the most flexibilty in creating custom network topologies. Instead of registering custom `keras.Model` objects, the factory provides a callable that takes a `keras.Input`, model parameters, and number of classes as arguments and returns a `keras.Model`. + +## hk.models.ModelFactoryItem + +::: heartkit.models.ModelFactoryItem + +See [Models](../../models/index.md) for information about available models. diff --git a/docs/api/tasks.md b/docs/api/tasks.md deleted file mode 100644 index 02ce4ab9..00000000 --- a/docs/api/tasks.md +++ /dev/null @@ -1,9 +0,0 @@ -# HeartKit: Tasks - -::: heartkit.tasks.rhythm - -::: heartkit.tasks.beat - -::: heartkit.tasks.denoise - -::: heartkit.tasks.segmentation diff --git a/docs/api/tasks/beat.md b/docs/api/tasks/beat.md new file mode 100644 index 00000000..c11d7676 --- /dev/null +++ b/docs/api/tasks/beat.md @@ -0,0 +1,25 @@ +# Beat Task API + +## hk.tasks.beat.dataloaders + +### hk.tasks.beat.dataloaders.Icentia11kDataloader + +::: heartkit.tasks.beat.dataloaders.icentia11k.Icentia11kDataloader + +## hk.tasks.BeatTask + +### hk.tasks.BeatTask.train + +::: heartkit.tasks.beat.train.train + +### hk.tasks.BeatTask.evaluate + +::: heartkit.tasks.beat.evaluate.evaluate + +### hk.tasks.BeatTask.export + +::: heartkit.tasks.beat.export.export + +### hk.tasks.BeatTask.train + +::: heartkit.tasks.beat.demo.demo diff --git a/docs/api/tasks/denoise.md b/docs/api/tasks/denoise.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/api/tasks/factory.md b/docs/api/tasks/factory.md new file mode 100644 index 00000000..4d3ed6fd --- /dev/null +++ b/docs/api/tasks/factory.md @@ -0,0 +1,14 @@ +# TaskFactory API + +See [Tasks](../../tasks/index.md) for information about available tasks. + +## hk.TaskFactory + +```python +import heartkit as hk + +for model in hk.TaskFactory.list(): + print(f"Task name: {model} - {hk.TaskFactory.get(model)}") +``` + +::: neuralspot_edge.utils.ItemFactory diff --git a/docs/api/tasks/foundation.md b/docs/api/tasks/foundation.md new file mode 100644 index 00000000..bbe3320a --- /dev/null +++ b/docs/api/tasks/foundation.md @@ -0,0 +1,30 @@ +# Foundation Task API + +## hk.tasks.foundation.dataloaders + +### hk.tasks.foundation.dataloaders.LsadDataloader + +::: heartkit.tasks.foundation.dataloaders.lsad.LsadDataloader + +### hk.tasks.foundation.dataloaders.PtbxlDataloader + +::: heartkit.tasks.foundation.dataloaders.ptbxl.PtbxlDataloader + + +## hk.tasks.FoundationTask + +### hk.tasks.FoundationTask.train + +::: heartkit.tasks.foundation.train.train + +### hk.tasks.FoundationTask.evaluate + +::: heartkit.tasks.foundation.evaluate.evaluate + +### hk.tasks.FoundationTask.export + +::: heartkit.tasks.foundation.export.export + +### hk.tasks.FoundationTask.train + +::: heartkit.tasks.foundation.demo.demo diff --git a/docs/api/tasks/rhythm.md b/docs/api/tasks/rhythm.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/api/tasks/segmentation.md b/docs/api/tasks/segmentation.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/api/tasks/task.md b/docs/api/tasks/task.md new file mode 100644 index 00000000..93404682 --- /dev/null +++ b/docs/api/tasks/task.md @@ -0,0 +1,10 @@ +# Task API + +## hk.HKTask + +::: heartkit.tasks.task.HKTask + + +## hk.HKTaskParams + +::: heartkit.defines.HKTaskParams diff --git a/docs/assets/modes/python-download-snippet.md b/docs/assets/modes/python-download-snippet.md index 33d39d1a..111d40ae 100644 --- a/docs/assets/modes/python-download-snippet.md +++ b/docs/assets/modes/python-download-snippet.md @@ -4,7 +4,7 @@ import heartkit as hk hk.datasets.download_datasets(hk.HKDownloadParams( ds_path=Path("./datasets"), - datasets=["icentia11k", "ludb", "qtdb", "synthetic"], + datasets=["icentia11k", "ludb", "qtdb", "ecg-synthetic"], progress=True )) ``` diff --git a/docs/assets/usage/json-configuration.md b/docs/assets/usage/json-configuration.md new file mode 100644 index 00000000..b256c53c --- /dev/null +++ b/docs/assets/usage/json-configuration.md @@ -0,0 +1,152 @@ +```javascript +{ + "name": "arr-2-eff-sm", + "project": "hk-rhythm-2", + "job_dir": "./results/arr-2-eff-sm", + "verbose": 2, + "datasets": [ + { + "name": "ptbxl", + "params": { + "path": "./datasets/ptbxl" + } + } + ], + "num_classes": 2, + "class_map": { + "0": 0, + "7": 1, + "8": 1 + }, + "class_names": [ + "NORMAL", + "AFIB/AFL" + ], + "class_weights": "balanced", + "sampling_rate": 100, + "frame_size": 512, + "samples_per_patient": [ + 10, + 10 + ], + "val_samples_per_patient": [ + 5, + 5 + ], + "test_samples_per_patient": [ + 5, + 5 + ], + "val_patients": 0.2, + "val_size": 20000, + "test_size": 20000, + "batch_size": 256, + "buffer_size": 20000, + "epochs": 100, + "steps_per_epoch": 50, + "val_metric": "loss", + "lr_rate": 0.001, + "lr_cycles": 1, + "threshold": 0.75, + "val_metric_threshold": 0.98, + "tflm_var_name": "g_rhythm_model", + "tflm_file": "rhythm_model_buffer.h", + "backend": "pc", + "demo_size": 896, + "display_report": true, + "quantization": { + "qat": false, + "format": "INT8", + "io_type": "int8", + "conversion": "CONCRETE", + "debug": false + }, + "preprocesses": [ + { + "name": "layer_norm", + "params": { + "epsilon": 0.01, + "name": "znorm" + } + } + ], + "augmentations": [], + "model_file": "model.keras", + "use_logits": false, + "architecture": { + "name": "efficientnetv2", + "params": { + "input_filters": 16, + "input_kernel_size": [ + 1, + 9 + ], + "input_strides": [ + 1, + 2 + ], + "blocks": [ + { + "filters": 24, + "depth": 2, + "kernel_size": [ + 1, + 9 + ], + "strides": [ + 1, + 2 + ], + "ex_ratio": 1, + "se_ratio": 2 + }, + { + "filters": 32, + "depth": 2, + "kernel_size": [ + 1, + 9 + ], + "strides": [ + 1, + 2 + ], + "ex_ratio": 1, + "se_ratio": 2 + }, + { + "filters": 40, + "depth": 2, + "kernel_size": [ + 1, + 9 + ], + "strides": [ + 1, + 2 + ], + "ex_ratio": 1, + "se_ratio": 2 + }, + { + "filters": 48, + "depth": 1, + "kernel_size": [ + 1, + 9 + ], + "strides": [ + 1, + 2 + ], + "ex_ratio": 1, + "se_ratio": 2 + } + ], + "output_filters": 0, + "include_top": true, + "use_logits": true + } + } +} +``` diff --git a/docs/assets/usage/python-configuration.md b/docs/assets/usage/python-configuration.md new file mode 100644 index 00000000..3e317cc9 --- /dev/null +++ b/docs/assets/usage/python-configuration.md @@ -0,0 +1,84 @@ +```python + +hk.HKTaskParams( + name="arr-2-eff-sm", + project="hk-rhythm-2", + job_dir="./results/arr-2-eff-sm", + verbose=2, + datasets=[hk.NamedParams( + name="ptbxl", + params=dict( + path="./datasets/ptbxl" + ) + )], + num_classes=2, + class_map={ + "0": 0, + "7": 1, + "8": 1 + }, + class_names=[ + "NORMAL", "AFIB/AFL" + ], + class_weights="balanced", + sampling_rate=100, + frame_size=512, + samples_per_patient=[10, 10], + val_samples_per_patient=[5, 5], + test_samples_per_patient=[5, 5], + val_patients=0.20, + val_size=20000, + test_size=20000, + batch_size=256, + buffer_size=20000, + epochs=100, + steps_per_epoch=50, + val_metric="loss", + lr_rate=1e-3, + lr_cycles=1, + threshold=0.75, + val_metric_threshold=0.98, + tflm_var_name="g_rhythm_model", + tflm_file="rhythm_model_buffer.h", + backend="pc", + demo_size=896, + display_report=True, + quantization=hk.QuantizationParams( + qat=False, + format="INT8", + io_type="int8", + conversion="CONCRETE", + debug=False + ), + preprocesses=[ + hk.NamedParams( + name="layer_norm", + params=dict( + epsilon=0.01, + name="znorm" + ) + ) + ], + augmentations=[ + ], + model_file="model.keras", + use_logits=False, + architecture=hk.NamedParams( + name="efficientnetv2", + params=dict( + input_filters=16, + input_kernel_size=[1, 9], + input_strides=[1, 2], + blocks=[ + {"filters": 24, "depth": 2, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 2}, + {"filters": 32, "depth": 2, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 2}, + {"filters": 40, "depth": 2, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 2}, + {"filters": 48, "depth": 1, "kernel_size": [1, 9], "strides": [1, 2], "ex_ratio": 1, "se_ratio": 2} + ], + output_filters=0, + include_top=True, + use_logits=True + ) + } +) +``` diff --git a/docs/assets/usage/python-download-snippet.md b/docs/assets/usage/python-download-snippet.md index 33d39d1a..111d40ae 100644 --- a/docs/assets/usage/python-download-snippet.md +++ b/docs/assets/usage/python-download-snippet.md @@ -4,7 +4,7 @@ import heartkit as hk hk.datasets.download_datasets(hk.HKDownloadParams( ds_path=Path("./datasets"), - datasets=["icentia11k", "ludb", "qtdb", "synthetic"], + datasets=["icentia11k", "ludb", "qtdb", "ecg-synthetic"], progress=True )) ``` diff --git a/docs/assets/usage/python-evaluate-snippet.md b/docs/assets/usage/python-evaluate-snippet.md index 71a882f3..2eb056b6 100644 --- a/docs/assets/usage/python-evaluate-snippet.md +++ b/docs/assets/usage/python-evaluate-snippet.md @@ -4,7 +4,7 @@ import heartkit as hk task = hk.TaskFactory.get("rhythm") -task.evaluate(hk.HKTestParams( +params = hk.HKTaskParams( job_dir=Path("./results/rhythm-class-2"), ds_path=Path("./datasets"), datasets=[{ @@ -22,10 +22,17 @@ task.evaluate(hk.HKTestParams( ], sampling_rate=200, frame_size=800, - test_samples_per_patient=[100, 800], - test_patients=1000, - test_size=100000, - model_file=Path("./results/rhythm-class-2/model.keras"), - threshold=0.75 -)) + samples_per_patient=[100, 800], + val_samples_per_patient=[100, 800], + train_patients=10000, + val_patients=0.10, + val_size=200000, + batch_size=256, + buffer_size=100000, + epochs=100, + steps_per_epoch=20, + val_metric="loss", +) + +task.evaluate(params) ``` diff --git a/docs/assets/usage/python-export-snippet.md b/docs/assets/usage/python-export-snippet.md index 79674ed6..959e62b6 100644 --- a/docs/assets/usage/python-export-snippet.md +++ b/docs/assets/usage/python-export-snippet.md @@ -3,6 +3,37 @@ from pathlib import Path import heartkit as hk task = hk.TaskFactory.get("rhythm") + +params = hk.HKTaskParams( + job_dir=Path("./results/rhythm-class-2"), + ds_path=Path("./datasets"), + datasets=[{ + "name": "icentia11k", + "params": {} + }], + num_classes=2, + class_map={ + 0: 0, + 1: 1, + 2: 1 + }, + class_names=[ + "NONE", "AFIB/AFL" + ], + sampling_rate=200, + frame_size=800, + samples_per_patient=[100, 800], + val_samples_per_patient=[100, 800], + train_patients=10000, + val_patients=0.10, + val_size=200000, + batch_size=256, + buffer_size=100000, + epochs=100, + steps_per_epoch=20, + val_metric="loss", +) + task.export(hk.HKExportParams( job_dir=Path("./results/rhythm-class-2"), ds_path=Path("./datasets"), diff --git a/docs/assets/usage/python-train-snippet.md b/docs/assets/usage/python-train-snippet.md index 25cf31d8..eb5ad663 100644 --- a/docs/assets/usage/python-train-snippet.md +++ b/docs/assets/usage/python-train-snippet.md @@ -4,7 +4,7 @@ import heartkit as hk task = hk.TaskFactory.get("rhythm") -task.train(hk.HKTrainParams( +params = hk.HKTaskParams( job_dir=Path("./results/rhythm-class-2"), ds_path=Path("./datasets"), datasets=[{ @@ -32,5 +32,8 @@ task.train(hk.HKTrainParams( epochs=100, steps_per_epoch=20, val_metric="loss", -)) +) + +task.train(params) + ``` diff --git a/docs/datasets/byod.md b/docs/datasets/byod.md index 4901fc08..02e797f7 100644 --- a/docs/datasets/byod.md +++ b/docs/datasets/byod.md @@ -2,36 +2,68 @@ The Bring-Your-Own-Dataset (BYOD) feature allows users to add custom datasets for training and evaluating models. This feature is useful when working with proprietary or custom datasets that are not available in the HeartKit library. -## How it Works +## How it Works -1. **Create a Dataset**: Define a new dataset by creating a new Python file. The file should contain a class that inherits from the `HKDataset` base class and implements the required methods. +1. **Create a Dataset**: Define a new dataset that inherits `HKDataset` and implements the required abstract methods. - ```python - import heartkit as hk +```python - class CustomDataset(hk.HKDataset): - def __init__(self, config): - super().__init__(config) +import numpy as np +import heartkit as hk - def download(self): - pass +class MyDataset(hk.HKDataset): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) - def generate(self): - pass - ``` + @property + def name(self) -> str: + return 'my-dataset' + + @property + def sampling_rate(self) -> int: + return 100 + + def get_train_patient_ids(self) -> npt.NDArray: + return np.arange(80) + + def get_test_patient_ids(self) -> npt.NDArray: + return np.arange(80, 100) + + @contextlib.contextmanager + def patient_data(self, patient_id: int) -> Generator[PatientData, None, None]: + data = np.random.randn(1000) + segs = np.random.randint(0, 1000, (10, 2)) + yield {"data": data, "segmentations": segs} + + def signal_generator( + self, + patient_generator: PatientGenerator, + frame_size: int, + samples_per_patient: int = 1, + target_rate: int | None = None, + ) -> Generator[npt.NDArray, None, None]: + for patient in patient_generator: + for _ in range(samples_per_patient): + with self.patient_data(patient) as pt: + yield pt["data"] + + def download(self, num_workers: int | None = None, force: bool = False): + pass + +``` 2. **Register the Dataset**: Register the new dataset with the `DatasetFactory` by calling the `register` method. This method takes the dataset name and the dataset class as arguments. ```python import heartkit as hk - hk.DatasetFactory.register("custom", CustomDataset) + hk.DatasetFactory.register("my-dataset", CustomDataset) ``` 3. **Use the Dataset**: The new dataset can now be used with the `DatasetFactory` to perform various operations such as downloading and generating data. ```python import heartkit as hk - - dataset = hk.DatasetFactory.create("custom", config) + params = {} + dataset = hk.DatasetFactory.get("my-dataset")(**params) ``` diff --git a/docs/datasets/dataset.md b/docs/datasets/dataset.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/datasets/icentia11k.md b/docs/datasets/icentia11k.md index 3809debf..c5bf1f30 100644 --- a/docs/datasets/icentia11k.md +++ b/docs/datasets/icentia11k.md @@ -6,6 +6,39 @@ This dataset consists of ECG recordings from 11,000 patients and 2 billion label More info available on [PhysioNet website](https://physionet.org/content/icentia11k-continuous-ecg/1.0) +## Usage + +!!! Example Python + + ```python + from pathlib import Path + import neuralspot_edge as nse + import heartkit as hk + + ds = hk.DatasetFactory.get('icentia11k')( + path=Path("./datasets/icentia11k") + ) + + # Download dataset + ds.download(force=False) + + # Create signal generator + data_gen = self.ds.signal_generator( + patient_generator=nse.utils.uniform_id_generator(ds.patient_ids, repeat=True, shuffle=True), + frame_size=256, + samples_per_patient=5, + target_rate=100, + ) + + # Grab single ECG sample + ecg = next(data_gen) + + ``` + +???+ note + The __Icentia11k dataset__ requires roughly 200 GB of disk space and can take around 2 hours to download. + + ## Funding This work is partially funded by a grant from Icentia, Fonds de Recherche en Santé du Québec, and the Institute of Data Valorization (IVADO). @@ -15,30 +48,11 @@ This work is partially funded by a grant from Icentia, Fonds de Recherche en San The Icentia11k dataset is available for non-commercial use only. [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](https://physionet.org/content/icentia11k-continuous-ecg/view-license/1.0/) -## Supported Tasks + !!! warning The dataset is intended for evaluation purposes only and cannot be used for commercial use without permission. Please visit [Physionet](https://physionet.org/content/icentia11k-continuous-ecg/1.0) for more details. - -## Usage - -!!! Example Python - - ```python - from pathlib import Path - import heartkit as hk - - # Download dataset - hk.datasets.download_datasets(hk.HKDownloadParams( - ds_path=Path("./datasets"), - datasets=["icentia11k"], - progress=True - )) - ``` - -???+ note - The __Icentia11k dataset__ requires roughly 200 GB of disk space and can take around 2 hours to download. diff --git a/docs/datasets/index.md b/docs/datasets/index.md index 9ce1a32c..a7206e5c 100644 --- a/docs/datasets/index.md +++ b/docs/datasets/index.md @@ -1,68 +1,52 @@ -# :factory: Dataset Factory +# :material-database: Datasets -HeartKit provides support for a number of datasets to facilitate training the __heart-monitoring tasks__. Most of the datasets are readily available and can be downloaded and used for training and evaluation. Please make sure to review each dataset's license for terms and limitations. +HeartKit provides support for a number of datasets to facilitate training the __heart-monitoring tasks__. Most of the datasets are readily available and can be downloaded and used for training and evaluation. The datasets inherit from `HKDataset` and can be accessed either directly or through the factory singleton [`DatasetFactory`](#dataset-factory). -## Denoise Datasets +## Available Datasets -ECG denoising is the process of removing noise from an ECG signal. The following datasets are available for denoising tasks: +Below is a list of the currently available datasets in HeartKit. Please make sure to review each dataset's license for terms and limitations. -* **[LUDB](./ludb.md)**: Lobachevsky University Electrocardiography database consists of 200 10-second 12-lead records. The boundaries and peaks of P, T waves and QRS complexes were manually annotated by cardiologists. Each record is annotated with the corresponding diagnosis. - -* **[PTB-XL](./ptbxl.md)**: The PTB-XL is a large publicly available electrocardiography dataset. It contains 21837 clinical 12-lead ECGs from 18885 patients of 10 second length. The ECGs are sampled at 500 Hz and are annotated by up to two cardiologists. - -* **[Synthetic](./synthetic.md)**: A synthetic dataset generated using PhysioKit. The dataset enables the generation of ECG signals with a variety of heart conditions and noise levels. - ---- - -## Segmentation Datasets +* **[Icentia11k](./icentia11k.md)**: This dataset consists of ECG recordings from 11,000 patients and 2 billion labelled beats. The data was collected by the CardioSTAT, a single-lead heart monitor device from Icentia. The raw signals were recorded with a 16-bit resolution and sampled at 250 Hz with the CardioSTAT in a modified lead 1 position. -ECG segmentation is the process of identifying the boundaries of the P-wave, QRS complex, and T-wave in an ECG signal. The following datasets are available for segmentation tasks: +* **[LSAD](./lsad.md)**: The Large Scale Rhythm Database (LSAD) is a large publicly available electrocardiography dataset. It contains 10 second, 12-lead ECGs of 45,152 patients with a 500 Hz sampling rate. The ECGs are sampled at 500 Hz and are annotated by up to two cardiologists. * **[LUDB](./ludb.md)**: Lobachevsky University Electrocardiography database consists of 200 10-second 12-lead records. The boundaries and peaks of P, T waves and QRS complexes were manually annotated by cardiologists. Each record is annotated with the corresponding diagnosis. -* **[QTDB](./qtdb.md)**: Over 100 fifteen-minute two-lead ECG recordings with onset, peak, and end markers for P, QRS, T, and (where present) U waves of from 30 to 50 selected beats in each recording. - -* **[Synthetic](./synthetic.md)**: A synthetic dataset generated using PhysioKit. The dataset enables the generation of ECG signals with a variety of heart conditions and noise levels. - ---- - -## Rhythm Datasets - -Rhythm detection is the process of identifying abnormal heart rhythms. The following datasets are available for rhythm tasks: +* **[PTB-XL](./ptbxl.md)**: The PTB-XL is a large publicly available electrocardiography dataset. It contains 21837 clinical 12-lead ECGs from 18885 patients of 10 second length. The ECGs are sampled at 500 Hz and are annotated by up to two cardiologists. -* **[Icentia11k](./icentia11k.md)**: This dataset consists of ECG recordings from 11,000 patients and 2 billion labelled beats. The data was collected by the CardioSTAT, a single-lead heart monitor device from Icentia. The raw signals were recorded with a 16-bit resolution and sampled at 250 Hz with the CardioSTAT in a modified lead 1 position. +* **[QTDB](./qtdb.md)**: Over 100 fifteen-minute two-lead ECG recordings with onset, peak, and end markers for P, QRS, T, and (where present) U waves of from 30 to 50 selected beats in each recording. -* **[PTB-XL](./ptbxl.md)**: The PTB-XL is a large publicly available electrocardiography dataset. It contains 21837 clinical 12-lead ECGs from 18885 patients of 10 second length. The ECGs are sampled at 500 Hz and are annotated by up to two cardiologists. +* **[ECG Synthetic](./synthetic.md)**: An ECG synthetic dataset generated using PhysioKit. The dataset enables the generation of 12-lead ECG signals with a variety of heart conditions and noise levels along with segmentations and fiducial points. -* **[LSAD](./lsad.md)**: The Large Scale Rhythm Database (LSAD) is a large publicly available electrocardiography dataset. It contains 10 second, 12-lead ECGs of 45,152 patients with a 500 Hz sampling rate. The ECGs are sampled at 500 Hz and are annotated by up to two cardiologists. +* **[PPG Synthetic](./synthetic.md)**: A PPG synthetic dataset generated using PhysioKit. The dataset enables the generation of a 1-lead PPG signal with segmentations and fiducials. -* **[Synthetic](./synthetic.md)**: A synthetic dataset generated using PhysioKit. The dataset enables the generation of ECG signals with a variety of heart conditions and noise levels. +* **[Bring-Your-Own-Data](./byod.md)**: Add new datasets to HeartKit by providing your own data. Subclass `HKDataset` and register it with the `DatasetFactory`. ---- +## Dataset Factory -## Beat Datasets +The dataset factory, `DatasetFactory`, provides a convenient way to access the datasets. The factory is a thread-safe singleton class that provides a single point of access to the datasets via the datasets' slug names. The benefit of using the factory is it allows registering new additional datasets that can then be leveraged by existing and new tasks. -Beat classification is the process of identifying abnormal beats in an ECG signal. The following datasets are available for beat classification tasks: +The dataset factory provides the following methods: -* **[Icentia11k](./icentia11k.md)**: This dataset consists of ECG recordings from 11,000 patients and 2 billion labelled beats. The data was collected by the CardioSTAT, a single-lead heart monitor device from Icentia. The raw signals were recorded with a 16-bit resolution and sampled at 250 Hz with the CardioSTAT in a modified lead 1 position. +* **hk.DatasetFactory.register**: Register a custom dataset +* **hk.DatasetFactory.unregister**: Unregister a custom dataset +* **hk.DatasetFactory.has**: Check if a dataset is registered +* **hk.DatasetFactory.get**: Get a dataset +* **hk.DatasetFactory.list**: List all available datasets -* **[PTB-XL](./ptbxl.md)**: The PTB-XL is a large publicly available electrocardiography dataset. It contains 21837 clinical 12-lead ECGs from 18885 patients of 10 second length. The ECGs are sampled at 500 Hz and are annotated by up to two cardiologists. +```python - +``` diff --git a/docs/datasets/lsad.md b/docs/datasets/lsad.md index 9aec0c22..24db25a0 100644 --- a/docs/datasets/lsad.md +++ b/docs/datasets/lsad.md @@ -6,6 +6,35 @@ The large scale arrhythmia database (LSAD) is a large-scale, multi-center, multi Please visit [Physionet](https://physionet.org/content/ecg-arrhythmia/1.0.0/) for more details. +## Usage + +!!! Example Python + + ```python + from pathlib import Path + import neuralspot_edge as nse + import heartkit as hk + + ds = hk.DatasetFactory.get('lsad')( + path=Path("./datasets/lsad") + ) + + # Download dataset + ds.download(force=False) + + # Create signal generator + data_gen = self.ds.signal_generator( + patient_generator=nse.utils.uniform_id_generator(ds.patient_ids, repeat=True, shuffle=True), + frame_size=256, + samples_per_patient=5, + target_rate=100, + ) + + # Grab single ECG sample + ecg = next(data_gen) + + ``` + ## Statistics | Acronym Name | Full Name | Frequency, n(%) | Age, Mean ± SD |Male,n(%) | @@ -29,23 +58,3 @@ This dataset received funding from the Kay Family Foundation Data Analytic Grant ## License The dataset is available under [Creative Commons Attribution 4.0 International Public License](https://physionet.org/content/ecg-arrhythmia/view-license/1.0.0/) - -## Supported Tasks - -* [Rhythm](../tasks/rhythm.md) - -## Usage - -!!! Example Python - - ```python - from pathlib import Path - import heartkit as hk - - # Download dataset - hk.datasets.download_datasets(hk.HKDownloadParams( - ds_path=Path("./datasets"), - datasets=["lsad"], - progress=True - )) - ``` diff --git a/docs/datasets/ludb.md b/docs/datasets/ludb.md index 1bb5e7d7..a8697355 100644 --- a/docs/datasets/ludb.md +++ b/docs/datasets/ludb.md @@ -6,6 +6,35 @@ The Lobachevsky University Electrocardiography database (LUDB) consists of 200 1 Please visit [Physionet](https://physionet.org/content/ludb/1.0.1/) for more details. +## Usage + +!!! Example Python + + ```python + from pathlib import Path + import neuralspot_edge as nse + import heartkit as hk + + ds = hk.DatasetFactory.get('ludb')( + path=Path("./datasets/ludb") + ) + + # Download dataset + ds.download(force=False) + + # Create signal generator + data_gen = self.ds.signal_generator( + patient_generator=nse.utils.uniform_id_generator(ds.patient_ids, repeat=True, shuffle=True), + frame_size=256, + samples_per_patient=5, + target_rate=100, + ) + + # Grab single ECG sample + ecg = next(data_gen) + + ``` + ## Funding The study was supported by the Ministry of Education of the Russian Federation (contract No. 02.G25.31.0157 of 01.12.2015). @@ -17,20 +46,3 @@ The LUDB is available for commercial use. ## Supported Tasks * [Segmentation](../tasks/segmentation.md) - - -## Usage - -!!! Example Python - - ```python - from pathlib import Path - import heartkit as hk - - # Download dataset - hk.datasets.download_datasets(hk.HKDownloadParams( - ds_path=Path("./datasets"), - datasets=["ludb"], - progress=True - )) - ``` diff --git a/docs/datasets/mitbih.md b/docs/datasets/mitbih.md index 75849b24..f87a5368 100644 --- a/docs/datasets/mitbih.md +++ b/docs/datasets/mitbih.md @@ -29,8 +29,12 @@ This database is available for commercial use. [Open Data Commons Attribution Li # Download dataset hk.datasets.download_datasets(hk.HKDownloadParams( - ds_path=Path("./datasets"), - datasets=["mitbih"], + datasets=[{ + "name": "mitbih", + "params": { + "path": "./datasets/mitbih" + } + }], progress=True )) ``` diff --git a/docs/datasets/ptbxl.md b/docs/datasets/ptbxl.md index 982bf333..f75da101 100644 --- a/docs/datasets/ptbxl.md +++ b/docs/datasets/ptbxl.md @@ -6,34 +6,47 @@ This dataset consists of 21837 clinical 12-lead ECGs from 18885 patients. The EC Please visit [Physionet](https://physionet.org/content/ptb-xl/1.0.3/) for more details. -### Funding - -This work was supported by BMBF (01IS14013A), Berlin Big Data Center, Berlin Center for Machine Learning, and EMPIR project 18HLT07 MedalCare. - -### License - -This database is available under [Creative Commons Attribution 4.0 International Public License](https://physionet.org/content/ptb-xl/view-license/1.0.3/) - -### Supported Tasks - -* [Rhythm](../tasks/rhythm.md) - ## Usage !!! Example Python ```python from pathlib import Path + import neuralspot_edge as nse import heartkit as hk + ds = hk.DatasetFactory.get('ptbxl')( + path=Path("./datasets/ptbxl") + ) + # Download dataset - hk.datasets.download_datasets(hk.HKDownloadParams( - ds_path=Path("./datasets"), - datasets=["ptbxl"], - progress=True - )) + ds.download(force=False) + + # Create signal generator + data_gen = self.ds.signal_generator( + patient_generator=nse.utils.uniform_id_generator(ds.patient_ids, repeat=True, shuffle=True), + frame_size=256, + samples_per_patient=5, + target_rate=100, + ) + + # Grab single ECG sample + ecg = next(data_gen) + ``` +### Funding + +This work was supported by BMBF (01IS14013A), Berlin Big Data Center, Berlin Center for Machine Learning, and EMPIR project 18HLT07 MedalCare. + +### License + +This database is available under [Creative Commons Attribution 4.0 International Public License](https://physionet.org/content/ptb-xl/view-license/1.0.3/) + + + ## References * [Deep Learning for ECG Analysis: Benchmarks and Insights from PTB-XL](https://arxiv.org/pdf/2004.13701.pdf) diff --git a/docs/datasets/qtdb.md b/docs/datasets/qtdb.md index a1e5dd74..bd062795 100644 --- a/docs/datasets/qtdb.md +++ b/docs/datasets/qtdb.md @@ -6,6 +6,33 @@ Over 100 fifteen-minute two-lead ECG recordings with onset, peak, and end marker Please visit [Physionet](https://doi.org/10.13026/C24K53) for more details. +!!! Example Python + + ```python + from pathlib import Path + import neuralspot_edge as nse + import heartkit as hk + + ds = hk.DatasetFactory.get('qtdb')( + path=Path("./datasets/qtdb") + ) + + # Download dataset + ds.download(force=False) + + # Create signal generator + data_gen = self.ds.signal_generator( + patient_generator=nse.utils.uniform_id_generator(ds.patient_ids, repeat=True, shuffle=True), + frame_size=256, + samples_per_patient=5, + target_rate=100, + ) + + # Grab single ECG sample + ecg = next(data_gen) + + ``` + ## Funding The QT Database was created as part of a project funded by the National Library of Medicine. @@ -17,19 +44,3 @@ The QT Database is available for commercial use. [Open Data Commons Attribution ## Supported Tasks * [Segmentation](../tasks/segmentation.md) - -## Usage - -!!! Example Python - - ```python - from pathlib import Path - import heartkit as hk - - # Download dataset - hk.datasets.download_datasets(hk.HKDownloadParams( - ds_path=Path("./datasets"), - datasets=["qtdb"], - progress=True - )) - ``` diff --git a/docs/datasets/synthetic.md b/docs/datasets/synthetic.md index 720d80dd..e00e6994 100644 --- a/docs/datasets/synthetic.md +++ b/docs/datasets/synthetic.md @@ -1,50 +1,51 @@ -# Synthetic Data +# Synthetic Datasets ### Overview By leveraging [PhysioKit](https://ambiqai.github.io/physiokit/), we are able to generate synthetic data for a variety of physiological signals, including ECG, PPG, and respiration. In addition to the signals, the tool also provides corresponding landmark fiducials and segmentation annotations. While not a replacement for real-world data, synthetic data can be useful in conjunction with real-world data for training and testing the models. -Please visit [PhysioKit](https://ambiqai.github.io/physiokit/) for more details. +## Available Datasets +### ECG Synthetic -## Funding +An ECG synthetic dataset generated using PhysioKit. The dataset enables the generation of 12-lead ECG signals with a variety of heart conditions and noise levels along with segmentations and fiducial points. -NA +### PPG Synthetic -## Licensing - -The tool is available under BSD-3-Clause License. - -## Supported Tasks - -* [Rhythm](../tasks/rhythm.md) -* [Segmentation](../tasks/segmentation.md) +A PPG synthetic dataset generated using PhysioKit. The dataset enables the generation of a 1-lead PPG signal with segmentations and fiducials. ## Usage !!! Example Python ```python - import physiokit as pk - - heart_rate = 64 # BPM - sample_rate = 1000 # Hz - signal_length = 10*sample_rate # 10 seconds - - # Generate NSR synthetic ECG signal - ecg, segs, fids = pk.ecg.synthesize( - signal_length=signal_length, - sample_rate=sample_rate, - heart_rate=heart_rate, - leads=1, - preset=pk.ecg.EcgPreset.NSR, - p_multiplier=1.5, - t_multiplier=1.2, - noise_multiplier=0.2 + import heartkit as hk + + ds = hk.DatasetFactory.get('ecg-synthetic')( + num_pts=100, + params=dict( + sample_rate=1000, # Hz + duration=10, # seconds + heart_rate=(40, 120), + ) ) + with ds.patient_data(patient_id=ds.patient_ids[0]) as pt: + ecg = pt["data"][:] + segs = pt["segmentations"][:] + fids = pt["fiducials"][:] + ```
- --8<-- "assets/segmentation_example.html" + --8<-- "assets/tasks/segmentation/segmentation-example.html"
+ + +## Funding + +NA + +## Licensing + +The tool is available under BSD-3-Clause License. diff --git a/docs/guides/byot.ipynb b/docs/guides/byot.ipynb new file mode 100644 index 00000000..e69de29b diff --git a/docs/guides/ecg-foundation-model copy.ipynb b/docs/guides/ecg-foundation-model copy.ipynb new file mode 100644 index 00000000..b222bbfd --- /dev/null +++ b/docs/guides/ecg-foundation-model copy.ipynb @@ -0,0 +1,2382 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ECG Foundation Model\n", + "\n", + "__Date created:__ 2024/07/17 \n", + "\n", + "__Last Modified:__ 2024/07/17 \n", + "\n", + "__Description:__ Train, evaluate, and export 4-stage ECG arrhythmia classifier\n", + "\n", + "## Overview \n", + "\n", + "This notebook demonstrates creating a foundational model for raw ECG signals. By creating a foundational model, we can create small, down-stream classification models." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "os.environ[\"KMP_AFFINITY\"] = \"noverbose\"\n", + "os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' # 3\n", + "os.environ['AUTOGRAPH_VERBOSITY'] = '2' # 5\n", + "\n", + "import functools\n", + "import random\n", + "from typing import Generator\n", + "from pathlib import Path\n", + "import tempfile\n", + "import tensorflow as tf\n", + "from tqdm import tqdm\n", + "import sklearn.model_selection\n", + "import keras\n", + "import numpy as np\n", + "import numpy.typing as npt\n", + "import heartkit as hk\n", + "import physiokit as pk\n", + "import neuralspot_edge as nse\n", + "from neuralspot_edge.trainers.simclr import SimCLRTrainer\n", + "import matplotlib as mpl\n", + "import matplotlib.pyplot as plt\n", + "import plotly.io as pio\n", + "\n", + "hk.silence_tensorflow()\n", + "logger = hk.setup_logger('heartkit', level=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Constants\n", + "\n", + "Here we provide the constants that we will use throughout the guide. For better performance, adjust parameters such as `BATCH_SIZE`, `EPOCHS`, and `LEARNING_RATE`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Seed for reproducibility\n", + "seed = 42\n", + "\n", + "# File paths\n", + "datasets_dir = Path(\"../../datasets\")\n", + "job_dir = Path(tempfile.gettempdir()) / \"hk-foundation\"\n", + "model_file = job_dir / \"model.keras\"\n", + "val_file = job_dir / \"val.pkl\"\n", + "\n", + "os.makedirs(job_dir, exist_ok=True)\n", + "\n", + "# Data settings\n", + "sampling_rate = 100 # 100 Hz\n", + "input_size = 1000 # 10 seconds\n", + "frame_size = 800 # 8 seconds\n", + "\n", + "# Training settings\n", + "batch_size = 1024 # Batch size for training\n", + "buffer_size = 10000 # How many samples are shuffled each epoch\n", + "epochs = 100 # Increase this to 100+\n", + "steps_per_epoch = 25 # # Steps per epoch (must set since ds has unknown size)\n", + "samples_per_patient = 1 # Number of samples per patient\n", + "val_size = 1000 # Number of samples used for validation\n", + "test_size = 1000 # Number of samples used for validation\n", + "val_percentage = 0.2 # Percentage of samples used for validation\n", + "verbose = 1 # Verbosity level\n", + "learning_rate = 1e-3 # Learning rate for Adam optimizer\n", + "\n", + "# Model settings\n", + "projection_width = 128\n", + "temperature = 0.1\n", + "\n", + "# Plotting settings\n", + "bg_rgba_color = \"rgba(38,42,50,1.0)\"\n", + "bg_color = \"#262a32\"\n", + "primary_color = \"#11acd5\"\n", + "secondary_color = \"#ce6cff\"\n", + "tertiary_color = \"#ea3424\"\n", + "quaternary_color = \"#5cc99a\"\n", + "colors = [primary_color, secondary_color, tertiary_color, quaternary_color]\n", + "plotly_template = \"plotly_dark\"\n", + "pio.renderers.default = \"notebook\"\n", + "plt.style.use('dark_background')\n", + "mpl.rcParams['axes.facecolor'] = bg_color\n", + "mpl.rcParams['figure.facecolor'] = bg_color" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configure datasets\n", + "\n", + "We are going to train our model using two large datasets: the PTB-XL dataset and the large-scale arrhythmia dataset. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "datasets = [\n", + " hk.DatasetParams(\n", + " name=\"lsad\",\n", + " path=datasets_dir / \"lsad\",\n", + " params={}\n", + " ),\n", + " hk.DatasetParams(\n", + " name=\"ptbxl\",\n", + " path=datasets_dir / \"ptbxl\",\n", + " params={}\n", + " ),\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Download datasets" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
DEBUG    Creating working directory in /tmp                                                          download.py:19\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Creating working directory in \u001b[35m/\u001b[0m\u001b[95mtmp\u001b[0m \u001b]8;id=729401;file:///workspaces/heartkit/heartkit/datasets/download.py\u001b\\\u001b[2mdownload.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=942857;file:///workspaces/heartkit/heartkit/datasets/download.py#19\u001b\\\u001b[2m19\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "hk.datasets.download_datasets(hk.HKDownloadParams(\n", + " datasets=datasets,\n", + " force=False,\n", + " progress=True\n", + "))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Lets load all subjects data and split into train and test" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 36120/36120 [00:13<00:00, 2764.34it/s]\n", + "100%|██████████| 18500/18500 [00:07<00:00, 2611.86it/s]\n" + ] + } + ], + "source": [ + "dsets = [hk.DatasetFactory.get(dataset.name)(\n", + " ds_path=dataset.path,\n", + ") for dataset in datasets]\n", + "\n", + "num_pts = sum((len(ds.get_train_patient_ids()) for ds in dsets))\n", + "\n", + "train_data = np.zeros((\n", + " num_pts,\n", + " input_size,\n", + " 1\n", + "))\n", + "pt_idx = 0\n", + "for ds in dsets:\n", + " train_pt_ids = ds.get_train_patient_ids()\n", + " for pt_id in tqdm(train_pt_ids):\n", + " with ds.patient_data(pt_id) as h5:\n", + " data = h5[\"data\"][0:1, :].T\n", + " # END WITH\n", + " data = pk.signal.resample_signal(data, sample_rate=ds.sampling_rate, target_rate=sampling_rate, axis=0)\n", + " data = np.expand_dims(data, axis=0)\n", + " train_data[pt_idx] = data\n", + " pt_idx += 1\n", + " # END FOR\n", + "# END FOR" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "train_data, val_data = sklearn.model_selection.train_test_split(\n", + " train_data,\n", + " test_size=val_percentage,\n", + " random_state=seed\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create TF train and validation datasets" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-07-25 21:33:53.890257: E external/local_xla/xla/stream_executor/cuda/cuda_driver.cc:266] failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected\n" + ] + } + ], + "source": [ + "train_ds = tf.data.Dataset.from_tensor_slices(train_data)\n", + "train_ds = train_ds.shuffle(\n", + " buffer_size,\n", + ").batch(\n", + " batch_size\n", + ")\n", + "\n", + "val_ds = tf.data.Dataset.from_tensor_slices(val_data)\n", + "val_ds = val_ds.batch(\n", + " batch_size\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(1024, 1000, 1)\n" + ] + } + ], + "source": [ + "x = next(iter(train_ds))\n", + "print(x.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "nstdb = hk.datasets.nstdb.NstdbNoise(target_rate=sample_rate)\n", + "noises = np.hstack((nstdb.get_noise(noise_type=\"bw\"), nstdb.get_noise(noise_type=\"ma\"), nstdb.get_noise(noise_type=\"em\")))\n", + "\n", + "augmentation_pipeline = nse.layers.preprocessing.ts.augmentation_pipeline.AugmentationPipeline(\n", + " layers=[\n", + " nse.layers.preprocessing.ts.random_crop.RandomCrop(\n", + " duration=frame_size,\n", + " ),\n", + " nse.layers.preprocessing.ts.gaussian_noise.GaussianNoise(\n", + " stddev=0.05\n", + " ),\n", + " nse.layers.preprocessing.ts.random_cutout.RandomCutout(\n", + " factor=(0.05, 0.1),\n", + " cutouts=(1, 3)\n", + " fill_mode=\"constant\",\n", + " fill_value=0.0\n", + " ),\n", + " nse.layers.preprocessing.ts.random_background_noises.RandomBackgroundNoises(\n", + " noises=noises\n", + " )\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "test_ds = train_ds.map(augmentation_pipeline)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(1024, 800, 1)\n" + ] + } + ], + "source": [ + "x = next(iter(test_ds))\n", + "print(x.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "dsets = []\n", + "for dset in datasets:\n", + " if hk.DatasetFactory.has(dset.name):\n", + " dsets.append(hk.DatasetFactory.get(dset.name)(ds_path=dset.path, **dset.params))\n", + " # END IF\n", + "# END FOR" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Preprocess pipeline\n", + "\n", + "We will preprocess the ECG signals by applying the following steps:\n", + "* Apply Z-score normalization w/ epsilon to avoid division by zero\n", + "\n", + "The task accepts a list of preprocessing functions that will be applied to the input data. \n", + "\n", + "__NOTE:__ We dont apply any filtering as the model is expected to learn the filtering mechanism." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "preprocesses = [\n", + " hk.PreprocessParams(name=\"znorm\", params=dict(eps=0.01, axis=None))\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Augmentation pipeline\n", + "\n", + "We will apply the following augmentations to the ECG signals:\n", + "* Baseline wander: Simulate baseline wander by adding a random frequency sinusoidal signal to the ECG signal\n", + "* Powerline noise: Simulate powerline noise by adding a 50 Hz sinusoidal signal to the ECG signal\n", + "* Burst noise: Simulate burst noise by randomly injecting burst of high frequency noise to the ECG signal\n", + "* Noise sources: Apply several noises at given frequencies to the ECG signal\n", + "* Lead noise: Simulate lead noise by adding a random frequency sinusoidal signal to the ECG signal\n", + "* NSTDB: Add real noise captured from NSTDB dataset to the ECG signal. \n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "augmentations = [\n", + " hk.AugmentationParams(name=\"baseline_wander\", params=dict(amplitude=[0.0, 0.5], frequency=[0.5, 1.5])),\n", + " hk.AugmentationParams(name=\"powerline_noise\", params=dict(amplitude=[0.05, 0.15], frequency=[45, 50])),\n", + " hk.AugmentationParams(name=\"burst_noise\", params=dict(burst_number=[0, 4], amplitude=[0.05, 0.1], frequency=[20, 49])),\n", + " hk.AugmentationParams(name=\"noise_sources\", params=dict(num_sources=[1, 2], amplitude=[0.05, 0.1], frequency=[10, 40])),\n", + " hk.AugmentationParams(name=\"lead_noise\", params=dict(scale=[0.05, 0.1])),\n", + " hk.AugmentationParams(name=\"nstdb\", params=dict(noise_level=[0.1, 0.3]))\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def data_generator(\n", + " patient_generator: hk.datasets.defines.PatientGenerator,\n", + " ds: hk.datasets.HKDataset,\n", + " frame_size: int,\n", + " samples_per_patient: int | list[int] = 1,\n", + " target_rate: int | None = None,\n", + ") -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]:\n", + " \"\"\"Generate frames using patient generator.\n", + "\n", + " Args:\n", + " patient_generator (PatientGenerator): Patient Generator\n", + " ds: PtbxlDataset\n", + " frame_size (int): Frame size\n", + " samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1.\n", + " target_rate (int|None, optional): Target rate. Defaults to None.\n", + "\n", + " Returns:\n", + " Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Sample generator\n", + "\n", + " \"\"\"\n", + " input_size = int(np.round((ds.sampling_rate / target_rate) * frame_size))\n", + " data_cache = {}\n", + " for pt in patient_generator:\n", + " if pt not in data_cache:\n", + " with ds.patient_data(pt) as h5:\n", + " data_cache[pt] = h5[\"data\"][:]\n", + " data = data_cache[pt]\n", + "\n", + " for _ in range(samples_per_patient):\n", + " leads = random.sample(ds.leads, k=2)\n", + " lead_p1 = leads[0]\n", + " lead_p2 = leads[1]\n", + " start_p1 = np.random.randint(0, data.shape[1] - input_size)\n", + " start_p2 = np.random.randint(0, data.shape[1] - input_size)\n", + " # start_p2 = start_p1\n", + "\n", + " x1 = np.nan_to_num(data[lead_p1, start_p1 : start_p1 + input_size].squeeze()).astype(np.float32)\n", + " x2 = np.nan_to_num(data[lead_p2, start_p2 : start_p2 + input_size].squeeze()).astype(np.float32)\n", + "\n", + " if ds.sampling_rate != target_rate:\n", + " x1 = pk.signal.resample_signal(x1, ds.sampling_rate, target_rate, axis=0)\n", + " x2 = pk.signal.resample_signal(x2, ds.sampling_rate, target_rate, axis=0)\n", + " # END IF\n", + " yield x1, x2\n", + " # END FOR\n", + " # END FOR\n", + "\n", + "def preprocess(x: npt.NDArray, preprocesses: list[hk.PreprocessParams], sample_rate: float) -> npt.NDArray:\n", + " \"\"\"Preprocess data pipeline\n", + "\n", + " Args:\n", + " x (npt.NDArray): Input data\n", + " preprocesses (list[PreprocessParams]): Preprocess parameters\n", + " sample_rate (float): Sample rate\n", + "\n", + " Returns:\n", + " npt.NDArray: Preprocessed data\n", + " \"\"\"\n", + " return hk.datasets.preprocess_pipeline(x, preprocesses=preprocesses, sample_rate=sample_rate)\n", + "\n", + "\n", + "def augment(x: npt.NDArray, augmentations: list[hk.AugmentationParams], sample_rate: float) -> npt.NDArray:\n", + " \"\"\"Augment data pipeline\n", + "\n", + " Args:\n", + " x (npt.NDArray): Input data\n", + " augmentations (list[AugmentationParams]): Augmentation parameters\n", + " sample_rate (float): Sample rate\n", + "\n", + " Returns:\n", + " npt.NDArray: Augmented data\n", + " \"\"\"\n", + "\n", + " return hk.datasets.augment_pipeline(x=x, augmentations=augmentations, sample_rate=sample_rate)\n", + "\n", + "def prepare(\n", + " x_y: tuple[npt.NDArray, npt.NDArray],\n", + " sample_rate: float,\n", + " preprocesses: list[hk.PreprocessParams],\n", + " augmentations: list[hk.AugmentationParams],\n", + " spec: tuple[tf.TensorSpec, tf.TensorSpec],\n", + ") -> tuple[npt.NDArray, npt.NDArray]:\n", + " \"\"\"Prepare dataset\n", + "\n", + " Args:\n", + " x_y (tuple[npt.NDArray, npt.NDArray]): Input data\n", + " sample_rate (float): Sampling rate\n", + " preprocesses (list[PreprocessParams]): Preprocessing pipeline\n", + " augmentations (list[AugmentationParams]): Augmentation pipeline\n", + " spec (tuple[tf.TensorSpec, tf.TensorSpec]): Spec\n", + " num_classes (int): Number of classes\n", + "\n", + " Returns:\n", + " tuple[npt.NDArray, npt.NDArray]: Prepared data\n", + " \"\"\"\n", + " x, y = x_y[0].copy(), x_y[1].copy()\n", + "\n", + " if augmentations:\n", + " x = augment(x, augmentations, sample_rate)\n", + " y = augment(y, augmentations, sample_rate)\n", + " # END IF\n", + "\n", + " if preprocesses:\n", + " x = preprocess(x, preprocesses, sample_rate)\n", + " y = preprocess(y, preprocesses, sample_rate)\n", + " # END IF\n", + "\n", + " x = x.reshape(spec[0].shape)\n", + " y = y.reshape(spec[0].shape)\n", + "\n", + " return x, y" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
DEBUG    Splitting patients into train and validation                                              dataloader.py:90\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Splitting patients into train and validation \u001b]8;id=308753;file:///workspaces/heartkit/heartkit/datasets/dataloader.py\u001b\\\u001b[2mdataloader.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=880211;file:///workspaces/heartkit/heartkit/datasets/dataloader.py#90\u001b\\\u001b[2m90\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Collecting 1000 validation samples                                                       dataloader.py:101\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Collecting \u001b[1;36m1000\u001b[0m validation samples \u001b]8;id=247172;file:///workspaces/heartkit/heartkit/datasets/dataloader.py\u001b\\\u001b[2mdataloader.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=936756;file:///workspaces/heartkit/heartkit/datasets/dataloader.py#101\u001b\\\u001b[2m101\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Splitting 7224 ids into 32 workers with 225 ids each                                          utils.py:182\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Splitting \u001b[1;36m7224\u001b[0m ids into \u001b[1;36m32\u001b[0m workers with \u001b[1;36m225\u001b[0m ids each \u001b]8;id=784128;file:///workspaces/heartkit/heartkit/datasets/utils.py\u001b\\\u001b[2mutils.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=844780;file:///workspaces/heartkit/heartkit/datasets/utils.py#182\u001b\\\u001b[2m182\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Loading noise data from HDF5 file.                                                             nstdb.py:37\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Loading noise data from HDF5 file. \u001b]8;id=101280;file:///workspaces/heartkit/heartkit/datasets/nstdb.py\u001b\\\u001b[2mnstdb.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=249635;file:///workspaces/heartkit/heartkit/datasets/nstdb.py#37\u001b\\\u001b[2m37\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Loading noise data from HDF5 file.                                                             nstdb.py:37\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Loading noise data from HDF5 file. \u001b]8;id=129266;file:///workspaces/heartkit/heartkit/datasets/nstdb.py\u001b\\\u001b[2mnstdb.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=746243;file:///workspaces/heartkit/heartkit/datasets/nstdb.py#37\u001b\\\u001b[2m37\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Loading noise data from HDF5 file.                                                             nstdb.py:37\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Loading noise data from HDF5 file. \u001b]8;id=642523;file:///workspaces/heartkit/heartkit/datasets/nstdb.py\u001b\\\u001b[2mnstdb.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=442469;file:///workspaces/heartkit/heartkit/datasets/nstdb.py#37\u001b\\\u001b[2m37\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Loading noise data from HDF5 file.                                                             nstdb.py:37\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Loading noise data from HDF5 file. \u001b]8;id=91296;file:///workspaces/heartkit/heartkit/datasets/nstdb.py\u001b\\\u001b[2mnstdb.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=703595;file:///workspaces/heartkit/heartkit/datasets/nstdb.py#37\u001b\\\u001b[2m37\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Loading noise data from HDF5 file.                                                             nstdb.py:37\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Loading noise data from HDF5 file. \u001b]8;id=405416;file:///workspaces/heartkit/heartkit/datasets/nstdb.py\u001b\\\u001b[2mnstdb.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=934266;file:///workspaces/heartkit/heartkit/datasets/nstdb.py#37\u001b\\\u001b[2m37\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Loading noise data from HDF5 file.                                                             nstdb.py:37\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Loading noise data from HDF5 file. \u001b]8;id=669030;file:///workspaces/heartkit/heartkit/datasets/nstdb.py\u001b\\\u001b[2mnstdb.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=500437;file:///workspaces/heartkit/heartkit/datasets/nstdb.py#37\u001b\\\u001b[2m37\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Loading noise data from HDF5 file.                                                             nstdb.py:37\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Loading noise data from HDF5 file. \u001b]8;id=395530;file:///workspaces/heartkit/heartkit/datasets/nstdb.py\u001b\\\u001b[2mnstdb.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=669595;file:///workspaces/heartkit/heartkit/datasets/nstdb.py#37\u001b\\\u001b[2m37\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Loading noise data from HDF5 file.                                                             nstdb.py:37\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Loading noise data from HDF5 file. \u001b]8;id=977903;file:///workspaces/heartkit/heartkit/datasets/nstdb.py\u001b\\\u001b[2mnstdb.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=502646;file:///workspaces/heartkit/heartkit/datasets/nstdb.py#37\u001b\\\u001b[2m37\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Loading noise data from HDF5 file.                                                             nstdb.py:37\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Loading noise data from HDF5 file. \u001b]8;id=828900;file:///workspaces/heartkit/heartkit/datasets/nstdb.py\u001b\\\u001b[2mnstdb.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=87930;file:///workspaces/heartkit/heartkit/datasets/nstdb.py#37\u001b\\\u001b[2m37\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Loading noise data from HDF5 file.                                                             nstdb.py:37\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Loading noise data from HDF5 file. \u001b]8;id=993314;file:///workspaces/heartkit/heartkit/datasets/nstdb.py\u001b\\\u001b[2mnstdb.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=883271;file:///workspaces/heartkit/heartkit/datasets/nstdb.py#37\u001b\\\u001b[2m37\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Loading noise data from HDF5 file.                                                             nstdb.py:37\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Loading noise data from HDF5 file. \u001b]8;id=868939;file:///workspaces/heartkit/heartkit/datasets/nstdb.py\u001b\\\u001b[2mnstdb.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=954823;file:///workspaces/heartkit/heartkit/datasets/nstdb.py#37\u001b\\\u001b[2m37\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Loading noise data from HDF5 file.                                                             nstdb.py:37\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Loading noise data from HDF5 file. \u001b]8;id=741203;file:///workspaces/heartkit/heartkit/datasets/nstdb.py\u001b\\\u001b[2mnstdb.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=903627;file:///workspaces/heartkit/heartkit/datasets/nstdb.py#37\u001b\\\u001b[2m37\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Loading noise data from HDF5 file.                                                             nstdb.py:37\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Loading noise data from HDF5 file. \u001b]8;id=278569;file:///workspaces/heartkit/heartkit/datasets/nstdb.py\u001b\\\u001b[2mnstdb.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=264936;file:///workspaces/heartkit/heartkit/datasets/nstdb.py#37\u001b\\\u001b[2m37\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Loading noise data from HDF5 file.                                                             nstdb.py:37\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Loading noise data from HDF5 file. \u001b]8;id=245771;file:///workspaces/heartkit/heartkit/datasets/nstdb.py\u001b\\\u001b[2mnstdb.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=950477;file:///workspaces/heartkit/heartkit/datasets/nstdb.py#37\u001b\\\u001b[2m37\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Loading noise data from HDF5 file.                                                             nstdb.py:37\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Loading noise data from HDF5 file. \u001b]8;id=681468;file:///workspaces/heartkit/heartkit/datasets/nstdb.py\u001b\\\u001b[2mnstdb.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=885918;file:///workspaces/heartkit/heartkit/datasets/nstdb.py#37\u001b\\\u001b[2m37\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Building train dataset                                                                   dataloader.py:123\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Building train dataset \u001b]8;id=695286;file:///workspaces/heartkit/heartkit/datasets/dataloader.py\u001b\\\u001b[2mdataloader.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=563967;file:///workspaces/heartkit/heartkit/datasets/dataloader.py#123\u001b\\\u001b[2m123\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Splitting 28896 ids into 32 workers with 903 ids each                                         utils.py:182\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Splitting \u001b[1;36m28896\u001b[0m ids into \u001b[1;36m32\u001b[0m workers with \u001b[1;36m903\u001b[0m ids each \u001b]8;id=49942;file:///workspaces/heartkit/heartkit/datasets/utils.py\u001b\\\u001b[2mutils.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=989743;file:///workspaces/heartkit/heartkit/datasets/utils.py#182\u001b\\\u001b[2m182\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Splitting patients into train and validation                                              dataloader.py:90\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Splitting patients into train and validation \u001b]8;id=572341;file:///workspaces/heartkit/heartkit/datasets/dataloader.py\u001b\\\u001b[2mdataloader.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=70582;file:///workspaces/heartkit/heartkit/datasets/dataloader.py#90\u001b\\\u001b[2m90\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Collecting 1000 validation samples                                                       dataloader.py:101\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Collecting \u001b[1;36m1000\u001b[0m validation samples \u001b]8;id=300188;file:///workspaces/heartkit/heartkit/datasets/dataloader.py\u001b\\\u001b[2mdataloader.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=105565;file:///workspaces/heartkit/heartkit/datasets/dataloader.py#101\u001b\\\u001b[2m101\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Splitting 3700 ids into 32 workers with 115 ids each                                          utils.py:182\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Splitting \u001b[1;36m3700\u001b[0m ids into \u001b[1;36m32\u001b[0m workers with \u001b[1;36m115\u001b[0m ids each \u001b]8;id=296311;file:///workspaces/heartkit/heartkit/datasets/utils.py\u001b\\\u001b[2mutils.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=360741;file:///workspaces/heartkit/heartkit/datasets/utils.py#182\u001b\\\u001b[2m182\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Building train dataset                                                                   dataloader.py:123\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Building train dataset \u001b]8;id=660475;file:///workspaces/heartkit/heartkit/datasets/dataloader.py\u001b\\\u001b[2mdataloader.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=217870;file:///workspaces/heartkit/heartkit/datasets/dataloader.py#123\u001b\\\u001b[2m123\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Splitting 14800 ids into 32 workers with 462 ids each                                         utils.py:182\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Splitting \u001b[1;36m14800\u001b[0m ids into \u001b[1;36m32\u001b[0m workers with \u001b[1;36m462\u001b[0m ids each \u001b]8;id=503890;file:///workspaces/heartkit/heartkit/datasets/utils.py\u001b\\\u001b[2mutils.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=3107;file:///workspaces/heartkit/heartkit/datasets/utils.py#182\u001b\\\u001b[2m182\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "id_generator = functools.partial(hk.datasets.utils.uniform_id_generator, repeat=True)\n", + "\n", + "feat_shape = (frame_size, 1)\n", + "\n", + "ds_spec = (\n", + " tf.TensorSpec(shape=feat_shape, dtype=\"float32\"),\n", + " tf.TensorSpec(shape=feat_shape, dtype=\"float32\"),\n", + ")\n", + "\n", + "train_prepare = functools.partial(\n", + " prepare,\n", + " sample_rate=sampling_rate,\n", + " preprocesses=preprocesses,\n", + " augmentations=augmentations,\n", + " spec=ds_spec\n", + ")\n", + "\n", + "train_datasets =[]\n", + "val_datasets = []\n", + "for ds in dsets:\n", + " ds_gen = functools.partial(\n", + " data_generator,\n", + " ds=ds,\n", + " frame_size=frame_size,\n", + " samples_per_patient=samples_per_patient,\n", + " target_rate=sampling_rate,\n", + " )\n", + "\n", + " train_ds, val_ds = hk.datasets.train_val_dataloader(\n", + " ds=ds,\n", + " spec=ds_spec,\n", + " data_generator=ds_gen,\n", + " id_generator=id_generator,\n", + " val_patients=val_percentage,\n", + " val_pt_samples=samples_per_patient,\n", + " val_size=val_size,\n", + " preprocess=train_prepare,\n", + " num_workers=os.cpu_count(),\n", + " )\n", + " train_datasets.append(train_ds)\n", + " val_datasets.append(val_ds)\n", + "# END FOR\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "ds_weights = np.array([d.weight for d in datasets])\n", + "ds_weights = ds_weights / ds_weights.sum()\n", + "\n", + "train_ds = tf.data.Dataset.sample_from_datasets(train_datasets, weights=ds_weights)\n", + "val_ds = tf.data.Dataset.sample_from_datasets(val_datasets, weights=ds_weights)\n", + "\n", + "# Shuffle and batch datasets for training\n", + "train_ds = (\n", + " train_ds.shuffle(\n", + " buffer_size=buffer_size,\n", + " reshuffle_each_iteration=True,\n", + " )\n", + " .batch(\n", + " batch_size=batch_size,\n", + " drop_remainder=False,\n", + " num_parallel_calls=tf.data.AUTOTUNE,\n", + " )\n", + " .prefetch(buffer_size=tf.data.AUTOTUNE)\n", + ")\n", + "val_ds = val_ds.batch(\n", + " batch_size=batch_size,\n", + " drop_remainder=True,\n", + " num_parallel_calls=tf.data.AUTOTUNE,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(1024, 800, 1) (1024, 800, 1)\n" + ] + } + ], + "source": [ + "x, y = next(iter(val_ds))\n", + "print(x.shape, y.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "inputs = keras.Input(shape=(frame_size, 1), name=\"input\")\n", + "\n", + "encoder_params=dict(\n", + " input_filters=24,\n", + " input_kernel_size=(1, 9),\n", + " input_strides=(1, 2),\n", + " blocks=[\n", + " dict(filters=32, depth=2, kernel_size=(1, 9), strides=(1, 2), ex_ratio=1, se_ratio=4, norm=\"layer\"),\n", + " dict(filters=48, depth=2, kernel_size=(1, 9), strides=(1, 2), ex_ratio=1, se_ratio=4, norm=\"layer\"),\n", + " dict(filters=64, depth=2, kernel_size=(1, 9), strides=(1, 2), ex_ratio=1, se_ratio=4, norm=\"layer\"),\n", + " dict(filters=80, depth=1, kernel_size=(1, 9), strides=(1, 2), ex_ratio=1, se_ratio=4, norm=\"layer\"),\n", + " dict(filters=96, depth=1, kernel_size=(1, 9), strides=(1, 2), ex_ratio=1, se_ratio=4, norm=\"layer\"),\n", + " ],\n", + " output_filters=projection_width,\n", + " include_top=True,\n", + ")\n", + "\n", + "encoder = nse.models.efficientnet.efficientnetv2_from_object(\n", + " x=inputs,\n", + " params=encoder_params,\n", + " num_classes=None\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
INFO     Model: \"EfficientNetV2\"                                                               summary_utils.py:380\n",
+       "         ┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓                              \n",
+       "         ┃ Layer (type)        ┃ Output Shape      ┃    Param # ┃ Connected to      ┃                              \n",
+       "         ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩                              \n",
+       "         │ input (InputLayer)(None, 800, 1)0 │ -                 │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ reshape (Reshape)(None, 1, 800, 1)0 │ input[0][0]                    \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stem.conv (Conv2D)(None, 1, 400,    │        216 │ reshape[0][0]                    \n",
+       "         │                     │ 24)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stem.bn             │ (None, 1, 400,    │         96 │ stem.conv[0][0]                    \n",
+       "         │ (BatchNormalizatio… │ 24)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stem.act            │ (None, 1, 400,    │          0 │ stem.bn[0][0]                    \n",
+       "         │ (Activation)24)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.dp   │ (None, 1, 400,    │        216 │ stem.act[0][0]                    \n",
+       "         │ (DepthwiseConv2D)24)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.dp.… │ (None, 1, 400,    │         96 │ stage1.mbconv1.d… │                              \n",
+       "         │ (BatchNormalizatio… │ 24)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.dp.… │ (None, 1, 400,    │          0 │ stage1.mbconv1.d… │                              \n",
+       "         │ (Activation)24)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ max_pooling2d       │ (None, 1, 200,    │          0 │ stage1.mbconv1.d… │                              \n",
+       "         │ (MaxPooling2D)24)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.se.… │ (None, 1, 1, 24)0 │ max_pooling2d[0]… │                              \n",
+       "         │ (GlobalAveragePool… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.se.… │ (None, 1, 1, 6)150 │ stage1.mbconv1.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.se.… │ (None, 1, 1, 6)0 │ stage1.mbconv1.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.se.… │ (None, 1, 1, 24)168 │ stage1.mbconv1.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.se.… │ (None, 1, 1, 24)0 │ stage1.mbconv1.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ multiply (Multiply)(None, 1, 200,    │          0 │ max_pooling2d[0]… │                              \n",
+       "         │                     │ 24)               │            │ stage1.mbconv1.s… │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.red… │ (None, 1, 200,    │        768 │ multiply[0][0]                    \n",
+       "         │ (Conv2D)32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.red… │ (None, 1, 200,    │        128 │ stage1.mbconv1.r… │                              \n",
+       "         │ (BatchNormalizatio… │ 32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.dp   │ (None, 1, 200,    │        288 │ stage1.mbconv1.r… │                              \n",
+       "         │ (DepthwiseConv2D)32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.dp.… │ (None, 1, 200,    │        128 │ stage1.mbconv2.d… │                              \n",
+       "         │ (BatchNormalizatio… │ 32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.dp.… │ (None, 1, 200,    │          0 │ stage1.mbconv2.d… │                              \n",
+       "         │ (Activation)32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.se.… │ (None, 1, 1, 32)0 │ stage1.mbconv2.d… │                              \n",
+       "         │ (GlobalAveragePool… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.se.… │ (None, 1, 1, 8)264 │ stage1.mbconv2.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.se.… │ (None, 1, 1, 8)0 │ stage1.mbconv2.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.se.… │ (None, 1, 1, 32)288 │ stage1.mbconv2.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.se.… │ (None, 1, 1, 32)0 │ stage1.mbconv2.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ multiply_1          │ (None, 1, 200,    │          0 │ stage1.mbconv2.d… │                              \n",
+       "         │ (Multiply)32)               │            │ stage1.mbconv2.s… │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.red… │ (None, 1, 200,    │      1,024 │ multiply_1[0][0]                    \n",
+       "         │ (Conv2D)32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.red… │ (None, 1, 200,    │        128 │ stage1.mbconv2.r… │                              \n",
+       "         │ (BatchNormalizatio… │ 32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ dropout (Dropout)(None, 1, 200,    │          0 │ stage1.mbconv2.r… │                              \n",
+       "         │                     │ 32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.res  │ (None, 1, 200,    │          0 │ stage1.mbconv1.r… │                              \n",
+       "         │ (Add)32)               │            │ dropout[0][0]                    \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.dp   │ (None, 1, 200,    │        288 │ stage1.mbconv2.r… │                              \n",
+       "         │ (DepthwiseConv2D)32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.dp.… │ (None, 1, 200,    │        128 │ stage2.mbconv1.d… │                              \n",
+       "         │ (BatchNormalizatio… │ 32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.dp.… │ (None, 1, 200,    │          0 │ stage2.mbconv1.d… │                              \n",
+       "         │ (Activation)32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ max_pooling2d_1     │ (None, 1, 100,    │          0 │ stage2.mbconv1.d… │                              \n",
+       "         │ (MaxPooling2D)32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.se.… │ (None, 1, 1, 32)0 │ max_pooling2d_1[… │                              \n",
+       "         │ (GlobalAveragePool… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.se.… │ (None, 1, 1, 8)264 │ stage2.mbconv1.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.se.… │ (None, 1, 1, 8)0 │ stage2.mbconv1.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.se.… │ (None, 1, 1, 32)288 │ stage2.mbconv1.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.se.… │ (None, 1, 1, 32)0 │ stage2.mbconv1.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ multiply_2          │ (None, 1, 100,    │          0 │ max_pooling2d_1[… │                              \n",
+       "         │ (Multiply)32)               │            │ stage2.mbconv1.s… │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.red… │ (None, 1, 100,    │      1,536 │ multiply_2[0][0]                    \n",
+       "         │ (Conv2D)48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.red… │ (None, 1, 100,    │        192 │ stage2.mbconv1.r… │                              \n",
+       "         │ (BatchNormalizatio… │ 48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.dp   │ (None, 1, 100,    │        432 │ stage2.mbconv1.r… │                              \n",
+       "         │ (DepthwiseConv2D)48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.dp.… │ (None, 1, 100,    │        192 │ stage2.mbconv2.d… │                              \n",
+       "         │ (BatchNormalizatio… │ 48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.dp.… │ (None, 1, 100,    │          0 │ stage2.mbconv2.d… │                              \n",
+       "         │ (Activation)48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.se.… │ (None, 1, 1, 48)0 │ stage2.mbconv2.d… │                              \n",
+       "         │ (GlobalAveragePool… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.se.… │ (None, 1, 1, 12)588 │ stage2.mbconv2.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.se.… │ (None, 1, 1, 12)0 │ stage2.mbconv2.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.se.… │ (None, 1, 1, 48)624 │ stage2.mbconv2.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.se.… │ (None, 1, 1, 48)0 │ stage2.mbconv2.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ multiply_3          │ (None, 1, 100,    │          0 │ stage2.mbconv2.d… │                              \n",
+       "         │ (Multiply)48)               │            │ stage2.mbconv2.s… │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.red… │ (None, 1, 100,    │      2,304 │ multiply_3[0][0]                    \n",
+       "         │ (Conv2D)48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.red… │ (None, 1, 100,    │        192 │ stage2.mbconv2.r… │                              \n",
+       "         │ (BatchNormalizatio… │ 48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ dropout_1 (Dropout)(None, 1, 100,    │          0 │ stage2.mbconv2.r… │                              \n",
+       "         │                     │ 48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.res  │ (None, 1, 100,    │          0 │ stage2.mbconv1.r… │                              \n",
+       "         │ (Add)48)               │            │ dropout_1[0][0]                    \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv1.dp   │ (None, 1, 100,    │        432 │ stage2.mbconv2.r… │                              \n",
+       "         │ (DepthwiseConv2D)48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv1.dp.… │ (None, 1, 100,    │        192 │ stage3.mbconv1.d… │                              \n",
+       "         │ (BatchNormalizatio… │ 48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv1.dp.… │ (None, 1, 100,    │          0 │ stage3.mbconv1.d… │                              \n",
+       "         │ (Activation)48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ max_pooling2d_2     │ (None, 1, 50, 48)0 │ stage3.mbconv1.d… │                              \n",
+       "         │ (MaxPooling2D)      │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv1.se.… │ (None, 1, 1, 48)0 │ max_pooling2d_2[… │                              \n",
+       "         │ (GlobalAveragePool… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv1.se.… │ (None, 1, 1, 12)588 │ stage3.mbconv1.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv1.se.… │ (None, 1, 1, 12)0 │ stage3.mbconv1.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv1.se.… │ (None, 1, 1, 48)624 │ stage3.mbconv1.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv1.se.… │ (None, 1, 1, 48)0 │ stage3.mbconv1.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ multiply_4          │ (None, 1, 50, 48)0 │ max_pooling2d_2[… │                              \n",
+       "         │ (Multiply)          │                   │            │ stage3.mbconv1.s… │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv1.red… │ (None, 1, 50, 64)3,072 │ multiply_4[0][0]                    \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv1.red… │ (None, 1, 50, 64)256 │ stage3.mbconv1.r… │                              \n",
+       "         │ (BatchNormalizatio… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv2.dp   │ (None, 1, 50, 64)576 │ stage3.mbconv1.r… │                              \n",
+       "         │ (DepthwiseConv2D)   │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv2.dp.… │ (None, 1, 50, 64)256 │ stage3.mbconv2.d… │                              \n",
+       "         │ (BatchNormalizatio… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv2.dp.… │ (None, 1, 50, 64)0 │ stage3.mbconv2.d… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv2.se.… │ (None, 1, 1, 64)0 │ stage3.mbconv2.d… │                              \n",
+       "         │ (GlobalAveragePool… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv2.se.… │ (None, 1, 1, 16)1,040 │ stage3.mbconv2.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv2.se.… │ (None, 1, 1, 16)0 │ stage3.mbconv2.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv2.se.… │ (None, 1, 1, 64)1,088 │ stage3.mbconv2.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv2.se.… │ (None, 1, 1, 64)0 │ stage3.mbconv2.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ multiply_5          │ (None, 1, 50, 64)0 │ stage3.mbconv2.d… │                              \n",
+       "         │ (Multiply)          │                   │            │ stage3.mbconv2.s… │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv2.red… │ (None, 1, 50, 64)4,096 │ multiply_5[0][0]                    \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv2.red… │ (None, 1, 50, 64)256 │ stage3.mbconv2.r… │                              \n",
+       "         │ (BatchNormalizatio… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ dropout_2 (Dropout)(None, 1, 50, 64)0 │ stage3.mbconv2.r… │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage3.mbconv2.res  │ (None, 1, 50, 64)0 │ stage3.mbconv1.r… │                              \n",
+       "         │ (Add)               │                   │            │ dropout_2[0][0]                    \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage4.mbconv1.dp   │ (None, 1, 50, 64)576 │ stage3.mbconv2.r… │                              \n",
+       "         │ (DepthwiseConv2D)   │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage4.mbconv1.dp.… │ (None, 1, 50, 64)256 │ stage4.mbconv1.d… │                              \n",
+       "         │ (BatchNormalizatio… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage4.mbconv1.dp.… │ (None, 1, 50, 64)0 │ stage4.mbconv1.d… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ max_pooling2d_3     │ (None, 1, 25, 64)0 │ stage4.mbconv1.d… │                              \n",
+       "         │ (MaxPooling2D)      │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage4.mbconv1.se.… │ (None, 1, 1, 64)0 │ max_pooling2d_3[… │                              \n",
+       "         │ (GlobalAveragePool… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage4.mbconv1.se.… │ (None, 1, 1, 16)1,040 │ stage4.mbconv1.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage4.mbconv1.se.… │ (None, 1, 1, 16)0 │ stage4.mbconv1.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage4.mbconv1.se.… │ (None, 1, 1, 64)1,088 │ stage4.mbconv1.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage4.mbconv1.se.… │ (None, 1, 1, 64)0 │ stage4.mbconv1.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ multiply_6          │ (None, 1, 25, 64)0 │ max_pooling2d_3[… │                              \n",
+       "         │ (Multiply)          │                   │            │ stage4.mbconv1.s… │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage4.mbconv1.red… │ (None, 1, 25, 80)5,120 │ multiply_6[0][0]                    \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage4.mbconv1.red… │ (None, 1, 25, 80)320 │ stage4.mbconv1.r… │                              \n",
+       "         │ (BatchNormalizatio… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage5.mbconv1.dp   │ (None, 1, 25, 80)720 │ stage4.mbconv1.r… │                              \n",
+       "         │ (DepthwiseConv2D)   │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage5.mbconv1.dp.… │ (None, 1, 25, 80)320 │ stage5.mbconv1.d… │                              \n",
+       "         │ (BatchNormalizatio… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage5.mbconv1.dp.… │ (None, 1, 25, 80)0 │ stage5.mbconv1.d… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ max_pooling2d_4     │ (None, 1, 13, 80)0 │ stage5.mbconv1.d… │                              \n",
+       "         │ (MaxPooling2D)      │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage5.mbconv1.se.… │ (None, 1, 1, 80)0 │ max_pooling2d_4[… │                              \n",
+       "         │ (GlobalAveragePool… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage5.mbconv1.se.… │ (None, 1, 1, 20)1,620 │ stage5.mbconv1.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage5.mbconv1.se.… │ (None, 1, 1, 20)0 │ stage5.mbconv1.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage5.mbconv1.se.… │ (None, 1, 1, 80)1,680 │ stage5.mbconv1.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage5.mbconv1.se.… │ (None, 1, 1, 80)0 │ stage5.mbconv1.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ multiply_7          │ (None, 1, 13, 80)0 │ max_pooling2d_4[… │                              \n",
+       "         │ (Multiply)          │                   │            │ stage5.mbconv1.s… │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage5.mbconv1.red… │ (None, 1, 13, 96)7,680 │ multiply_7[0][0]                    \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage5.mbconv1.red… │ (None, 1, 13, 96)384 │ stage5.mbconv1.r… │                              \n",
+       "         │ (BatchNormalizatio… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ neck.conv (Conv2D)(None, 1, 13,     │     12,288 │ stage5.mbconv1.r… │                              \n",
+       "         │                     │ 128)              │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ neck.bn             │ (None, 1, 13,     │        512 │ neck.conv[0][0]                    \n",
+       "         │ (BatchNormalizatio… │ 128)              │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ neck.act            │ (None, 1, 13,     │          0 │ neck.bn[0][0]                    \n",
+       "         │ (Activation)128)              │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ top.pool            │ (None, 128)0 │ neck.act[0][0]                    \n",
+       "         │ (GlobalAveragePool… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ dropout_3 (Dropout)(None, 128)0 │ top.pool[0][0]                    \n",
+       "         └─────────────────────┴───────────────────┴────────────┴───────────────────┘                              \n",
+       "          Total params: 57,066 (222.91 KB)                                                                         \n",
+       "          Trainable params: 55,050 (215.04 KB)                                                                     \n",
+       "          Non-trainable params: 2,016 (7.88 KB)                                                                    \n",
+       "                                                                                                                   \n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34mINFO \u001b[0m Model: \u001b[32m\"EfficientNetV2\"\u001b[0m \u001b]8;id=405106;file:///workspaces/heartkit/.venv/lib/python3.12/site-packages/keras/src/utils/summary_utils.py\u001b\\\u001b[2msummary_utils.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=760327;file:///workspaces/heartkit/.venv/lib/python3.12/site-packages/keras/src/utils/summary_utils.py#380\u001b\\\u001b[2m380\u001b[0m\u001b]8;;\u001b\\\n", + " ┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ \u001b[2m \u001b[0m\n", + " ┃ Layer \u001b[1m(\u001b[0mtype\u001b[1m)\u001b[0m ┃ Output Shape ┃ Param # ┃ Connected to ┃ \u001b[2m \u001b[0m\n", + " ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ \u001b[2m \u001b[0m\n", + " │ input \u001b[1m(\u001b[0mInputLayer\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m800\u001b[0m, \u001b[1;36m1\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ - │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ reshape \u001b[1m(\u001b[0mReshape\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m800\u001b[0m, \u001b[1;36m1\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ input\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stem.conv \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m400\u001b[0m, │ \u001b[1;36m216\u001b[0m │ reshape\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stem.bn │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m400\u001b[0m, │ \u001b[1;36m96\u001b[0m │ stem.conv\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stem.act │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m400\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stem.bn\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.dp │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m400\u001b[0m, │ \u001b[1;36m216\u001b[0m │ stem.act\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mDepthwiseConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m400\u001b[0m, │ \u001b[1;36m96\u001b[0m │ stage1.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m400\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage1.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ max_pooling2d │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage1.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMaxPooling2D\u001b[1m)\u001b[0m │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ max_pooling2d\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mGlobalAveragePool… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m6\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m150\u001b[0m │ stage1.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m6\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage1.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m168\u001b[0m │ stage1.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage1.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ multiply \u001b[1m(\u001b[0mMultiply\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m0\u001b[0m │ max_pooling2d\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m… │ \u001b[2m \u001b[0m\n", + " │ │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ stage1.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m768\u001b[0m │ multiply\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m128\u001b[0m │ stage1.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.dp │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m288\u001b[0m │ stage1.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mDepthwiseConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m128\u001b[0m │ stage1.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage1.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage1.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mGlobalAveragePool… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m8\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m264\u001b[0m │ stage1.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m8\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage1.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m288\u001b[0m │ stage1.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage1.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ multiply_1 │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage1.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMultiply\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ stage1.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m1\u001b[0m,\u001b[1;36m024\u001b[0m │ multiply_1\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m128\u001b[0m │ stage1.mbconv2.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ dropout \u001b[1m(\u001b[0mDropout\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage1.mbconv2.r… │ \u001b[2m \u001b[0m\n", + " │ │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.res │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage1.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mAdd\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ dropout\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.dp │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m288\u001b[0m │ stage1.mbconv2.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mDepthwiseConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m128\u001b[0m │ stage2.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage2.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ max_pooling2d_1 │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage2.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMaxPooling2D\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ max_pooling2d_1\u001b[1m[\u001b[0m… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mGlobalAveragePool… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m8\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m264\u001b[0m │ stage2.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m8\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage2.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m288\u001b[0m │ stage2.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage2.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ multiply_2 │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m0\u001b[0m │ max_pooling2d_1\u001b[1m[\u001b[0m… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMultiply\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ stage2.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m1\u001b[0m,\u001b[1;36m536\u001b[0m │ multiply_2\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m192\u001b[0m │ stage2.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.dp │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m432\u001b[0m │ stage2.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mDepthwiseConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m192\u001b[0m │ stage2.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage2.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage2.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mGlobalAveragePool… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m12\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m588\u001b[0m │ stage2.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m12\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage2.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m624\u001b[0m │ stage2.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage2.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ multiply_3 │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage2.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMultiply\u001b[1m)\u001b[0m │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ stage2.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m2\u001b[0m,\u001b[1;36m304\u001b[0m │ multiply_3\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m192\u001b[0m │ stage2.mbconv2.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ dropout_1 \u001b[1m(\u001b[0mDropout\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage2.mbconv2.r… │ \u001b[2m \u001b[0m\n", + " │ │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.res │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage2.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mAdd\u001b[1m)\u001b[0m │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ dropout_1\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv1.dp │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m432\u001b[0m │ stage2.mbconv2.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mDepthwiseConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv1.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m192\u001b[0m │ stage3.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv1.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage3.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ max_pooling2d_2 │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m50\u001b[0m, \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage3.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMaxPooling2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ max_pooling2d_2\u001b[1m[\u001b[0m… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mGlobalAveragePool… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m12\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m588\u001b[0m │ stage3.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m12\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage3.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m624\u001b[0m │ stage3.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage3.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ multiply_4 │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m50\u001b[0m, \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ max_pooling2d_2\u001b[1m[\u001b[0m… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMultiply\u001b[1m)\u001b[0m │ │ │ stage3.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv1.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m50\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m3\u001b[0m,\u001b[1;36m072\u001b[0m │ multiply_4\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv1.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m50\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m256\u001b[0m │ stage3.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv2.dp │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m50\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m576\u001b[0m │ stage3.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mDepthwiseConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv2.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m50\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m256\u001b[0m │ stage3.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv2.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m50\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage3.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage3.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mGlobalAveragePool… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m16\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m1\u001b[0m,\u001b[1;36m040\u001b[0m │ stage3.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m16\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage3.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m1\u001b[0m,\u001b[1;36m088\u001b[0m │ stage3.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage3.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ multiply_5 │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m50\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage3.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMultiply\u001b[1m)\u001b[0m │ │ │ stage3.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv2.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m50\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m4\u001b[0m,\u001b[1;36m096\u001b[0m │ multiply_5\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv2.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m50\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m256\u001b[0m │ stage3.mbconv2.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ dropout_2 \u001b[1m(\u001b[0mDropout\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m50\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage3.mbconv2.r… │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage3.mbconv2.res │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m50\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage3.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mAdd\u001b[1m)\u001b[0m │ │ │ dropout_2\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage4.mbconv1.dp │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m50\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m576\u001b[0m │ stage3.mbconv2.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mDepthwiseConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage4.mbconv1.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m50\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m256\u001b[0m │ stage4.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage4.mbconv1.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m50\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage4.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ max_pooling2d_3 │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m25\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage4.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMaxPooling2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage4.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ max_pooling2d_3\u001b[1m[\u001b[0m… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mGlobalAveragePool… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage4.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m16\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m1\u001b[0m,\u001b[1;36m040\u001b[0m │ stage4.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage4.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m16\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage4.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage4.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m1\u001b[0m,\u001b[1;36m088\u001b[0m │ stage4.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage4.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage4.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ multiply_6 │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m25\u001b[0m, \u001b[1;36m64\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ max_pooling2d_3\u001b[1m[\u001b[0m… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMultiply\u001b[1m)\u001b[0m │ │ │ stage4.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage4.mbconv1.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m25\u001b[0m, \u001b[1;36m80\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m5\u001b[0m,\u001b[1;36m120\u001b[0m │ multiply_6\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage4.mbconv1.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m25\u001b[0m, \u001b[1;36m80\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m320\u001b[0m │ stage4.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage5.mbconv1.dp │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m25\u001b[0m, \u001b[1;36m80\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m720\u001b[0m │ stage4.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mDepthwiseConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage5.mbconv1.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m25\u001b[0m, \u001b[1;36m80\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m320\u001b[0m │ stage5.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage5.mbconv1.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m25\u001b[0m, \u001b[1;36m80\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage5.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ max_pooling2d_4 │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m13\u001b[0m, \u001b[1;36m80\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage5.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMaxPooling2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage5.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m80\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ max_pooling2d_4\u001b[1m[\u001b[0m… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mGlobalAveragePool… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage5.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m20\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m1\u001b[0m,\u001b[1;36m620\u001b[0m │ stage5.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage5.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m20\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage5.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage5.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m80\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m1\u001b[0m,\u001b[1;36m680\u001b[0m │ stage5.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage5.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m80\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage5.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ multiply_7 │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m13\u001b[0m, \u001b[1;36m80\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ max_pooling2d_4\u001b[1m[\u001b[0m… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMultiply\u001b[1m)\u001b[0m │ │ │ stage5.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage5.mbconv1.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m13\u001b[0m, \u001b[1;36m96\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m7\u001b[0m,\u001b[1;36m680\u001b[0m │ multiply_7\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage5.mbconv1.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m13\u001b[0m, \u001b[1;36m96\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m384\u001b[0m │ stage5.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ neck.conv \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m13\u001b[0m, │ \u001b[1;36m12\u001b[0m,\u001b[1;36m288\u001b[0m │ stage5.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ │ \u001b[1;36m128\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ neck.bn │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m13\u001b[0m, │ \u001b[1;36m512\u001b[0m │ neck.conv\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m128\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ neck.act │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m13\u001b[0m, │ \u001b[1;36m0\u001b[0m │ neck.bn\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ \u001b[1;36m128\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ top.pool │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m128\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ neck.act\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mGlobalAveragePool… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ dropout_3 \u001b[1m(\u001b[0mDropout\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m128\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ top.pool\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " └─────────────────────┴───────────────────┴────────────┴───────────────────┘ \u001b[2m \u001b[0m\n", + " Total params: \u001b[1;36m57\u001b[0m,\u001b[1;36m066\u001b[0m \u001b[1m(\u001b[0m\u001b[1;36m222.91\u001b[0m KB\u001b[1m)\u001b[0m \u001b[2m \u001b[0m\n", + " Trainable params: \u001b[1;36m55\u001b[0m,\u001b[1;36m050\u001b[0m \u001b[1m(\u001b[0m\u001b[1;36m215.04\u001b[0m KB\u001b[1m)\u001b[0m \u001b[2m \u001b[0m\n", + " Non-trainable params: \u001b[1;36m2\u001b[0m,\u001b[1;36m016\u001b[0m \u001b[1m(\u001b[0m\u001b[1;36m7.88\u001b[0m KB\u001b[1m)\u001b[0m \u001b[2m \u001b[0m\n", + " \u001b[2m \u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
INFO     Computation: 4.17 MFLOPs                                                                    404182745.py:3\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34mINFO \u001b[0m Computation: \u001b[1;36m4.17\u001b[0m MFLOPs \u001b]8;id=210027;file:///tmp/ipykernel_1440105/404182745.py\u001b\\\u001b[2m404182745.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=343897;file:///tmp/ipykernel_1440105/404182745.py#3\u001b\\\u001b[2m3\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "encoder.summary(print_fn=logger.info)\n", + "flops = nse.metrics.flops.get_flops(encoder, batch_size=1, fpath=os.devnull)\n", + "logger.info(f\"Computation: {flops/1e6:0.2f} MFLOPs\")\n", + "encoder_output = encoder(inputs)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
INFO     Model: \"projector\"                                                                    summary_utils.py:380\n",
+       "         ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓                              \n",
+       "         ┃ Layer (type)                    ┃ Output Shape           ┃       Param # ┃                              \n",
+       "         ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩                              \n",
+       "         │ keras_tensor_108CLONE           │ (None, 128)0                    \n",
+       "         │ (InputLayer)                    │                        │               │                              \n",
+       "         ├─────────────────────────────────┼────────────────────────┼───────────────┤                              \n",
+       "         │ dense (Dense)(None, 128)16,512                    \n",
+       "         ├─────────────────────────────────┼────────────────────────┼───────────────┤                              \n",
+       "         │ dense_1 (Dense)(None, 128)16,512                    \n",
+       "         └─────────────────────────────────┴────────────────────────┴───────────────┘                              \n",
+       "          Total params: 33,024 (129.00 KB)                                                                         \n",
+       "          Trainable params: 33,024 (129.00 KB)                                                                     \n",
+       "          Non-trainable params: 0 (0.00 B)                                                                         \n",
+       "                                                                                                                   \n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34mINFO \u001b[0m Model: \u001b[32m\"projector\"\u001b[0m \u001b]8;id=220129;file:///workspaces/heartkit/.venv/lib/python3.12/site-packages/keras/src/utils/summary_utils.py\u001b\\\u001b[2msummary_utils.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=797710;file:///workspaces/heartkit/.venv/lib/python3.12/site-packages/keras/src/utils/summary_utils.py#380\u001b\\\u001b[2m380\u001b[0m\u001b]8;;\u001b\\\n", + " ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ \u001b[2m \u001b[0m\n", + " ┃ Layer \u001b[1m(\u001b[0mtype\u001b[1m)\u001b[0m ┃ Output Shape ┃ Param # ┃ \u001b[2m \u001b[0m\n", + " ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ \u001b[2m \u001b[0m\n", + " │ keras_tensor_108CLONE │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m128\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mInputLayer\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────────────────┼────────────────────────┼───────────────┤ \u001b[2m \u001b[0m\n", + " │ dense \u001b[1m(\u001b[0mDense\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m128\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m16\u001b[0m,\u001b[1;36m512\u001b[0m │ \u001b[2m \u001b[0m\n", + " ├─────────────────────────────────┼────────────────────────┼───────────────┤ \u001b[2m \u001b[0m\n", + " │ dense_1 \u001b[1m(\u001b[0mDense\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m128\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m16\u001b[0m,\u001b[1;36m512\u001b[0m │ \u001b[2m \u001b[0m\n", + " └─────────────────────────────────┴────────────────────────┴───────────────┘ \u001b[2m \u001b[0m\n", + " Total params: \u001b[1;36m33\u001b[0m,\u001b[1;36m024\u001b[0m \u001b[1m(\u001b[0m\u001b[1;36m129.00\u001b[0m KB\u001b[1m)\u001b[0m \u001b[2m \u001b[0m\n", + " Trainable params: \u001b[1;36m33\u001b[0m,\u001b[1;36m024\u001b[0m \u001b[1m(\u001b[0m\u001b[1;36m129.00\u001b[0m KB\u001b[1m)\u001b[0m \u001b[2m \u001b[0m\n", + " Non-trainable params: \u001b[1;36m0\u001b[0m \u001b[1m(\u001b[0m\u001b[1;36m0.00\u001b[0m B\u001b[1m)\u001b[0m \u001b[2m \u001b[0m\n", + " \u001b[2m \u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
DEBUG    Projector requires 0.07 MFLOPS                                                             2487210472.py:7\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[32mDEBUG \u001b[0m Projector requires \u001b[1;36m0.07\u001b[0m MFLOPS \u001b]8;id=912478;file:///tmp/ipykernel_1440105/2487210472.py\u001b\\\u001b[2m2487210472.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=794178;file:///tmp/ipykernel_1440105/2487210472.py#7\u001b\\\u001b[2m7\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "projector_input = encoder_output\n", + "projector_output = keras.layers.Dense(projection_width, activation=\"relu6\")(projector_input)\n", + "projector_output = keras.layers.Dense(projection_width)(projector_output)\n", + "projector = keras.Model(inputs=projector_input, outputs=projector_output, name=\"projector\")\n", + "flops = nse.metrics.flops.get_flops(projector, batch_size=1, fpath=os.devnull)\n", + "projector.summary(print_fn=logger.info)\n", + "logger.debug(f\"Projector requires {flops/1e6:0.2f} MFLOPS\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "model = SimCLR(\n", + " contrastive_augmenter=lambda x: x,\n", + " encoder=encoder,\n", + " projector=projector,\n", + " # momentum_coeff=0.999,\n", + " temperature=temperature,\n", + " # queue_size=65536,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "def get_scheduler():\n", + " return keras.optimizers.schedules.CosineDecay(\n", + " initial_learning_rate=learning_rate,\n", + " decay_steps=steps_per_epoch * epochs,\n", + " )\n", + "\n", + "model.compile(\n", + " contrastive_optimizer=keras.optimizers.Adam(get_scheduler()),\n", + " probe_optimizer=keras.optimizers.Adam(get_scheduler()),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1/100\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING: All log messages before absl::InitializeLog() is called are written to STDERR\n", + "I0000 00:00:1721671560.218993 1440263 service.cc:146] XLA service 0x74f6300176b0 initialized for platform CUDA (this does not guarantee that XLA will be used). Devices:\n", + "I0000 00:00:1721671560.219066 1440263 service.cc:154] StreamExecutor device (0): NVIDIA GeForce RTX 4090, Compute Capability 8.9\n", + "I0000 00:00:1721671584.509272 1440263 device_compiler.h:188] Compiled cluster using XLA! This line is logged at most once for the lifetime of the process.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Epoch 1: val_loss improved from inf to 6.92869, saving model to /tmp/hk-foundation/model.keras\n", + "25/25 - 240s - 10s/step - c_acc: 0.0020 - loss: 6.9286 - r_acc: 0.0463 - val_c_acc: 9.7656e-04 - val_loss: 6.9287 - val_r_acc: 0.0352\n", + "Epoch 2/100\n", + "\n", + "Epoch 2: val_loss did not improve from 6.92869\n", + "25/25 - 176s - 7s/step - c_acc: 9.7656e-04 - loss: 6.9301 - r_acc: 0.0358 - val_c_acc: 9.7656e-04 - val_loss: 6.9301 - val_r_acc: 0.0352\n", + "Epoch 3/100\n", + "\n", + "Epoch 3: val_loss did not improve from 6.92869\n", + "25/25 - 181s - 7s/step - c_acc: 9.7656e-04 - loss: 6.9305 - r_acc: 0.0344 - val_c_acc: 9.7656e-04 - val_loss: 6.9305 - val_r_acc: 0.0312\n", + "Epoch 4/100\n", + "\n", + "Epoch 4: val_loss did not improve from 6.92869\n", + "25/25 - 178s - 7s/step - c_acc: 9.7656e-04 - loss: 6.9308 - r_acc: 0.0352 - val_c_acc: 9.7656e-04 - val_loss: 6.9308 - val_r_acc: 0.0312\n", + "Epoch 5/100\n", + "\n", + "Epoch 5: val_loss did not improve from 6.92869\n", + "25/25 - 173s - 7s/step - c_acc: 9.9609e-04 - loss: 6.9309 - r_acc: 0.0369 - val_c_acc: 9.7656e-04 - val_loss: 6.9309 - val_r_acc: 0.0312\n", + "Epoch 6/100\n", + "\n", + "Epoch 6: val_loss did not improve from 6.92869\n", + "25/25 - 172s - 7s/step - c_acc: 9.7656e-04 - loss: 6.9310 - r_acc: 0.0358 - val_c_acc: 9.7656e-04 - val_loss: 6.9310 - val_r_acc: 0.0312\n", + "Epoch 7/100\n", + "\n", + "Epoch 7: val_loss did not improve from 6.92869\n", + "25/25 - 173s - 7s/step - c_acc: 9.9609e-04 - loss: 6.9311 - r_acc: 0.0342 - val_c_acc: 9.7656e-04 - val_loss: 6.9311 - val_r_acc: 0.0312\n", + "Epoch 8/100\n", + "\n", + "Epoch 8: val_loss did not improve from 6.92869\n", + "25/25 - 172s - 7s/step - c_acc: 9.9609e-04 - loss: 6.9311 - r_acc: 0.0362 - val_c_acc: 9.7656e-04 - val_loss: 6.9311 - val_r_acc: 0.0312\n", + "Epoch 9/100\n", + "\n", + "Epoch 9: val_loss did not improve from 6.92869\n", + "25/25 - 172s - 7s/step - c_acc: 9.7656e-04 - loss: 6.9312 - r_acc: 0.0347 - val_c_acc: 9.7656e-04 - val_loss: 6.9312 - val_r_acc: 0.0312\n", + "Epoch 10/100\n", + "\n", + "Epoch 10: val_loss did not improve from 6.92869\n", + "25/25 - 172s - 7s/step - c_acc: 9.7656e-04 - loss: 6.9312 - r_acc: 0.0347 - val_c_acc: 9.7656e-04 - val_loss: 6.9312 - val_r_acc: 0.0391\n", + "Epoch 11/100\n", + "\n", + "Epoch 11: val_loss did not improve from 6.92869\n", + "25/25 - 173s - 7s/step - c_acc: 9.7656e-04 - loss: 6.9312 - r_acc: 0.0312 - val_c_acc: 9.7656e-04 - val_loss: 6.9312 - val_r_acc: 0.0312\n", + "Epoch 12/100\n", + "\n", + "Epoch 12: val_loss did not improve from 6.92869\n", + "25/25 - 173s - 7s/step - c_acc: 0.0010 - loss: 6.9312 - r_acc: 0.0286 - val_c_acc: 0.0015 - val_loss: 6.9312 - val_r_acc: 0.0312\n", + "Epoch 13/100\n", + "\n", + "Epoch 13: val_loss did not improve from 6.92869\n", + "25/25 - 172s - 7s/step - c_acc: 9.3750e-04 - loss: 6.9313 - r_acc: 0.0280 - val_c_acc: 4.8828e-04 - val_loss: 6.9313 - val_r_acc: 0.0312\n", + "Epoch 14/100\n", + "\n", + "Epoch 14: val_loss did not improve from 6.92869\n", + "25/25 - 173s - 7s/step - c_acc: 0.0010 - loss: 6.9313 - r_acc: 0.0272 - val_c_acc: 0.0015 - val_loss: 6.9313 - val_r_acc: 0.0312\n", + "Epoch 15/100\n", + "\n", + "Epoch 15: val_loss did not improve from 6.92869\n", + "25/25 - 172s - 7s/step - c_acc: 0.0010 - loss: 6.9313 - r_acc: 0.0270 - val_c_acc: 9.7656e-04 - val_loss: 6.9313 - val_r_acc: 0.0312\n", + "Epoch 16/100\n", + "\n", + "Epoch 16: val_loss did not improve from 6.92869\n", + "25/25 - 173s - 7s/step - c_acc: 9.3750e-04 - loss: 6.9313 - r_acc: 0.0267 - val_c_acc: 9.7656e-04 - val_loss: 6.9313 - val_r_acc: 0.0312\n", + "Epoch 17/100\n", + "\n", + "Epoch 17: val_loss did not improve from 6.92869\n", + "25/25 - 172s - 7s/step - c_acc: 9.9609e-04 - loss: 6.9313 - r_acc: 0.0267 - val_c_acc: 9.7656e-04 - val_loss: 6.9313 - val_r_acc: 0.0312\n", + "Epoch 18/100\n", + "\n", + "Epoch 18: val_loss did not improve from 6.92869\n", + "25/25 - 174s - 7s/step - c_acc: 9.7656e-04 - loss: 6.9313 - r_acc: 0.0269 - val_c_acc: 9.7656e-04 - val_loss: 6.9313 - val_r_acc: 0.0312\n", + "Epoch 19/100\n", + "\n", + "Epoch 19: val_loss did not improve from 6.92869\n", + "25/25 - 171s - 7s/step - c_acc: 9.7656e-04 - loss: 6.9313 - r_acc: 0.0264 - val_c_acc: 9.7656e-04 - val_loss: 6.9313 - val_r_acc: 0.0312\n", + "Epoch 20/100\n", + "\n", + "Epoch 20: val_loss did not improve from 6.92869\n", + "25/25 - 174s - 7s/step - c_acc: 0.0011 - loss: 6.9313 - r_acc: 0.0280 - val_c_acc: 9.7656e-04 - val_loss: 6.9313 - val_r_acc: 0.0312\n", + "Epoch 21/100\n", + "\n", + "Epoch 21: val_loss did not improve from 6.92869\n", + "25/25 - 173s - 7s/step - c_acc: 9.3750e-04 - loss: 6.9313 - r_acc: 0.0266 - val_c_acc: 9.7656e-04 - val_loss: 6.9313 - val_r_acc: 0.0312\n", + "Epoch 22/100\n", + "\n", + "Epoch 22: val_loss did not improve from 6.92869\n", + "25/25 - 172s - 7s/step - c_acc: 0.0011 - loss: 6.9313 - r_acc: 0.0264 - val_c_acc: 9.7656e-04 - val_loss: 6.9313 - val_r_acc: 0.0273\n", + "Epoch 23/100\n", + "\n", + "Epoch 23: val_loss did not improve from 6.92869\n", + "25/25 - 172s - 7s/step - c_acc: 0.0011 - loss: 6.9313 - r_acc: 0.0267 - val_c_acc: 9.7656e-04 - val_loss: 6.9313 - val_r_acc: 0.0273\n", + "Epoch 24/100\n", + "\n", + "Epoch 24: val_loss did not improve from 6.92869\n", + "25/25 - 172s - 7s/step - c_acc: 0.0012 - loss: 6.9313 - r_acc: 0.0255 - val_c_acc: 0.0020 - val_loss: 6.9313 - val_r_acc: 0.0234\n", + "Epoch 25/100\n", + "\n", + "Epoch 25: val_loss did not improve from 6.92869\n", + "25/25 - 172s - 7s/step - c_acc: 0.0017 - loss: 6.9307 - r_acc: 0.0131 - val_c_acc: 9.7656e-04 - val_loss: 6.9307 - val_r_acc: 0.0078\n", + "Epoch 26/100\n", + "\n", + "Epoch 26: val_loss did not improve from 6.92869\n", + "25/25 - 172s - 7s/step - c_acc: 9.9609e-04 - loss: 6.9307 - r_acc: 0.0078 - val_c_acc: 9.7656e-04 - val_loss: 6.9307 - val_r_acc: 0.0078\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "val_metric = \"loss\"\n", + "\n", + "model_callbacks = [\n", + " keras.callbacks.EarlyStopping(\n", + " monitor=f\"val_{val_metric}\",\n", + " patience=max(int(0.25 * epochs), 1),\n", + " mode=\"max\" if val_metric == \"f1\" else \"auto\",\n", + " restore_best_weights=True,\n", + " ),\n", + " keras.callbacks.ModelCheckpoint(\n", + " filepath=str(model_file),\n", + " monitor=f\"val_{val_metric}\",\n", + " save_best_only=True,\n", + " mode=\"max\" if val_metric == \"f1\" else \"auto\",\n", + " verbose=1,\n", + " ),\n", + " keras.callbacks.CSVLogger(job_dir / \"history.csv\"),\n", + "]\n", + "if hk.utils.env_flag(\"TENSORBOARD\"):\n", + " model_callbacks.append(\n", + " keras.callbacks.TensorBoard(\n", + " log_dir=job_dir,\n", + " write_steps_per_second=True,\n", + " )\n", + " )\n", + "\n", + "\n", + "model.fit(\n", + " train_ds,\n", + " steps_per_epoch=steps_per_epoch,\n", + " verbose=2,\n", + " epochs=epochs,\n", + " validation_data=val_ds,\n", + " callbacks=model_callbacks,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1/100\n" + ] + }, + { + "ename": "ValueError", + "evalue": "Dimensions must be equal, but are 800 and 128 for '{{node compile_loss/mean_squared_error/sub}} = Sub[T=DT_FLOAT](compile_loss/mean_squared_error/Squeeze, EfficientNetV2_1/dropout_7_1/stateless_dropout/SelectV2)' with input shapes: [?,800], [?,128].", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[23], line 11\u001b[0m\n\u001b[1;32m 8\u001b[0m loss \u001b[38;5;241m=\u001b[39m keras\u001b[38;5;241m.\u001b[39mlosses\u001b[38;5;241m.\u001b[39mMeanSquaredError()\n\u001b[1;32m 9\u001b[0m encoder\u001b[38;5;241m.\u001b[39mcompile(optimizer\u001b[38;5;241m=\u001b[39moptimizer, loss\u001b[38;5;241m=\u001b[39mloss, metrics\u001b[38;5;241m=\u001b[39mmetrics)\n\u001b[0;32m---> 11\u001b[0m \u001b[43mencoder\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfit\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 12\u001b[0m \u001b[43m \u001b[49m\u001b[43mtrain_ds\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 13\u001b[0m \u001b[43m \u001b[49m\u001b[43msteps_per_epoch\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msteps_per_epoch\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 14\u001b[0m \u001b[43m \u001b[49m\u001b[43mverbose\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 15\u001b[0m \u001b[43m \u001b[49m\u001b[43mepochs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mepochs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 16\u001b[0m \u001b[43m \u001b[49m\u001b[43mvalidation_data\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mval_ds\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 17\u001b[0m \u001b[43m \u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmodel_callbacks\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 18\u001b[0m \u001b[43m)\u001b[49m\n", + "File \u001b[0;32m/workspaces/heartkit/.venv/lib/python3.12/site-packages/keras/src/utils/traceback_utils.py:122\u001b[0m, in \u001b[0;36mfilter_traceback..error_handler\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 119\u001b[0m filtered_tb \u001b[38;5;241m=\u001b[39m _process_traceback_frames(e\u001b[38;5;241m.\u001b[39m__traceback__)\n\u001b[1;32m 120\u001b[0m \u001b[38;5;66;03m# To get the full stack trace, call:\u001b[39;00m\n\u001b[1;32m 121\u001b[0m \u001b[38;5;66;03m# `keras.config.disable_traceback_filtering()`\u001b[39;00m\n\u001b[0;32m--> 122\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m e\u001b[38;5;241m.\u001b[39mwith_traceback(filtered_tb) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m 123\u001b[0m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[1;32m 124\u001b[0m \u001b[38;5;28;01mdel\u001b[39;00m filtered_tb\n", + "File \u001b[0;32m/workspaces/heartkit/.venv/lib/python3.12/site-packages/keras/src/losses/losses.py:1286\u001b[0m, in \u001b[0;36mmean_squared_error\u001b[0;34m(y_true, y_pred)\u001b[0m\n\u001b[1;32m 1284\u001b[0m y_true \u001b[38;5;241m=\u001b[39m ops\u001b[38;5;241m.\u001b[39mconvert_to_tensor(y_true, dtype\u001b[38;5;241m=\u001b[39my_pred\u001b[38;5;241m.\u001b[39mdtype)\n\u001b[1;32m 1285\u001b[0m y_true, y_pred \u001b[38;5;241m=\u001b[39m squeeze_or_expand_to_same_rank(y_true, y_pred)\n\u001b[0;32m-> 1286\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m ops\u001b[38;5;241m.\u001b[39mmean(ops\u001b[38;5;241m.\u001b[39msquare(\u001b[43my_true\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[43m \u001b[49m\u001b[43my_pred\u001b[49m), axis\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m)\n", + "\u001b[0;31mValueError\u001b[0m: Dimensions must be equal, but are 800 and 128 for '{{node compile_loss/mean_squared_error/sub}} = Sub[T=DT_FLOAT](compile_loss/mean_squared_error/Squeeze, EfficientNetV2_1/dropout_7_1/stateless_dropout/SelectV2)' with input shapes: [?,800], [?,128]." + ] + } + ], + "source": [ + "metrics = [\n", + " keras.metrics.MeanAbsoluteError(name=\"mae\"),\n", + " keras.metrics.MeanSquaredError(name=\"mse\"),\n", + " keras.metrics.CosineSimilarity(name=\"cosine\"),\n", + "]\n", + "\n", + "optimizer = keras.optimizers.Adam(get_scheduler())\n", + "loss = keras.losses.MeanSquaredError()\n", + "encoder.compile(optimizer=optimizer, loss=loss, metrics=metrics)\n", + "\n", + "encoder.fit(\n", + " train_ds,\n", + " steps_per_epoch=steps_per_epoch,\n", + " verbose=2,\n", + " epochs=epochs,\n", + " validation_data=val_ds,\n", + " callbacks=model_callbacks,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "from abc import abstractmethod\n", + "from typing import Callable\n", + "\n", + "import keras\n", + "import tensorflow as tf\n", + "\n", + "\n", + "class ContrastiveModel(keras.Model):\n", + " \"\"\"Base class for contrastive learning models\"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " encoder: keras.Model,\n", + " projector: keras.Model,\n", + " contrastive_augmenter: Callable[[keras.KerasTensor], keras.KerasTensor] | None = None,\n", + " classification_augmenter: Callable[[keras.KerasTensor], keras.KerasTensor] | None = None,\n", + " linear_probe: keras.Model | None = None,\n", + " ):\n", + " super().__init__()\n", + "\n", + " self.encoder = encoder\n", + " self.projector = projector\n", + " self.contrastive_augmenter = contrastive_augmenter\n", + " self.classification_augmenter = classification_augmenter\n", + " self.linear_probe = linear_probe\n", + "\n", + " self.probe_loss = None\n", + " self.probe_optimizer = None\n", + " self.contrastive_loss_tracker = None\n", + " self.contrastive_optimizer = None\n", + " self.contrastive_accuracy = None\n", + " self.correlation_accuracy = None\n", + " self.probe_accuracy = None\n", + "\n", + " @property\n", + " def metrics(self):\n", + " \"\"\"List of metrics to track during training and evaluation\"\"\"\n", + " return [\n", + " self.contrastive_loss_tracker,\n", + " self.correlation_accuracy,\n", + " self.contrastive_accuracy,\n", + " # self.probe_loss_tracker,\n", + " # self.probe_accuracy,\n", + " ]\n", + "\n", + " @abstractmethod\n", + " def contrastive_loss(self, projections_1, projections_2):\n", + " \"\"\"Contrastive loss function\"\"\"\n", + " raise NotImplementedError()\n", + "\n", + " def call(self, inputs, training=None, mask=None):\n", + " \"\"\"Forward pass through the encoder model\"\"\"\n", + " return self.encoder(inputs, training=training, mask=mask)\n", + "\n", + " # pylint: disable=unused-argument,arguments-differ\n", + " def compile(\n", + " self,\n", + " contrastive_optimizer: keras.optimizers.Optimizer,\n", + " probe_optimizer: keras.optimizers.Optimizer | None = None,\n", + " **kwargs,\n", + " ):\n", + " \"\"\"Compile the model with the specified optimizers\"\"\"\n", + " super().compile(**kwargs)\n", + "\n", + " self.contrastive_optimizer = contrastive_optimizer\n", + " self.probe_optimizer = probe_optimizer\n", + "\n", + " # self.contrastive_loss is a method that will be implemented by the subclasses\n", + " self.probe_loss = keras.losses.SparseCategoricalCrossentropy(from_logits=True)\n", + "\n", + " self.contrastive_loss_tracker = keras.metrics.Mean(name=\"loss\")\n", + " self.contrastive_accuracy = keras.metrics.SparseCategoricalAccuracy(name=\"c_acc\")\n", + " self.correlation_accuracy = keras.metrics.SparseCategoricalAccuracy(name=\"r_acc\")\n", + "\n", + " self.probe_accuracy = keras.metrics.SparseCategoricalAccuracy()\n", + "\n", + " def save(self, filepath, overwrite=True, save_format=None, **kwargs):\n", + " \"\"\"Save the encoder model to file\n", + "\n", + " Args:\n", + " filepath (str): Filepath\n", + " overwrite (bool, optional): Overwrite existing file. Defaults to True.\n", + " save_format ([type], optional): Save format. Defaults to None.\n", + " \"\"\"\n", + " self.encoder.save(filepath, overwrite, save_format, **kwargs)\n", + "\n", + " def reset_metrics(self):\n", + " \"\"\"Reset the metrics to their initial state\"\"\"\n", + " self.contrastive_accuracy.reset_state()\n", + " self.correlation_accuracy.reset_state()\n", + " self.probe_accuracy.reset_state()\n", + "\n", + " def update_contrastive_accuracy(self, features_1, features_2):\n", + " \"\"\"Update the contrastive accuracy metric\n", + " self-supervised metric inspired by the SimCLR loss\n", + " \"\"\"\n", + "\n", + " # cosine similarity: the dot product of the l2-normalized feature vectors\n", + " features_1 = keras.ops.normalize(features_1, axis=1)\n", + " features_2 = keras.ops.normalize(features_2, axis=1)\n", + " similarities = keras.ops.matmul(features_1, keras.ops.transpose(features_2))\n", + "\n", + " # Push positive pairs to the diagonal\n", + " batch_size = keras.ops.shape(features_1)[0]\n", + " contrastive_labels = keras.ops.arange(batch_size)\n", + " self.contrastive_accuracy.update_state(contrastive_labels, similarities)\n", + " self.contrastive_accuracy.update_state(contrastive_labels, keras.ops.transpose(similarities))\n", + "\n", + " def update_correlation_accuracy(self, features_1, features_2):\n", + " \"\"\"Update the correlation accuracy metric\n", + " self-supervised metric inspired by the BarlowTwins loss\n", + " \"\"\"\n", + "\n", + " # normalization so that cross-correlation will be between -1 and 1\n", + " features_1 = (features_1 - keras.ops.mean(features_1, axis=0)) / keras.ops.std(features_1, axis=0)\n", + " features_2 = (features_2 - keras.ops.mean(features_2, axis=0)) / keras.ops.std(features_2, axis=0)\n", + "\n", + " # the cross correlation of image representations should be the identity matrix\n", + " batch_size = keras.ops.shape(features_1)[0]\n", + " batch_size = keras.ops.cast(batch_size, dtype=\"float32\")\n", + " print(features_1.shape, features_2.shape, batch_size)\n", + " print(\"DBG0\", features_1.shape)\n", + " cross_correlation = keras.ops.matmul(keras.ops.transpose(features_1), features_2) / batch_size\n", + " print(\"DBG1\", cross_correlation.shape)\n", + " feature_dim = keras.ops.shape(features_1)[1]\n", + " print(\"DBG2\", feature_dim)\n", + " correlation_labels = keras.ops.arange(feature_dim)\n", + " print(\"DBG3\", correlation_labels.shape)\n", + " self.correlation_accuracy.update_state(correlation_labels, cross_correlation)\n", + " print(\"DBG4\", cross_correlation.shape)\n", + " self.correlation_accuracy.update_state(correlation_labels, keras.ops.transpose(cross_correlation))\n", + "\n", + " def train_step(self, data):\n", + " \"\"\"Training step for the model\"\"\"\n", + " pair1, pair2 = data\n", + "\n", + " # each input is augmented twice, differently\n", + " augmented_inputs_1 = self.contrastive_augmenter(pair1)\n", + " augmented_inputs_2 = self.contrastive_augmenter(pair2)\n", + " with tf.GradientTape() as tape:\n", + " # Encoder phase\n", + " features_1 = self.encoder(augmented_inputs_1)\n", + " features_2 = self.encoder(augmented_inputs_2)\n", + " # Projection phase\n", + " projections_1 = self.projector(features_1)\n", + " projections_2 = self.projector(features_2)\n", + " contrastive_loss = self.contrastive_loss(projections_1, projections_2)\n", + " # END WITH\n", + "\n", + " # backpropagation\n", + " gradients = tape.gradient(\n", + " contrastive_loss,\n", + " self.encoder.trainable_weights + self.projector.trainable_weights,\n", + " )\n", + " self.contrastive_optimizer.apply_gradients(\n", + " zip(\n", + " gradients,\n", + " self.encoder.trainable_weights + self.projector.trainable_weights,\n", + " )\n", + " )\n", + "\n", + " self.contrastive_loss_tracker.update_state(contrastive_loss)\n", + "\n", + " self.update_contrastive_accuracy(features_1, features_2)\n", + " self.update_correlation_accuracy(features_1, features_2)\n", + "\n", + " # # labels are only used in evalutation for probing\n", + " # augmented_inputs = self.classification_augmenter(labeled_pair)\n", + " # with tf.GradientTape() as tape:\n", + " # features = self.encoder(augmented_inputs)\n", + " # class_logits = self.linear_probe(features)\n", + " # probe_loss = self.probe_loss(labels, class_logits)\n", + " # gradients = tape.gradient(probe_loss, self.linear_probe.trainable_weights)\n", + " # self.probe_optimizer.apply_gradients(\n", + " # zip(gradients, self.linear_probe.trainable_weights)\n", + " # )\n", + " # self.probe_accuracy.update_state(labels, class_logits)\n", + "\n", + " return {m.name: m.result() for m in self.metrics}\n", + "\n", + " def test_step(self, data):\n", + " \"\"\"Test step for the model\"\"\"\n", + " pair1, pair2 = data\n", + " augmented_inputs_1 = self.contrastive_augmenter(pair1)\n", + " augmented_inputs_2 = self.contrastive_augmenter(pair2)\n", + " features_1 = self.encoder(augmented_inputs_1, training=False)\n", + " features_2 = self.encoder(augmented_inputs_2, training=False)\n", + " projections_1 = self.projector(features_1, training=False)\n", + " projections_2 = self.projector(features_2, training=False)\n", + "\n", + " contrastive_loss = self.contrastive_loss(projections_1, projections_2)\n", + " self.contrastive_loss_tracker.update_state(contrastive_loss)\n", + " self.update_contrastive_accuracy(features_1, features_2)\n", + " self.update_correlation_accuracy(features_1, features_2)\n", + "\n", + " return {m.name: m.result() for m in self.metrics}\n", + "\n", + "\n", + "class SimCLR(ContrastiveModel):\n", + " \"\"\"SimCLR model for self-supervised learning\"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " encoder: keras.Model,\n", + " projector: keras.Model,\n", + " contrastive_augmenter: Callable[[keras.KerasTensor], keras.KerasTensor] | None = None,\n", + " classification_augmenter: Callable[[keras.KerasTensor], keras.KerasTensor] | None = None,\n", + " linear_probe: keras.Model | None = None,\n", + " temperature: float = 0.1,\n", + " ):\n", + " super().__init__(\n", + " encoder=encoder,\n", + " projector=projector,\n", + " contrastive_augmenter=contrastive_augmenter,\n", + " classification_augmenter=classification_augmenter,\n", + " linear_probe=linear_probe,\n", + " )\n", + " self.temperature = temperature\n", + "\n", + " def contrastive_loss(self, projections_1, projections_2):\n", + " \"\"\"Contrastive loss function for SimCLR\"\"\"\n", + " # InfoNCE loss (information noise-contrastive estimation)\n", + " # NT-Xent loss (normalized temperature-scaled cross entropy)\n", + "\n", + " # cosine similarity: the dot product of the l2-normalized feature vectors\n", + " projections_1 = keras.ops.normalize(projections_1, axis=1)\n", + " projections_2 = keras.ops.normalize(projections_2, axis=1)\n", + " similarities = keras.ops.matmul(projections_1, keras.ops.transpose(projections_2)) / self.temperature\n", + "\n", + " # the temperature-scaled similarities are used as logits for cross-entropy\n", + " batch_size = keras.ops.shape(projections_1)[0]\n", + " contrastive_labels = keras.ops.arange(batch_size)\n", + " loss1 = keras.losses.sparse_categorical_crossentropy(contrastive_labels, similarities, from_logits=True)\n", + " loss2 = keras.losses.sparse_categorical_crossentropy(\n", + " contrastive_labels, keras.ops.transpose(similarities), from_logits=True\n", + " )\n", + " return (loss1 + loss2) / 2\n" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "def get_scheduler():\n", + " return keras.optimizers.schedules.CosineDecay(\n", + " initial_learning_rate=learning_rate,\n", + " decay_steps=steps_per_epoch * epochs,\n", + " )\n", + "\n", + "\n", + "model = SimCLR(\n", + " contrastive_augmenter=lambda x: x,\n", + " encoder=encoder,\n", + " projector=projector,\n", + " # momentum_coeff=0.999,\n", + " temperature=temperature,\n", + " # queue_size=65536,\n", + ")\n", + "\n", + "model.compile(\n", + " contrastive_optimizer=keras.optimizers.Adam(get_scheduler()),\n", + " probe_optimizer=keras.optimizers.Adam(get_scheduler()),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(1024, 128) (1024, 128) tf.Tensor(1024.0, shape=(), dtype=float32)\n", + "DBG0 (1024, 128)\n", + "DBG1 (128, 128)\n", + "DBG2 128\n", + "DBG3 (128,)\n", + "DBG4 (128, 128)\n" + ] + }, + { + "data": { + "text/plain": [ + "{'loss': ,\n", + " 'r_acc': ,\n", + " 'c_acc': }" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model.train_step((x, y))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "keras.preprocessing" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/guides/ecg-foundation-model.ipynb b/docs/guides/ecg-foundation-model.ipynb new file mode 100644 index 00000000..794a0de0 --- /dev/null +++ b/docs/guides/ecg-foundation-model.ipynb @@ -0,0 +1,1932 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ECG Foundation Model\n", + "\n", + "__Date created:__ 2024/07/25 \n", + "\n", + "__Last Modified:__ 2024/08/14 \n", + "\n", + "__Description:__ Train, evaluate, and export an ECG foundation model\n", + "\n", + "## Overview \n", + "\n", + "This notebook demonstrates creating a foundation model for raw ECG signals. By creating a foundation model, we can create small, down-stream classification models.\n", + "\n", + "\n", + "
\n", + "\n", + "- \n", + "\n", + " View in Colab\n", + "\n", + "\n", + "- \n", + "\n", + " GitHub source\n", + "\n", + "\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "#!pip install -q --disable-pip-version-check heartkit" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-08-14 16:43:51.133924: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:485] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered\n", + "2024-08-14 16:43:51.141788: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:8454] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered\n", + "2024-08-14 16:43:51.144156: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1452] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered\n" + ] + } + ], + "source": [ + "import os\n", + "os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' # 3\n", + "import contextlib\n", + "from pathlib import Path\n", + "import tempfile\n", + "import keras\n", + "import heartkit as hk\n", + "import tensorflow as tf\n", + "import numpy as np\n", + "import neuralspot_edge as nse\n", + "import matplotlib.pyplot as plt\n", + "from sklearn.manifold import TSNE\n", + "\n", + "os.environ['DATASET_PATH'] = '../datasets'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Constants\n", + "\n", + "Here we provide the constants that we will use throughout the guide. For better performance, adjust parameters such as `BATCH_SIZE`, `EPOCHS`, and `LEARNING_RATE`." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# File paths\n", + "datasets_dir = Path(os.getenv(\"DATASET_PATH\", \"./datasets\"))\n", + "job_dir = Path(tempfile.gettempdir()) / \"hk-foundation\"\n", + "model_file = job_dir / \"model.keras\"\n", + "val_file = job_dir / \"val.pkl\"\n", + "\n", + "# Data settings\n", + "sampling_rate = 100 # 100 Hz\n", + "input_size = 1000 # 10 seconds\n", + "frame_size = 800 # 8 seconds\n", + "\n", + "# Training settings\n", + "batch_size = 1024 # Batch size for training\n", + "buffer_size = 2000 # How many samples are shuffled each epoch\n", + "epochs = 150 # Increase this to 100+\n", + "steps_per_epoch = 25 # # Steps per epoch (must set since ds has unknown size)\n", + "samples_per_patient = 1 # Number of samples per patient\n", + "val_metric = \"loss\" # Metric to monitor for early stopping\n", + "val_mode = \"min\" # Mode for early stopping\n", + "val_size = 10000 # Number of samples used for validation\n", + "learning_rate = 1e-3 # Learning rate for Adam optimizer\n", + "epsilon = 0.001\n", + "\n", + "# Model settings\n", + "projection_width = 128\n", + "temperature = 0.1\n", + "\n", + "# Other settings\n", + "seed = 42 # Seed for reproducibility\n", + "verbose = 1 # Verbosity level\n", + "plot_theme = hk.utils.dark_theme\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
INFO     Job directory: /tmp/hk-foundation                                                          1079341004.py:6\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34mINFO \u001b[0m Job directory: \u001b[35m/tmp/\u001b[0m\u001b[95mhk-foundation\u001b[0m \u001b]8;id=984212;file:///tmp/ipykernel_43488/1079341004.py\u001b\\\u001b[2m1079341004.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=333817;file:///tmp/ipykernel_43488/1079341004.py#6\u001b\\\u001b[2m6\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "nse.utils.silence_tensorflow()\n", + "hk.utils.setup_plotting(plot_theme)\n", + "logger = nse.utils.setup_logger(__name__, level=verbose)\n", + "\n", + "os.makedirs(job_dir, exist_ok=True)\n", + "logger.info(f\"Job directory: {job_dir}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configure datasets\n", + "\n", + "We are going to train our model using two large datasets: the PTB-XL dataset and the large-scale arrhythmia dataset. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "datasets = [\n", + " hk.NamedParams(\n", + " name=\"lsad\",\n", + " params=dict(\n", + " path=datasets_dir / \"lsad\"\n", + " )\n", + " ),\n", + " hk.NamedParams(\n", + " name=\"ptbxl\",\n", + " params=dict(\n", + " path=datasets_dir / \"ptbxl\"\n", + " )\n", + " ),\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Download datasets\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "hk.datasets.download_datasets(hk.HKDownloadParams(\n", + " datasets=datasets,\n", + " force=False,\n", + " progress=True\n", + "))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create data pipeline\n", + "\n", + "Next, we will create a `tf.data` pipeline by performing the following steps on each dataset: \n", + "* Loading dataset class handler \n", + "* Leverage task specific data loader for given dataset\n", + "* Splittiing the dataset into training and validation sets\n", + "* Creating `tf.data.Dataset` objects for training and validation\n", + "\n", + "After creating all the `tf.data.Dataset` objects, we will merge them into a single dataset for training and validation. \n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# Load datasets\n", + "dsets = [hk.DatasetFactory.get(ds.name)(**ds.params) for ds in datasets]" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING: All log messages before absl::InitializeLog() is called are written to STDERR\n", + "I0000 00:00:1723653833.492531 43488 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723653833.512335 43488 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723653833.512436 43488 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723653833.514575 43488 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723653833.514661 43488 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723653833.514718 43488 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723653833.558813 43488 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723653833.558902 43488 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723653833.558960 43488 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n" + ] + } + ], + "source": [ + "dset_weights = np.array([0.5, 0.5])\n", + "\n", + "train_datasets = []\n", + "val_datasets = []\n", + "for ds in dsets:\n", + "\n", + " # Create dataloader specific to dataset\n", + " dataloader = hk.tasks.foundation.FoundationTaskFactory.get(ds.name)(\n", + " ds=ds,\n", + " frame_size=frame_size,\n", + " sampling_rate=sampling_rate,\n", + " )\n", + "\n", + " # Split patients into train and validation sets\n", + " train_patients, val_patients = dataloader.split_train_val_patients()\n", + "\n", + " # Create train dataset\n", + " train_ds = dataloader.create_dataloader(\n", + " patient_ids=train_patients,\n", + " samples_per_patient=samples_per_patient,\n", + " shuffle=True\n", + " )\n", + "\n", + " # Create validation dataset\n", + " val_ds = dataloader.create_dataloader(\n", + " patient_ids=val_patients,\n", + " samples_per_patient=samples_per_patient,\n", + " shuffle=False\n", + " )\n", + " train_datasets.append(train_ds)\n", + " val_datasets.append(val_ds)\n", + "# END FOR\n", + "\n", + "# Combine datasets\n", + "train_ds = tf.data.Dataset.sample_from_datasets(train_datasets, weights=dset_weights)\n", + "val_ds = tf.data.Dataset.sample_from_datasets(val_datasets, weights=dset_weights)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Visualize the data\n", + "\n", + "Let's visualize a sample ECG signal from the synthetic dataset. Note this contains no noise or artifacts. Augmentations will be applied later to generate noisy samples for training." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ecg1, ecg2 = next(iter(train_ds))\n", + "ecg1, ecg2 = ecg1.numpy().squeeze(), ecg2.numpy().squeeze()\n", + "\n", + "ts = np.arange(0, len(ecg1)) / sampling_rate\n", + "fig, ax = plt.subplots(1, 1, figsize=(9, 4))\n", + "ax.plot(ts, ecg1, color=plot_theme.primary_color, lw=3)\n", + "ax.plot(ts, ecg2, color=plot_theme.secondary_color, lw=3)\n", + "fig.suptitle(\"Raw ECG Signal\")\n", + "ax.set_xlabel(\"Time (s)\")\n", + "ax.set_ylabel(\"Amplitude\")\n", + "fig.tight_layout()\n", + "fig.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create augmentation pipeline\n", + "\n", + "To enable self-supervised training to learn useful features from raw ECG signals, we need to create an augmentation pipeline. Each sample will be augmented into two different ways. Using contrastive learning, the model should generate features that are similar for the same sample and different for different samples. " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "nstdb = hk.datasets.nstdb.NstdbNoise(target_rate=sampling_rate)\n", + "noises = np.hstack((nstdb.get_noise(noise_type=\"bw\"), nstdb.get_noise(noise_type=\"ma\"), nstdb.get_noise(noise_type=\"em\")))\n", + "noises = noises.astype(np.float32)\n", + "\n", + "preprocessor = nse.layers.preprocessing.LayerNormalization1D(\n", + " epsilon=epsilon,\n", + " name=\"LayerNormalization\"\n", + ")\n", + "\n", + "augmenter = nse.layers.preprocessing.AugmentationPipeline(\n", + " layers=[\n", + " nse.layers.preprocessing.RandomNoiseDistortion1D(\n", + " sample_rate=sampling_rate,\n", + " amplitude=(0, 1.0),\n", + " frequency=(0.5, 1.5),\n", + " name=\"BaselineWander\"\n", + " ),\n", + " nse.layers.preprocessing.RandomSineWave(\n", + " sample_rate=sampling_rate,\n", + " amplitude=(0, 0.05),\n", + " frequency=(45, 50),\n", + " name=\"PowerlineNoise\"\n", + " ),\n", + " nse.layers.preprocessing.AmplitudeWarp(\n", + " sample_rate=sampling_rate,\n", + " amplitude=(0.9, 1.1),\n", + " frequency=(0.5, 1.5),\n", + " name=\"AmplitudeWarp\"\n", + " ),\n", + " nse.layers.preprocessing.RandomGaussianNoise1D(\n", + " factor=(0.05, 0.2),\n", + " name=\"GaussianNoise\"\n", + " ),\n", + " nse.layers.preprocessing.RandomBackgroundNoises1D(\n", + " noises=noises,\n", + " amplitude=(0.05, 0.2),\n", + " num_noises=2,\n", + " name=\"RandomBackgroundNoises\"\n", + " ),\n", + " nse.layers.preprocessing.RandomCutout1D(\n", + " factor=(0.01, 0.05),\n", + " cutouts=2,\n", + " fill_mode=\"constant\",\n", + " fill_value=0.0,\n", + " name=\"RandomCutout\"\n", + " ),\n", + " nse.layers.preprocessing.RandomCrop1D(\n", + " duration=frame_size,\n", + " name=\"RandomCrop\",\n", + " auto_vectorize=True\n", + " )\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualize augmented pair" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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n/+iqKgIAOD3p2Do26/j7jtvxcxZc/Db739atuW005qhZkYGli793vC9C85vj5NPbowx4ybgldfe0nwvLS0VgUCR4Wd+/e0P9O7VHW3OPZsBAhEpiylGRERUKtOnz4oGBwCQm5eHJd8sRVpaKj77/MtocAAAe/fuw49r1uG005rB5/NFj99y03XIzcvDf156LRocAEAgUIS333kfANCt6zW2balfrx4AIOvAQdPP5OcXGI7l5R1DIBAwHM86cKD499avZ/tvExHJijMIRERUKn/8ucFwLDMr6/j31hu/l5mFlJQU1Kp1Evbvz0SVKlVw+t//hv37M3Hv3cZlSVNSih9NpzZratuWjIwaAICjR48avrdq9Vrs35+J++65A83POB1LvlmKH1b/iM2bt5r+vsOHjwAAamZk2P7bRESyYoBARESlkpObazhWVBQs/l6O4HvB4u/5j7/4V69eDV6vF/Xr18PAhwaY/jtpaam2bYnMDlSqVMnYzpwcXHfTHXjk4ftxecdL0PGyiwEAu/fsxUcjx+DTz74w/EyVKpUBAMfy823/bSIiWTFAICKiCpV7PIj49dff0e968xWOnMjOzgYAZNSoIfz+nj178e9Bg+HxeHDGGX/HxReej1tvvgHPP/sUDh85gpmz5mo+X+P47zl4/PcSEamINQhERFShcvPysGnzFpx6ajNUq1bV/gcsbNy0GcFgEM2aNbH8XDgcxp9/bsDIUePwf088DQDodPmlhs81a1r8ezZs2HRC7SIiSmYMEIiIqMKN/+QzpKWl4qX/PIvUVOOOzY0bNUSjhg1sf8/RozlYv2EjWrdqEd2HIeJvp52KWrVOMvxM7dq1AAAFBYWG7519VmsEAkVYu+4np38KEZF0mGJEREQV7rNJk3H22Weib+8eaHPu2fh++Ursz8xCrVon4dRmTXH2Wa3x2JODsGv3HtvftWDhEjzy8P045+wzsXbdz9HjF114Hp547FGsWbsO27bvwKFDh3Fy40bodPmlyM/Px6cTJ2l+T1paKs4+60x8v3wFjh1jDQIRqYsBAhERueLfgwbj22+X4dr+vdGx4yVIS0vDwQMHsX3HX3jtzSFYvvwHR7/niy+n4IEB96Bnj66aAGHpsuVo1Kgh2rVtg85XdkJaWir27cvErDnzMXLUWMNqRp2vugKpqVXw+aSvyvTvJCJKNp7TW7YJu90IIiKiE/H6Ky/gsssuRqcruyM3Ly+u3zFh3EjUqnUSuvboj1AoVMYtJCJKHqxBICKipDdk6PuoUrkybrn5+rh+/vzz2qNd23Px5lvvMjggIuUxQCAioqS3e89ePPX0YOTmxjd7UK1aVbz6+ttYsHBxGbeMiCj5MMWIiIiIiIiiOINARERERERRDBCIiIiIiCiKAQIREREREUUxQCAiIiIioigGCEREREREFMUAgYiIiIiIohggEBERERFRFAMEIiIiIiKKYoBARERERERR/w81cmcLYwqRbgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "aug_ecg1 = augmenter(preprocessor(keras.ops.convert_to_tensor(np.reshape(ecg1, (1, -1, 1)))), training=True)\n", + "aug_ecg1 = aug_ecg1.numpy().squeeze()\n", + "\n", + "aug_ecg2 = augmenter(preprocessor(keras.ops.convert_to_tensor(np.reshape(ecg2, (1, -1, 1)))), training=True)\n", + "aug_ecg2 = aug_ecg2.numpy().squeeze()\n", + "\n", + "\n", + "ts = np.arange(0, frame_size, 1) / sampling_rate\n", + "\n", + "fig, ax = plt.subplots(1, 1, figsize=(9, 4))\n", + "plt.title(\"Augmented ECG\")\n", + "plt.plot(ts, aug_ecg1, color=plot_theme.primary_color, lw=2)\n", + "plt.plot(ts, aug_ecg2, color=plot_theme.secondary_color, lw=2)\n", + "ax.set_xlabel(\"Time (s)\")\n", + "ax.set_ylabel(\"Amplitude\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create full data pipeline w/ augmentation\n", + "\n", + "We will now create a full data pipeline by extended the original with shuffling, batching, augmentations, and prefetching.\n", + "\n", + "For validation, we will cache a subset of the validation data to speed up the evaluation process." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "train_ds = train_ds.shuffle(\n", + " buffer_size=buffer_size,\n", + " reshuffle_each_iteration=True,\n", + ").batch(\n", + " batch_size=batch_size,\n", + " drop_remainder=True,\n", + " num_parallel_calls=tf.data.AUTOTUNE,\n", + ").map(\n", + " lambda x1, x2: {\n", + " nse.trainers.SimCLRTrainer.SAMPLES: x1,\n", + " nse.trainers.SimCLRTrainer.AUG_SAMPLES_0: augmenter(preprocessor(x1), training=True),\n", + " nse.trainers.SimCLRTrainer.AUG_SAMPLES_1: augmenter(preprocessor(x2), training=True),\n", + " },\n", + " num_parallel_calls=tf.data.AUTOTUNE\n", + ").prefetch(\n", + " tf.data.AUTOTUNE\n", + ")\n", + "\n", + "val_ds = val_ds.batch(\n", + " batch_size=batch_size,\n", + " drop_remainder=True,\n", + " num_parallel_calls=tf.data.AUTOTUNE,\n", + ").map(\n", + " lambda x1, x2: {\n", + " nse.trainers.SimCLRTrainer.SAMPLES: x1,\n", + " nse.trainers.SimCLRTrainer.AUG_SAMPLES_0: augmenter(preprocessor(x1), training=True),\n", + " nse.trainers.SimCLRTrainer.AUG_SAMPLES_1: augmenter(preprocessor(x2), training=True),\n", + " },\n", + " num_parallel_calls=tf.data.AUTOTUNE\n", + ").prefetch(\n", + " tf.data.AUTOTUNE\n", + ")\n", + "\n", + "# Cache the validation dataset\n", + "val_ds = val_ds.take(val_size//batch_size).cache()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define encoder model \n", + "\n", + "For this task, we are going to leverage a customized __EfficientNetV2__ model architecture for the encoder that is smaller and can handle 1D signals. The model consists of 5 main MBConv blocks with a global average pooling layer and a dense layer for classification." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "inputs = keras.Input(shape=(frame_size, 1), name=\"input\")\n", + "\n", + "encoder_params=dict(\n", + " input_filters=24,\n", + " input_kernel_size=(1, 9),\n", + " input_strides=(1, 2),\n", + " blocks=[\n", + " dict(filters=32, depth=2, kernel_size=(1, 9), strides=(1, 2), ex_ratio=1, se_ratio=4, norm=\"layer\"),\n", + " dict(filters=48, depth=2, kernel_size=(1, 9), strides=(1, 2), ex_ratio=1, se_ratio=4, norm=\"layer\"),\n", + " dict(filters=64, depth=2, kernel_size=(1, 9), strides=(1, 2), ex_ratio=1, se_ratio=4, norm=\"layer\"),\n", + " dict(filters=80, depth=1, kernel_size=(1, 9), strides=(1, 2), ex_ratio=1, se_ratio=4, norm=\"layer\"),\n", + " dict(filters=96, depth=1, kernel_size=(1, 9), strides=(1, 2), ex_ratio=1, se_ratio=4, norm=\"layer\"),\n", + " ],\n", + " output_filters=projection_width,\n", + " include_top=True,\n", + ")\n", + "\n", + "encoder = nse.models.efficientnet.efficientnetv2_from_object(\n", + " x=inputs,\n", + " params=encoder_params,\n", + " num_classes=None\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Visualize the model\n", + "\n", + "Let's view the encoder to understand the architecture better." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
INFO     Model: \"EfficientNetV2\"                                                               summary_utils.py:380\n",
+       "         ┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓                              \n",
+       "         ┃ Layer (type)        ┃ Output Shape      ┃    Param # ┃ Connected to      ┃                              \n",
+       "         ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩                              \n",
+       "         │ input (InputLayer)(None, 800, 1)0 │ -                 │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ reshape (Reshape)(None, 1, 800, 1)0 │ input[0][0]                    \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stem.conv (Conv2D)(None, 1, 400,    │        216 │ reshape[0][0]                    \n",
+       "         │                     │ 24)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stem.bn             │ (None, 1, 400,    │         96 │ stem.conv[0][0]                    \n",
+       "         │ (BatchNormalizatio… │ 24)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stem.act            │ (None, 1, 400,    │          0 │ stem.bn[0][0]                    \n",
+       "         │ (Activation)24)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.dp   │ (None, 1, 400,    │        216 │ stem.act[0][0]                    \n",
+       "         │ (DepthwiseConv2D)24)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.dp.… │ (None, 1, 400,    │         96 │ stage1.mbconv1.d… │                              \n",
+       "         │ (BatchNormalizatio… │ 24)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.dp.… │ (None, 1, 400,    │          0 │ stage1.mbconv1.d… │                              \n",
+       "         │ (Activation)24)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ max_pooling2d       │ (None, 1, 200,    │          0 │ stage1.mbconv1.d… │                              \n",
+       "         │ (MaxPooling2D)24)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.se.… │ (None, 1, 1, 24)0 │ max_pooling2d[0]… │                              \n",
+       "         │ (GlobalAveragePool… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.se.… │ (None, 1, 1, 6)150 │ stage1.mbconv1.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.se.… │ (None, 1, 1, 6)0 │ stage1.mbconv1.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.se.… │ (None, 1, 1, 24)168 │ stage1.mbconv1.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.se.… │ (None, 1, 1, 24)0 │ stage1.mbconv1.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ multiply (Multiply)(None, 1, 200,    │          0 │ max_pooling2d[0]… │                              \n",
+       "         │                     │ 24)               │            │ stage1.mbconv1.s… │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.red… │ (None, 1, 200,    │        768 │ multiply[0][0]                    \n",
+       "         │ (Conv2D)32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv1.red… │ (None, 1, 200,    │        128 │ stage1.mbconv1.r… │                              \n",
+       "         │ (BatchNormalizatio… │ 32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.dp   │ (None, 1, 200,    │        288 │ stage1.mbconv1.r… │                              \n",
+       "         │ (DepthwiseConv2D)32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.dp.… │ (None, 1, 200,    │        128 │ stage1.mbconv2.d… │                              \n",
+       "         │ (BatchNormalizatio… │ 32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.dp.… │ (None, 1, 200,    │          0 │ stage1.mbconv2.d… │                              \n",
+       "         │ (Activation)32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.se.… │ (None, 1, 1, 32)0 │ stage1.mbconv2.d… │                              \n",
+       "         │ (GlobalAveragePool… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.se.… │ (None, 1, 1, 8)264 │ stage1.mbconv2.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.se.… │ (None, 1, 1, 8)0 │ stage1.mbconv2.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.se.… │ (None, 1, 1, 32)288 │ stage1.mbconv2.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.se.… │ (None, 1, 1, 32)0 │ stage1.mbconv2.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ multiply_1          │ (None, 1, 200,    │          0 │ stage1.mbconv2.d… │                              \n",
+       "         │ (Multiply)32)               │            │ stage1.mbconv2.s… │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.red… │ (None, 1, 200,    │      1,024 │ multiply_1[0][0]                    \n",
+       "         │ (Conv2D)32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.red… │ (None, 1, 200,    │        128 │ stage1.mbconv2.r… │                              \n",
+       "         │ (BatchNormalizatio… │ 32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ dropout (Dropout)(None, 1, 200,    │          0 │ stage1.mbconv2.r… │                              \n",
+       "         │                     │ 32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage1.mbconv2.res  │ (None, 1, 200,    │          0 │ stage1.mbconv1.r… │                              \n",
+       "         │ (Add)32)               │            │ dropout[0][0]                    \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.dp   │ (None, 1, 200,    │        288 │ stage1.mbconv2.r… │                              \n",
+       "         │ (DepthwiseConv2D)32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.dp.… │ (None, 1, 200,    │        128 │ stage2.mbconv1.d… │                              \n",
+       "         │ (BatchNormalizatio… │ 32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.dp.… │ (None, 1, 200,    │          0 │ stage2.mbconv1.d… │                              \n",
+       "         │ (Activation)32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ max_pooling2d_1     │ (None, 1, 100,    │          0 │ stage2.mbconv1.d… │                              \n",
+       "         │ (MaxPooling2D)32)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.se.… │ (None, 1, 1, 32)0 │ max_pooling2d_1[… │                              \n",
+       "         │ (GlobalAveragePool… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.se.… │ (None, 1, 1, 8)264 │ stage2.mbconv1.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.se.… │ (None, 1, 1, 8)0 │ stage2.mbconv1.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.se.… │ (None, 1, 1, 32)288 │ stage2.mbconv1.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.se.… │ (None, 1, 1, 32)0 │ stage2.mbconv1.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ multiply_2          │ (None, 1, 100,    │          0 │ max_pooling2d_1[… │                              \n",
+       "         │ (Multiply)32)               │            │ stage2.mbconv1.s… │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.red… │ (None, 1, 100,    │      1,536 │ multiply_2[0][0]                    \n",
+       "         │ (Conv2D)48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv1.red… │ (None, 1, 100,    │        192 │ stage2.mbconv1.r… │                              \n",
+       "         │ (BatchNormalizatio… │ 48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.dp   │ (None, 1, 100,    │        432 │ stage2.mbconv1.r… │                              \n",
+       "         │ (DepthwiseConv2D)48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.dp.… │ (None, 1, 100,    │        192 │ stage2.mbconv2.d… │                              \n",
+       "         │ (BatchNormalizatio… │ 48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.dp.… │ (None, 1, 100,    │          0 │ stage2.mbconv2.d… │                              \n",
+       "         │ (Activation)48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.se.… │ (None, 1, 1, 48)0 │ stage2.mbconv2.d… │                              \n",
+       "         │ (GlobalAveragePool… │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.se.… │ (None, 1, 1, 12)588 │ stage2.mbconv2.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.se.… │ (None, 1, 1, 12)0 │ stage2.mbconv2.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.se.… │ (None, 1, 1, 48)624 │ stage2.mbconv2.s… │                              \n",
+       "         │ (Conv2D)            │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.se.… │ (None, 1, 1, 48)0 │ stage2.mbconv2.s… │                              \n",
+       "         │ (Activation)        │                   │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ multiply_3          │ (None, 1, 100,    │          0 │ stage2.mbconv2.d… │                              \n",
+       "         │ (Multiply)48)               │            │ stage2.mbconv2.s… │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.red… │ (None, 1, 100,    │      2,304 │ multiply_3[0][0]                    \n",
+       "         │ (Conv2D)48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ stage2.mbconv2.red… │ (None, 1, 100,    │        192 │ stage2.mbconv2.r… │                              \n",
+       "         │ (BatchNormalizatio… │ 48)               │            │                   │                              \n",
+       "         ├─────────────────────┼───────────────────┼────────────┼───────────────────┤                              \n",
+       "         │ dropout_1 (Dropout)(None, 1, 100,    │          0 │ stage2.mbconv2.r… │                              \n",
+       "         │                     │ 48)               │            │                   │                              \n",
+       "         └─────────────────────┴───────────────────┴────────────┴───────────────────┘                              \n",
+       "          Total params: 57,066 (222.91 KB)                                                                         \n",
+       "          Trainable params: 55,050 (215.04 KB)                                                                     \n",
+       "          Non-trainable params: 2,016 (7.88 KB)                                                                    \n",
+       "                                                                                                                   \n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34mINFO \u001b[0m Model: \u001b[32m\"EfficientNetV2\"\u001b[0m \u001b]8;id=922879;file:///workspaces/heartkit/.venv/lib/python3.12/site-packages/keras/src/utils/summary_utils.py\u001b\\\u001b[2msummary_utils.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=790357;file:///workspaces/heartkit/.venv/lib/python3.12/site-packages/keras/src/utils/summary_utils.py#380\u001b\\\u001b[2m380\u001b[0m\u001b]8;;\u001b\\\n", + " ┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ \u001b[2m \u001b[0m\n", + " ┃ Layer \u001b[1m(\u001b[0mtype\u001b[1m)\u001b[0m ┃ Output Shape ┃ Param # ┃ Connected to ┃ \u001b[2m \u001b[0m\n", + " ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ \u001b[2m \u001b[0m\n", + " │ input \u001b[1m(\u001b[0mInputLayer\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m800\u001b[0m, \u001b[1;36m1\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ - │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ reshape \u001b[1m(\u001b[0mReshape\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m800\u001b[0m, \u001b[1;36m1\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ input\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stem.conv \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m400\u001b[0m, │ \u001b[1;36m216\u001b[0m │ reshape\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stem.bn │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m400\u001b[0m, │ \u001b[1;36m96\u001b[0m │ stem.conv\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stem.act │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m400\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stem.bn\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.dp │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m400\u001b[0m, │ \u001b[1;36m216\u001b[0m │ stem.act\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mDepthwiseConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m400\u001b[0m, │ \u001b[1;36m96\u001b[0m │ stage1.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m400\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage1.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ max_pooling2d │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage1.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMaxPooling2D\u001b[1m)\u001b[0m │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ max_pooling2d\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mGlobalAveragePool… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m6\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m150\u001b[0m │ stage1.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m6\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage1.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m168\u001b[0m │ stage1.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage1.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ multiply \u001b[1m(\u001b[0mMultiply\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m0\u001b[0m │ max_pooling2d\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m… │ \u001b[2m \u001b[0m\n", + " │ │ \u001b[1;36m24\u001b[0m\u001b[1m)\u001b[0m │ │ stage1.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m768\u001b[0m │ multiply\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv1.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m128\u001b[0m │ stage1.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.dp │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m288\u001b[0m │ stage1.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mDepthwiseConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m128\u001b[0m │ stage1.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage1.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage1.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mGlobalAveragePool… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m8\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m264\u001b[0m │ stage1.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m8\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage1.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m288\u001b[0m │ stage1.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage1.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ multiply_1 │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage1.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMultiply\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ stage1.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m1\u001b[0m,\u001b[1;36m024\u001b[0m │ multiply_1\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m128\u001b[0m │ stage1.mbconv2.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ dropout \u001b[1m(\u001b[0mDropout\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage1.mbconv2.r… │ \u001b[2m \u001b[0m\n", + " │ │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage1.mbconv2.res │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage1.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mAdd\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ dropout\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.dp │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m288\u001b[0m │ stage1.mbconv2.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mDepthwiseConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m128\u001b[0m │ stage2.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m200\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage2.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ max_pooling2d_1 │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage2.mbconv1.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMaxPooling2D\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ max_pooling2d_1\u001b[1m[\u001b[0m… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mGlobalAveragePool… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m8\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m264\u001b[0m │ stage2.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m8\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage2.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m288\u001b[0m │ stage2.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage2.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ multiply_2 │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m0\u001b[0m │ max_pooling2d_1\u001b[1m[\u001b[0m… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMultiply\u001b[1m)\u001b[0m │ \u001b[1;36m32\u001b[0m\u001b[1m)\u001b[0m │ │ stage2.mbconv1.s… │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m1\u001b[0m,\u001b[1;36m536\u001b[0m │ multiply_2\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv1.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m192\u001b[0m │ stage2.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.dp │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m432\u001b[0m │ stage2.mbconv1.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mDepthwiseConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m192\u001b[0m │ stage2.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.dp.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage2.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage2.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mGlobalAveragePool… │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m12\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m588\u001b[0m │ stage2.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m12\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage2.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m624\u001b[0m │ stage2.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.se.… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ stage2.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mActivation\u001b[1m)\u001b[0m │ │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ multiply_3 │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage2.mbconv2.d… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mMultiply\u001b[1m)\u001b[0m │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ stage2.mbconv2.s… │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m2\u001b[0m,\u001b[1;36m304\u001b[0m │ multiply_3\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1m]\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mConv2D\u001b[1m)\u001b[0m │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ stage2.mbconv2.red… │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m192\u001b[0m │ stage2.mbconv2.r… │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mBatchNormalizatio… │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ \u001b[2m \u001b[0m\n", + " │ dropout_1 \u001b[1m(\u001b[0mDropout\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m100\u001b[0m, │ \u001b[1;36m0\u001b[0m │ stage2.mbconv2.r… │ \u001b[2m \u001b[0m\n", + " │ │ \u001b[1;36m48\u001b[0m\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " └─────────────────────┴───────────────────┴────────────┴───────────────────┘ \u001b[2m \u001b[0m\n", + " Total params: \u001b[1;36m57\u001b[0m,\u001b[1;36m066\u001b[0m \u001b[1m(\u001b[0m\u001b[1;36m222.91\u001b[0m KB\u001b[1m)\u001b[0m \u001b[2m \u001b[0m\n", + " Trainable params: \u001b[1;36m55\u001b[0m,\u001b[1;36m050\u001b[0m \u001b[1m(\u001b[0m\u001b[1;36m215.04\u001b[0m KB\u001b[1m)\u001b[0m \u001b[2m \u001b[0m\n", + " Non-trainable params: \u001b[1;36m2\u001b[0m,\u001b[1;36m016\u001b[0m \u001b[1m(\u001b[0m\u001b[1;36m7.88\u001b[0m KB\u001b[1m)\u001b[0m \u001b[2m \u001b[0m\n", + " \u001b[2m \u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
INFO     Computation: 4.17 MFLOPs                                                                    689369687.py:3\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34mINFO \u001b[0m Computation: \u001b[1;36m4.17\u001b[0m MFLOPs \u001b]8;id=701803;file:///tmp/ipykernel_43488/689369687.py\u001b\\\u001b[2m689369687.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=782029;file:///tmp/ipykernel_43488/689369687.py#3\u001b\\\u001b[2m3\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "encoder.summary(print_fn=logger.info, layer_range=('input', 'dropout_1'))\n", + "flops = nse.metrics.flops.get_flops(encoder, batch_size=1, fpath=os.devnull)\n", + "logger.info(f\"Computation: {flops/1e6:0.2f} MFLOPs\")\n", + "encoder_output = encoder(inputs)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
INFO     Model: \"projector\"                                                                    summary_utils.py:380\n",
+       "         ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓                              \n",
+       "         ┃ Layer (type)                    ┃ Output Shape           ┃       Param # ┃                              \n",
+       "         ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩                              \n",
+       "         │ keras_tensor_108CLONE           │ (None, 128)0                    \n",
+       "         │ (InputLayer)                    │                        │               │                              \n",
+       "         ├─────────────────────────────────┼────────────────────────┼───────────────┤                              \n",
+       "         │ dense (Dense)(None, 128)16,512                    \n",
+       "         ├─────────────────────────────────┼────────────────────────┼───────────────┤                              \n",
+       "         │ dense_1 (Dense)(None, 128)16,512                    \n",
+       "         └─────────────────────────────────┴────────────────────────┴───────────────┘                              \n",
+       "          Total params: 33,024 (129.00 KB)                                                                         \n",
+       "          Trainable params: 33,024 (129.00 KB)                                                                     \n",
+       "          Non-trainable params: 0 (0.00 B)                                                                         \n",
+       "                                                                                                                   \n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34mINFO \u001b[0m Model: \u001b[32m\"projector\"\u001b[0m \u001b]8;id=80169;file:///workspaces/heartkit/.venv/lib/python3.12/site-packages/keras/src/utils/summary_utils.py\u001b\\\u001b[2msummary_utils.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=604200;file:///workspaces/heartkit/.venv/lib/python3.12/site-packages/keras/src/utils/summary_utils.py#380\u001b\\\u001b[2m380\u001b[0m\u001b]8;;\u001b\\\n", + " ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ \u001b[2m \u001b[0m\n", + " ┃ Layer \u001b[1m(\u001b[0mtype\u001b[1m)\u001b[0m ┃ Output Shape ┃ Param # ┃ \u001b[2m \u001b[0m\n", + " ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ \u001b[2m \u001b[0m\n", + " │ keras_tensor_108CLONE │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m128\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m0\u001b[0m │ \u001b[2m \u001b[0m\n", + " │ \u001b[1m(\u001b[0mInputLayer\u001b[1m)\u001b[0m │ │ │ \u001b[2m \u001b[0m\n", + " ├─────────────────────────────────┼────────────────────────┼───────────────┤ \u001b[2m \u001b[0m\n", + " │ dense \u001b[1m(\u001b[0mDense\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m128\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m16\u001b[0m,\u001b[1;36m512\u001b[0m │ \u001b[2m \u001b[0m\n", + " ├─────────────────────────────────┼────────────────────────┼───────────────┤ \u001b[2m \u001b[0m\n", + " │ dense_1 \u001b[1m(\u001b[0mDense\u001b[1m)\u001b[0m │ \u001b[1m(\u001b[0m\u001b[3;35mNone\u001b[0m, \u001b[1;36m128\u001b[0m\u001b[1m)\u001b[0m │ \u001b[1;36m16\u001b[0m,\u001b[1;36m512\u001b[0m │ \u001b[2m \u001b[0m\n", + " └─────────────────────────────────┴────────────────────────┴───────────────┘ \u001b[2m \u001b[0m\n", + " Total params: \u001b[1;36m33\u001b[0m,\u001b[1;36m024\u001b[0m \u001b[1m(\u001b[0m\u001b[1;36m129.00\u001b[0m KB\u001b[1m)\u001b[0m \u001b[2m \u001b[0m\n", + " Trainable params: \u001b[1;36m33\u001b[0m,\u001b[1;36m024\u001b[0m \u001b[1m(\u001b[0m\u001b[1;36m129.00\u001b[0m KB\u001b[1m)\u001b[0m \u001b[2m \u001b[0m\n", + " Non-trainable params: \u001b[1;36m0\u001b[0m \u001b[1m(\u001b[0m\u001b[1;36m0.00\u001b[0m B\u001b[1m)\u001b[0m \u001b[2m \u001b[0m\n", + " \u001b[2m \u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "projector_input = encoder_output\n", + "projector_output = keras.layers.Dense(projection_width, activation=\"relu6\")(projector_input)\n", + "projector_output = keras.layers.Dense(projection_width)(projector_output)\n", + "projector = keras.Model(inputs=projector_input, outputs=projector_output, name=\"projector\")\n", + "flops = nse.metrics.flops.get_flops(projector, batch_size=1, fpath=os.devnull)\n", + "projector.summary(print_fn=logger.info)\n", + "logger.debug(f\"Projector requires {flops/1e6:0.2f} MFLOPS\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create a SimCLR model to train" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "model = nse.trainers.SimCLRTrainer(\n", + " encoder=encoder,\n", + " augmenter=None, # We augment in the data pipeline\n", + " projector=projector,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Compile the model\n", + "\n", + "We will compile the model using Adam optimizer with cosine learning rate scheduler and custom cosine similarity loss function. We will also attach metrics and callbacks to monitor the training process.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "def get_scheduler():\n", + " return keras.optimizers.schedules.CosineDecay(\n", + " initial_learning_rate=learning_rate,\n", + " decay_steps=steps_per_epoch * epochs,\n", + " )\n", + "\n", + "optimizer = keras.optimizers.Adam(get_scheduler())\n", + "loss = nse.losses.simclr.SimCLRLoss(temperature=temperature)\n", + "\n", + "metrics = [\n", + " keras.metrics.MeanSquaredError(name=\"mse\"),\n", + " keras.metrics.CosineSimilarity(name=\"cos\"),\n", + "]\n", + "\n", + "model_callbacks = [\n", + " keras.callbacks.EarlyStopping(\n", + " monitor=f\"val_{val_metric}\",\n", + " patience=max(int(0.25 * epochs), 1),\n", + " mode=val_mode,\n", + " restore_best_weights=True,\n", + " verbose=verbose - 1\n", + " ),\n", + " keras.callbacks.ModelCheckpoint(\n", + " filepath=str(model_file),\n", + " monitor=f\"val_{val_metric}\",\n", + " save_best_only=True,\n", + " mode=val_mode,\n", + " verbose=verbose - 1\n", + " ),\n", + " keras.callbacks.CSVLogger(job_dir / \"history.csv\"),\n", + "]\n", + "if nse.utils.env_flag(\"TENSORBOARD\"):\n", + " model_callbacks.append(\n", + " keras.callbacks.TensorBoard(\n", + " log_dir=job_dir,\n", + " write_steps_per_second=True,\n", + " )\n", + " )\n", + "\n", + "model.compile(\n", + " encoder_optimizer=optimizer,\n", + " encoder_loss=loss,\n", + " encoder_metrics=metrics,\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Train the model" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1/150\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-08-14 16:43:57.885843: E tensorflow/core/util/util.cc:131] oneDNN supports DT_INT32 only on platforms with AVX-512. Falling back to the default Eigen-based implementation if present.\n", + "WARNING: All log messages before absl::InitializeLog() is called are written to STDERR\n", + "I0000 00:00:1723653847.481601 43638 service.cc:146] XLA service 0x72a96c0040a0 initialized for platform CUDA (this does not guarantee that XLA will be used). Devices:\n", + "I0000 00:00:1723653847.481621 43638 service.cc:154] StreamExecutor device (0): NVIDIA GeForce RTX 4090, Compute Capability 8.9\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m 1/25\u001b[0m \u001b[37m━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[1m13:38\u001b[0m 34s/step - cos: 0.5873 - loss: 15.6832 - mse: 0.2619" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "I0000 00:00:1723653871.429351 43638 device_compiler.h:188] Compiled cluster using XLA! This line is logged at most once for the lifetime of the process.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m76s\u001b[0m 2s/step - cos: 0.6079 - loss: 14.7544 - mse: 0.2572 - val_cos: 0.6789 - val_loss: 12.3536 - val_mse: 0.2902\n", + "Epoch 2/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 218ms/step - cos: 0.6985 - loss: 12.0094 - mse: 0.2832 - val_cos: 0.7271 - val_loss: 11.2826 - val_mse: 0.2748\n", + "Epoch 3/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 186ms/step - cos: 0.7307 - loss: 11.0921 - mse: 0.2751 - val_cos: 0.7381 - val_loss: 10.5437 - val_mse: 0.2747\n", + "Epoch 4/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 185ms/step - cos: 0.7414 - loss: 10.3584 - mse: 0.2735 - val_cos: 0.7466 - val_loss: 9.9348 - val_mse: 0.2739\n", + "Epoch 5/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7485 - loss: 9.8257 - mse: 0.2716 - val_cos: 0.7494 - val_loss: 9.5416 - val_mse: 0.2717\n", + "Epoch 6/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7507 - loss: 9.4662 - mse: 0.2699 - val_cos: 0.7525 - val_loss: 9.2581 - val_mse: 0.2706\n", + "Epoch 7/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 177ms/step - cos: 0.7522 - loss: 9.1834 - mse: 0.2683 - val_cos: 0.7544 - val_loss: 8.9701 - val_mse: 0.2668\n", + "Epoch 8/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7560 - loss: 8.9178 - mse: 0.2655 - val_cos: 0.7574 - val_loss: 8.7733 - val_mse: 0.2668\n", + "Epoch 9/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 177ms/step - cos: 0.7573 - loss: 8.7257 - mse: 0.2665 - val_cos: 0.7583 - val_loss: 8.5376 - val_mse: 0.2623\n", + "Epoch 10/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7593 - loss: 8.5024 - mse: 0.2614 - val_cos: 0.7602 - val_loss: 8.3879 - val_mse: 0.2616\n", + "Epoch 11/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 189ms/step - cos: 0.7619 - loss: 8.3384 - mse: 0.2589 - val_cos: 0.7596 - val_loss: 8.2429 - val_mse: 0.2622\n", + "Epoch 12/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 178ms/step - cos: 0.7608 - loss: 8.1915 - mse: 0.2590 - val_cos: 0.7609 - val_loss: 8.0610 - val_mse: 0.2590\n", + "Epoch 13/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7608 - loss: 8.0654 - mse: 0.2590 - val_cos: 0.7624 - val_loss: 7.9472 - val_mse: 0.2581\n", + "Epoch 14/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 176ms/step - cos: 0.7619 - loss: 7.9416 - mse: 0.2545 - val_cos: 0.7623 - val_loss: 7.8332 - val_mse: 0.2539\n", + "Epoch 15/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7634 - loss: 7.8057 - mse: 0.2534 - val_cos: 0.7622 - val_loss: 7.7071 - val_mse: 0.2507\n", + "Epoch 16/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 184ms/step - cos: 0.7624 - loss: 7.7334 - mse: 0.2497 - val_cos: 0.7648 - val_loss: 7.6007 - val_mse: 0.2476\n", + "Epoch 17/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 186ms/step - cos: 0.7646 - loss: 7.5843 - mse: 0.2449 - val_cos: 0.7636 - val_loss: 7.5129 - val_mse: 0.2444\n", + "Epoch 18/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 184ms/step - cos: 0.7625 - loss: 7.5148 - mse: 0.2450 - val_cos: 0.7641 - val_loss: 7.4185 - val_mse: 0.2415\n", + "Epoch 19/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7632 - loss: 7.4050 - mse: 0.2425 - val_cos: 0.7644 - val_loss: 7.3061 - val_mse: 0.2372\n", + "Epoch 20/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7625 - loss: 7.2983 - mse: 0.2399 - val_cos: 0.7646 - val_loss: 7.2433 - val_mse: 0.2351\n", + "Epoch 21/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 178ms/step - cos: 0.7645 - loss: 7.2308 - mse: 0.2344 - val_cos: 0.7637 - val_loss: 7.1359 - val_mse: 0.2329\n", + "Epoch 22/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 186ms/step - cos: 0.7645 - loss: 7.1363 - mse: 0.2323 - val_cos: 0.7656 - val_loss: 7.0779 - val_mse: 0.2290\n", + "Epoch 23/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7639 - loss: 7.1185 - mse: 0.2329 - val_cos: 0.7647 - val_loss: 7.0107 - val_mse: 0.2300\n", + "Epoch 24/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 180ms/step - cos: 0.7652 - loss: 6.9564 - mse: 0.2318 - val_cos: 0.7645 - val_loss: 6.9260 - val_mse: 0.2304\n", + "Epoch 25/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 188ms/step - cos: 0.7655 - loss: 6.9658 - mse: 0.2294 - val_cos: 0.7629 - val_loss: 6.9134 - val_mse: 0.2292\n", + "Epoch 26/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 188ms/step - cos: 0.7635 - loss: 6.9111 - mse: 0.2293 - val_cos: 0.7656 - val_loss: 6.7977 - val_mse: 0.2227\n", + "Epoch 27/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 186ms/step - cos: 0.7645 - loss: 6.8593 - mse: 0.2240 - val_cos: 0.7667 - val_loss: 6.7824 - val_mse: 0.2229\n", + "Epoch 28/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 186ms/step - cos: 0.7647 - loss: 6.8003 - mse: 0.2231 - val_cos: 0.7637 - val_loss: 6.7338 - val_mse: 0.2186\n", + "Epoch 29/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7652 - loss: 6.7164 - mse: 0.2184 - val_cos: 0.7655 - val_loss: 6.6791 - val_mse: 0.2188\n", + "Epoch 30/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7641 - loss: 6.6790 - mse: 0.2182 - val_cos: 0.7656 - val_loss: 6.6183 - val_mse: 0.2149\n", + "Epoch 31/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 184ms/step - cos: 0.7656 - loss: 6.6425 - mse: 0.2136 - val_cos: 0.7657 - val_loss: 6.5779 - val_mse: 0.2169\n", + "Epoch 32/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7654 - loss: 6.5587 - mse: 0.2139 - val_cos: 0.7646 - val_loss: 6.5292 - val_mse: 0.2123\n", + "Epoch 33/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 180ms/step - cos: 0.7635 - loss: 6.5275 - mse: 0.2149 - val_cos: 0.7655 - val_loss: 6.5103 - val_mse: 0.2094\n", + "Epoch 34/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 184ms/step - cos: 0.7647 - loss: 6.5032 - mse: 0.2090 - val_cos: 0.7650 - val_loss: 6.4241 - val_mse: 0.2073\n", + "Epoch 35/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 188ms/step - cos: 0.7661 - loss: 6.4233 - mse: 0.2054 - val_cos: 0.7650 - val_loss: 6.4136 - val_mse: 0.2063\n", + "Epoch 36/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7647 - loss: 6.4019 - mse: 0.2058 - val_cos: 0.7660 - val_loss: 6.3694 - val_mse: 0.2018\n", + "Epoch 37/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 186ms/step - cos: 0.7665 - loss: 6.3876 - mse: 0.2023 - val_cos: 0.7654 - val_loss: 6.3280 - val_mse: 0.2013\n", + "Epoch 38/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 188ms/step - cos: 0.7634 - loss: 6.3524 - mse: 0.2026 - val_cos: 0.7643 - val_loss: 6.3219 - val_mse: 0.2011\n", + "Epoch 39/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 184ms/step - cos: 0.7656 - loss: 6.2882 - mse: 0.2002 - val_cos: 0.7653 - val_loss: 6.2572 - val_mse: 0.1989\n", + "Epoch 40/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 185ms/step - cos: 0.7662 - loss: 6.2738 - mse: 0.1985 - val_cos: 0.7658 - val_loss: 6.2181 - val_mse: 0.1965\n", + "Epoch 41/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7663 - loss: 6.2027 - mse: 0.1955 - val_cos: 0.7641 - val_loss: 6.2243 - val_mse: 0.1942\n", + "Epoch 42/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 188ms/step - cos: 0.7651 - loss: 6.2156 - mse: 0.1957 - val_cos: 0.7651 - val_loss: 6.1500 - val_mse: 0.1930\n", + "Epoch 43/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 180ms/step - cos: 0.7663 - loss: 6.1530 - mse: 0.1916 - val_cos: 0.7648 - val_loss: 6.1132 - val_mse: 0.1917\n", + "Epoch 44/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 180ms/step - cos: 0.7640 - loss: 6.1610 - mse: 0.1934 - val_cos: 0.7652 - val_loss: 6.1303 - val_mse: 0.1893\n", + "Epoch 45/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7649 - loss: 6.1110 - mse: 0.1910 - val_cos: 0.7653 - val_loss: 6.0887 - val_mse: 0.1884\n", + "Epoch 46/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7658 - loss: 6.0577 - mse: 0.1873 - val_cos: 0.7670 - val_loss: 6.0524 - val_mse: 0.1829\n", + "Epoch 47/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 189ms/step - cos: 0.7652 - loss: 6.0289 - mse: 0.1853 - val_cos: 0.7648 - val_loss: 6.0331 - val_mse: 0.1857\n", + "Epoch 48/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 188ms/step - cos: 0.7648 - loss: 6.0146 - mse: 0.1864 - val_cos: 0.7636 - val_loss: 6.0035 - val_mse: 0.1834\n", + "Epoch 49/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 178ms/step - cos: 0.7669 - loss: 5.9781 - mse: 0.1838 - val_cos: 0.7653 - val_loss: 5.9919 - val_mse: 0.1807\n", + "Epoch 50/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7653 - loss: 5.9600 - mse: 0.1807 - val_cos: 0.7639 - val_loss: 5.9345 - val_mse: 0.1814\n", + "Epoch 51/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7658 - loss: 5.9082 - mse: 0.1806 - val_cos: 0.7651 - val_loss: 5.9202 - val_mse: 0.1788\n", + "Epoch 52/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7657 - loss: 5.8902 - mse: 0.1795 - val_cos: 0.7653 - val_loss: 5.9247 - val_mse: 0.1777\n", + "Epoch 53/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 190ms/step - cos: 0.7667 - loss: 5.8934 - mse: 0.1755 - val_cos: 0.7662 - val_loss: 5.8941 - val_mse: 0.1748\n", + "Epoch 54/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 180ms/step - cos: 0.7654 - loss: 5.8449 - mse: 0.1777 - val_cos: 0.7639 - val_loss: 5.8760 - val_mse: 0.1750\n", + "Epoch 55/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7646 - loss: 5.8363 - mse: 0.1752 - val_cos: 0.7663 - val_loss: 5.8520 - val_mse: 0.1748\n", + "Epoch 56/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7643 - loss: 5.8241 - mse: 0.1771 - val_cos: 0.7639 - val_loss: 5.8281 - val_mse: 0.1738\n", + "Epoch 57/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7661 - loss: 5.8124 - mse: 0.1735 - val_cos: 0.7662 - val_loss: 5.7910 - val_mse: 0.1695\n", + "Epoch 58/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 174ms/step - cos: 0.7653 - loss: 5.7982 - mse: 0.1701 - val_cos: 0.7641 - val_loss: 5.7939 - val_mse: 0.1704\n", + "Epoch 59/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 184ms/step - cos: 0.7654 - loss: 5.7101 - mse: 0.1697 - val_cos: 0.7649 - val_loss: 5.7593 - val_mse: 0.1684\n", + "Epoch 60/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 186ms/step - cos: 0.7647 - loss: 5.7514 - mse: 0.1694 - val_cos: 0.7650 - val_loss: 5.7670 - val_mse: 0.1706\n", + "Epoch 61/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7657 - loss: 5.7096 - mse: 0.1699 - val_cos: 0.7658 - val_loss: 5.7169 - val_mse: 0.1678\n", + "Epoch 62/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 179ms/step - cos: 0.7665 - loss: 5.6322 - mse: 0.1684 - val_cos: 0.7648 - val_loss: 5.7120 - val_mse: 0.1699\n", + "Epoch 63/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7648 - loss: 5.7168 - mse: 0.1666 - val_cos: 0.7654 - val_loss: 5.6789 - val_mse: 0.1653\n", + "Epoch 64/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 190ms/step - cos: 0.7656 - loss: 5.6733 - mse: 0.1663 - val_cos: 0.7642 - val_loss: 5.6711 - val_mse: 0.1668\n", + "Epoch 65/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 188ms/step - cos: 0.7651 - loss: 5.6072 - mse: 0.1660 - val_cos: 0.7652 - val_loss: 5.6348 - val_mse: 0.1655\n", + "Epoch 66/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 197ms/step - cos: 0.7662 - loss: 5.6351 - mse: 0.1633 - val_cos: 0.7641 - val_loss: 5.6270 - val_mse: 0.1629\n", + "Epoch 67/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 184ms/step - cos: 0.7645 - loss: 5.6237 - mse: 0.1628 - val_cos: 0.7657 - val_loss: 5.6281 - val_mse: 0.1608\n", + "Epoch 68/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7663 - loss: 5.5894 - mse: 0.1605 - val_cos: 0.7656 - val_loss: 5.6172 - val_mse: 0.1631\n", + "Epoch 69/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7651 - loss: 5.5756 - mse: 0.1614 - val_cos: 0.7666 - val_loss: 5.5881 - val_mse: 0.1591\n", + "Epoch 70/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 180ms/step - cos: 0.7655 - loss: 5.6054 - mse: 0.1606 - val_cos: 0.7651 - val_loss: 5.5713 - val_mse: 0.1599\n", + "Epoch 71/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 185ms/step - cos: 0.7652 - loss: 5.5083 - mse: 0.1595 - val_cos: 0.7679 - val_loss: 5.5469 - val_mse: 0.1567\n", + "Epoch 72/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 176ms/step - cos: 0.7664 - loss: 5.4904 - mse: 0.1565 - val_cos: 0.7665 - val_loss: 5.5304 - val_mse: 0.1572\n", + "Epoch 73/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 174ms/step - cos: 0.7652 - loss: 5.4812 - mse: 0.1579 - val_cos: 0.7637 - val_loss: 5.5598 - val_mse: 0.1584\n", + "Epoch 74/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 189ms/step - cos: 0.7644 - loss: 5.5416 - mse: 0.1566 - val_cos: 0.7657 - val_loss: 5.5168 - val_mse: 0.1556\n", + "Epoch 75/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 179ms/step - cos: 0.7636 - loss: 5.5131 - mse: 0.1564 - val_cos: 0.7657 - val_loss: 5.5117 - val_mse: 0.1551\n", + "Epoch 76/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7655 - loss: 5.5103 - mse: 0.1556 - val_cos: 0.7646 - val_loss: 5.4959 - val_mse: 0.1543\n", + "Epoch 77/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 184ms/step - cos: 0.7651 - loss: 5.4582 - mse: 0.1536 - val_cos: 0.7648 - val_loss: 5.4829 - val_mse: 0.1530\n", + "Epoch 78/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 178ms/step - cos: 0.7647 - loss: 5.4871 - mse: 0.1542 - val_cos: 0.7668 - val_loss: 5.4578 - val_mse: 0.1516\n", + "Epoch 79/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 175ms/step - cos: 0.7644 - loss: 5.4601 - mse: 0.1513 - val_cos: 0.7645 - val_loss: 5.4772 - val_mse: 0.1527\n", + "Epoch 80/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7665 - loss: 5.4279 - mse: 0.1530 - val_cos: 0.7668 - val_loss: 5.4751 - val_mse: 0.1511\n", + "Epoch 81/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7658 - loss: 5.4332 - mse: 0.1508 - val_cos: 0.7657 - val_loss: 5.4332 - val_mse: 0.1500\n", + "Epoch 82/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 190ms/step - cos: 0.7649 - loss: 5.4055 - mse: 0.1500 - val_cos: 0.7651 - val_loss: 5.4315 - val_mse: 0.1497\n", + "Epoch 83/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 179ms/step - cos: 0.7658 - loss: 5.3739 - mse: 0.1495 - val_cos: 0.7649 - val_loss: 5.4271 - val_mse: 0.1504\n", + "Epoch 84/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7669 - loss: 5.3743 - mse: 0.1484 - val_cos: 0.7656 - val_loss: 5.4130 - val_mse: 0.1491\n", + "Epoch 85/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7662 - loss: 5.3568 - mse: 0.1504 - val_cos: 0.7658 - val_loss: 5.4074 - val_mse: 0.1494\n", + "Epoch 86/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7645 - loss: 5.3625 - mse: 0.1490 - val_cos: 0.7658 - val_loss: 5.3706 - val_mse: 0.1473\n", + "Epoch 87/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 173ms/step - cos: 0.7653 - loss: 5.3806 - mse: 0.1476 - val_cos: 0.7650 - val_loss: 5.3798 - val_mse: 0.1489\n", + "Epoch 88/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7633 - loss: 5.3545 - mse: 0.1499 - val_cos: 0.7660 - val_loss: 5.3665 - val_mse: 0.1472\n", + "Epoch 89/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7659 - loss: 5.3272 - mse: 0.1465 - val_cos: 0.7652 - val_loss: 5.3705 - val_mse: 0.1472\n", + "Epoch 90/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 185ms/step - cos: 0.7663 - loss: 5.3293 - mse: 0.1479 - val_cos: 0.7663 - val_loss: 5.3406 - val_mse: 0.1457\n", + "Epoch 91/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7664 - loss: 5.2704 - mse: 0.1462 - val_cos: 0.7646 - val_loss: 5.3628 - val_mse: 0.1448\n", + "Epoch 92/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 184ms/step - cos: 0.7649 - loss: 5.3189 - mse: 0.1471 - val_cos: 0.7652 - val_loss: 5.3332 - val_mse: 0.1445\n", + "Epoch 93/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7657 - loss: 5.2883 - mse: 0.1448 - val_cos: 0.7648 - val_loss: 5.3310 - val_mse: 0.1457\n", + "Epoch 94/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7649 - loss: 5.3115 - mse: 0.1452 - val_cos: 0.7653 - val_loss: 5.3161 - val_mse: 0.1444\n", + "Epoch 95/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 178ms/step - cos: 0.7663 - loss: 5.2990 - mse: 0.1442 - val_cos: 0.7649 - val_loss: 5.3473 - val_mse: 0.1428\n", + "Epoch 96/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 180ms/step - cos: 0.7645 - loss: 5.2752 - mse: 0.1441 - val_cos: 0.7656 - val_loss: 5.2979 - val_mse: 0.1439\n", + "Epoch 97/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 178ms/step - cos: 0.7659 - loss: 5.3156 - mse: 0.1440 - val_cos: 0.7648 - val_loss: 5.3119 - val_mse: 0.1437\n", + "Epoch 98/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7654 - loss: 5.2814 - mse: 0.1436 - val_cos: 0.7664 - val_loss: 5.2911 - val_mse: 0.1416\n", + "Epoch 99/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7655 - loss: 5.2715 - mse: 0.1438 - val_cos: 0.7657 - val_loss: 5.2657 - val_mse: 0.1405\n", + "Epoch 100/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 175ms/step - cos: 0.7662 - loss: 5.2725 - mse: 0.1426 - val_cos: 0.7655 - val_loss: 5.2761 - val_mse: 0.1427\n", + "Epoch 101/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 185ms/step - cos: 0.7640 - loss: 5.2789 - mse: 0.1444 - val_cos: 0.7650 - val_loss: 5.2813 - val_mse: 0.1430\n", + "Epoch 102/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 185ms/step - cos: 0.7657 - loss: 5.2833 - mse: 0.1421 - val_cos: 0.7658 - val_loss: 5.2767 - val_mse: 0.1409\n", + "Epoch 103/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 184ms/step - cos: 0.7635 - loss: 5.2581 - mse: 0.1423 - val_cos: 0.7654 - val_loss: 5.2443 - val_mse: 0.1418\n", + "Epoch 104/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 179ms/step - cos: 0.7650 - loss: 5.2546 - mse: 0.1418 - val_cos: 0.7651 - val_loss: 5.2777 - val_mse: 0.1410\n", + "Epoch 105/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7652 - loss: 5.2356 - mse: 0.1404 - val_cos: 0.7648 - val_loss: 5.2592 - val_mse: 0.1422\n", + "Epoch 106/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7661 - loss: 5.2543 - mse: 0.1421 - val_cos: 0.7656 - val_loss: 5.2489 - val_mse: 0.1399\n", + "Epoch 107/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 181ms/step - cos: 0.7658 - loss: 5.2340 - mse: 0.1411 - val_cos: 0.7644 - val_loss: 5.2261 - val_mse: 0.1417\n", + "Epoch 108/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 179ms/step - cos: 0.7652 - loss: 5.2379 - mse: 0.1407 - val_cos: 0.7648 - val_loss: 5.2458 - val_mse: 0.1408\n", + "Epoch 109/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 179ms/step - cos: 0.7652 - loss: 5.2290 - mse: 0.1403 - val_cos: 0.7671 - val_loss: 5.2311 - val_mse: 0.1386\n", + "Epoch 110/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 179ms/step - cos: 0.7658 - loss: 5.2189 - mse: 0.1405 - val_cos: 0.7637 - val_loss: 5.2488 - val_mse: 0.1399\n", + "Epoch 111/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 185ms/step - cos: 0.7662 - loss: 5.1476 - mse: 0.1399 - val_cos: 0.7658 - val_loss: 5.2175 - val_mse: 0.1391\n", + "Epoch 112/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 179ms/step - cos: 0.7648 - loss: 5.1940 - mse: 0.1395 - val_cos: 0.7665 - val_loss: 5.2373 - val_mse: 0.1393\n", + "Epoch 113/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 185ms/step - cos: 0.7667 - loss: 5.1483 - mse: 0.1382 - val_cos: 0.7645 - val_loss: 5.2505 - val_mse: 0.1392\n", + "Epoch 114/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 180ms/step - cos: 0.7649 - loss: 5.2019 - mse: 0.1394 - val_cos: 0.7656 - val_loss: 5.2092 - val_mse: 0.1382\n", + "Epoch 115/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7641 - loss: 5.2252 - mse: 0.1404 - val_cos: 0.7655 - val_loss: 5.2196 - val_mse: 0.1379\n", + "Epoch 116/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 181ms/step - cos: 0.7655 - loss: 5.2091 - mse: 0.1387 - val_cos: 0.7667 - val_loss: 5.2100 - val_mse: 0.1379\n", + "Epoch 117/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 187ms/step - cos: 0.7661 - loss: 5.1415 - mse: 0.1380 - val_cos: 0.7660 - val_loss: 5.2030 - val_mse: 0.1383\n", + "Epoch 118/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 179ms/step - cos: 0.7657 - loss: 5.1769 - mse: 0.1396 - val_cos: 0.7649 - val_loss: 5.2257 - val_mse: 0.1390\n", + "Epoch 119/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 188ms/step - cos: 0.7659 - loss: 5.1658 - mse: 0.1385 - val_cos: 0.7656 - val_loss: 5.1892 - val_mse: 0.1399\n", + "Epoch 120/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 177ms/step - cos: 0.7659 - loss: 5.1950 - mse: 0.1398 - val_cos: 0.7658 - val_loss: 5.2018 - val_mse: 0.1386\n", + "Epoch 121/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 178ms/step - cos: 0.7644 - loss: 5.1714 - mse: 0.1375 - val_cos: 0.7659 - val_loss: 5.1822 - val_mse: 0.1392\n", + "Epoch 122/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 177ms/step - cos: 0.7660 - loss: 5.1258 - mse: 0.1372 - val_cos: 0.7645 - val_loss: 5.2019 - val_mse: 0.1381\n", + "Epoch 123/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 185ms/step - cos: 0.7647 - loss: 5.1697 - mse: 0.1387 - val_cos: 0.7657 - val_loss: 5.1806 - val_mse: 0.1380\n", + "Epoch 124/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 175ms/step - cos: 0.7668 - loss: 5.1285 - mse: 0.1372 - val_cos: 0.7654 - val_loss: 5.1829 - val_mse: 0.1380\n", + "Epoch 125/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 184ms/step - cos: 0.7657 - loss: 5.1229 - mse: 0.1383 - val_cos: 0.7658 - val_loss: 5.1817 - val_mse: 0.1391\n", + "Epoch 126/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 186ms/step - cos: 0.7661 - loss: 5.1769 - mse: 0.1372 - val_cos: 0.7652 - val_loss: 5.1979 - val_mse: 0.1373\n", + "Epoch 127/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7661 - loss: 5.1240 - mse: 0.1386 - val_cos: 0.7651 - val_loss: 5.1885 - val_mse: 0.1379\n", + "Epoch 128/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 178ms/step - cos: 0.7646 - loss: 5.1769 - mse: 0.1365 - val_cos: 0.7658 - val_loss: 5.1890 - val_mse: 0.1380\n", + "Epoch 129/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7649 - loss: 5.1534 - mse: 0.1388 - val_cos: 0.7666 - val_loss: 5.1853 - val_mse: 0.1367\n", + "Epoch 130/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7661 - loss: 5.1237 - mse: 0.1367 - val_cos: 0.7647 - val_loss: 5.1686 - val_mse: 0.1385\n", + "Epoch 131/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 177ms/step - cos: 0.7655 - loss: 5.1069 - mse: 0.1363 - val_cos: 0.7657 - val_loss: 5.1731 - val_mse: 0.1371\n", + "Epoch 132/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 178ms/step - cos: 0.7651 - loss: 5.2106 - mse: 0.1390 - val_cos: 0.7671 - val_loss: 5.1701 - val_mse: 0.1374\n", + "Epoch 133/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7665 - loss: 5.1153 - mse: 0.1371 - val_cos: 0.7654 - val_loss: 5.1739 - val_mse: 0.1380\n", + "Epoch 134/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 186ms/step - cos: 0.7647 - loss: 5.1489 - mse: 0.1377 - val_cos: 0.7658 - val_loss: 5.1684 - val_mse: 0.1371\n", + "Epoch 135/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 178ms/step - cos: 0.7647 - loss: 5.1739 - mse: 0.1381 - val_cos: 0.7664 - val_loss: 5.1759 - val_mse: 0.1362\n", + "Epoch 136/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7657 - loss: 5.1280 - mse: 0.1367 - val_cos: 0.7670 - val_loss: 5.1561 - val_mse: 0.1364\n", + "Epoch 137/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 184ms/step - cos: 0.7652 - loss: 5.1234 - mse: 0.1373 - val_cos: 0.7651 - val_loss: 5.1574 - val_mse: 0.1378\n", + "Epoch 138/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 187ms/step - cos: 0.7656 - loss: 5.1879 - mse: 0.1378 - val_cos: 0.7644 - val_loss: 5.1774 - val_mse: 0.1370\n", + "Epoch 139/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 175ms/step - cos: 0.7656 - loss: 5.1358 - mse: 0.1355 - val_cos: 0.7644 - val_loss: 5.1737 - val_mse: 0.1378\n", + "Epoch 140/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7648 - loss: 5.1623 - mse: 0.1373 - val_cos: 0.7647 - val_loss: 5.1624 - val_mse: 0.1377\n", + "Epoch 141/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7666 - loss: 5.1460 - mse: 0.1366 - val_cos: 0.7662 - val_loss: 5.1674 - val_mse: 0.1384\n", + "Epoch 142/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7649 - loss: 5.1491 - mse: 0.1389 - val_cos: 0.7663 - val_loss: 5.1577 - val_mse: 0.1369\n", + "Epoch 143/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 182ms/step - cos: 0.7652 - loss: 5.1276 - mse: 0.1375 - val_cos: 0.7655 - val_loss: 5.1551 - val_mse: 0.1372\n", + "Epoch 144/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 183ms/step - cos: 0.7657 - loss: 5.1474 - mse: 0.1374 - val_cos: 0.7649 - val_loss: 5.1546 - val_mse: 0.1383\n", + "Epoch 145/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 181ms/step - cos: 0.7649 - loss: 5.1533 - mse: 0.1373 - val_cos: 0.7657 - val_loss: 5.1580 - val_mse: 0.1374\n", + "Epoch 146/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 178ms/step - cos: 0.7650 - loss: 5.1728 - mse: 0.1389 - val_cos: 0.7652 - val_loss: 5.1623 - val_mse: 0.1367\n", + "Epoch 147/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m5s\u001b[0m 185ms/step - cos: 0.7655 - loss: 5.1751 - mse: 0.1374 - val_cos: 0.7646 - val_loss: 5.1568 - val_mse: 0.1374\n", + "Epoch 148/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 177ms/step - cos: 0.7674 - loss: 5.1343 - mse: 0.1385 - val_cos: 0.7650 - val_loss: 5.1751 - val_mse: 0.1379\n", + "Epoch 149/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 178ms/step - cos: 0.7654 - loss: 5.1472 - mse: 0.1366 - val_cos: 0.7647 - val_loss: 5.1577 - val_mse: 0.1377\n", + "Epoch 150/150\n", + "\u001b[1m25/25\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m4s\u001b[0m 180ms/step - cos: 0.7663 - loss: 5.1473 - mse: 0.1376 - val_cos: 0.7658 - val_loss: 5.1742 - val_mse: 0.1373\n" + ] + } + ], + "source": [ + "history = model.fit(\n", + " train_ds,\n", + " steps_per_epoch=steps_per_epoch,\n", + " verbose=verbose,\n", + " epochs=epochs,\n", + " validation_data=val_ds,\n", + " callbacks=model_callbacks,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Visualize training history\n", + "\n", + "Let's visualize the training history to understand the model's performance during training. This will help to ensure the model is learning and not under or overfitting." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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cmKiw9wnP/npfTz3zP84e8r3mHTYBBYv+QaX28XhkyayZs06Fi/5hvHjxhwUQ+tPOmw8V191OWVl5axavZaysjK8Xg9jRo8iKyuTgoJCHn/ymUbbfPLpDK6/7vs8/fjDzF/wFT5fLQB//us/qaisZPbcL/nvI49zy0038NTj/2Xxkm/Izy+gX7++jBwxjHA4zP2/+X2zc9C1xXPPv8yRR8SmoXjrnfcbTWwuIiIHByV0IiJd1LLlK7jg4qu484c3M2rkCE6cfBz5BYU8/+LLPPzIE/zy3nsSHeI+W79hIxdcdCU/vO0mjjn6SE4+6QTyCwp59rmXePjRJ3j/7djAHeXlFYkNFHjz7ffw+wNMGD+WgQP6kXHYeKqra8jPL+CZ517i1dfepKKystE2f//Xw0TNKKecfCInn3QCDkds5NCHH308XvYf/3qYxUuWcuXllzJm9EjGjB5FeUUFH037lCeefo7ly1e2O/Z5CxYSDoexWCy8qMFQREQOSsbg4eMPrqHARERE2qFnj+588tHb+Hy1TDpq8kE34mVncuEF5/K7B+5jzhfzuOEHtyU6HBERaYb60ImISKfjdrsYOKBp/7Lu3fL40x/+D6vVytvvvK9krh3cbhdTb7gW+LbPnoiIHHzU5FJERDqdjPR0Pnj3NbZu286WLVupqfHRrVseI4YPxel0snrNWv7+r4cTHWandP21VzFo0EAmjBtL7949mT3nC774cn6iwxIRkRYooRMRkU6nvKKCJ558lsMPP4xRI0eQnJyM3+9n7br1fPLpDJ574RX8fn+iw+yUjj/uGA6fNJGysnLeeOtdHvrjXxMdkoiI7IH60ImIiIiIiHRS6kMnIiIiIiLSSSmhExERERER6aSU0ImIiIiIiHRSSuhEREREREQ6KSV0IiIiIiIinZQSOhERERERkU5KCZ2IiIiIiEgnpYRORERERESkk1JCJyIiIiIi0kkpoRMREREREemklNCJiIiIiIh0UkroREREREREOikldCIiIiIiIp2UEjoREREREZFOSgmdiIiIiIhIJ6WETkREREREpJNSQiciIiIiItJJKaETERERERHppJTQiYiIiIiIdFJK6ERERERERDopJXQiIiIiIiKdlBI6ERERERGRTkoJnYiIiIiISCelhE5ERERERKSTsiU6gK4gJycbn6820WGIiIiIiEgX4fV6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Kex3zV/wFeUVFVx26YWNll92yYXU1tYxa/bcAxFul/Pc8y83+Xy+7/7/A+CNt97lltt/xI4duwC9DvY39aHrZJYtX8FH0z7l7jtvIzMzna3btnPeOWfRo3t37r3vgUSH1+X87J67OOnEE5gx83PSUlM4+6wzGq1/9/2PAHjksSc5/bSTefapR3n2uZfweDxcf91VrF27njfeejcRoXcJ5RUVTJ8xq8nyq6+6DKDRur/947+8/MKTPPfMY7z62pvk5eVw7dVXMueLecyZO+8ARdz1rF6zltffeJsLLzgXq9XKV18vZtJhEzjj9FN45H9Pxpue6frvP088+SzHHXMULzz7OC+89CoVFZWccPwxHH/cMbz6+lu6B/vBFZdfTEpycrwmevIJx5JXX0v93AuvUFNT0+r3/cLCIp597kVuuO5qbDYby1es4uQTT+CwieP50T33ajLlFuztHpjRKE/87z+kpCTzxFPPNplLbtv2HfEfYQOBAP/81yP8+r6f8Y+//oE5X8xj4oRxnHP2mfz17/+msrLqwJ5cJ7G3e7Bq9RpWrV7TaJse3bsBsGHDpkaf0Xod7F/G4OHjm58wQg5aDoeDO2+/mSlTvkdqSjJr163nH/96hLlf6AO7oz371KMcPmlii+uHjJgQ/++BA/rzs5/ezYRxYwmFQnw+ey4P/elvTfoeSfs9+9SjpKenMeXcSxotnzB+LD+++3aGDxuKz1fLRx9/yl//9m98tbUJirRrsNlsTL3xWs4/72xycrLZtSufF196lWeee6lROV3//WfUqBHcfssPGDZsKGlpqezcsZO33nmfx598tlETM92DjjH9k/fo2aN7s+tOPOUsdu7KB1r/vm8YBjdefw2XXHw+OdlZbNm6jf899jTvffDRfj+Xzmpv9wBgxqfvt7j9m2+/x8/vvb/RsosuPI/rrr6Snj27k19QyAsvvtLkfUy+1drXwe56dO/GjE/f5w9/+jtPPv1co3V6Hew/SuhEREREREQ6KfWhExERERER6aSU0ImIiIiIiHRSSuhEREREREQ6KSV0IiIiIiIinZQSOhERERERkU5KCZ2IiIiIiEgnpYRORERERESkk1JCJyIiIiIi0kkpoRMREREREemklNCJiIiIiIh0UkroREREREREOikldCIiIiIiIp2UEjoREREREZFOSgmdiIiIiIhIJ6WETkREREREpJNSQiciIiIiItJJKaETERERERHppJTQiYiIiIiIdFJK6ERERERERDopJXQiIiIiIiKdlC3RAXQFOTnZ+Hy1iQ5DRERERES6CK/XQ1FR8V7LKaFrp5ycbObMnJboMEREREREpIs5dvLpe03qlNC1U0PN3LGTT1ctnYiIiIiItJvX62HOzGmtyi+U0HUQn68Wn8+X6DBEREREROQQokFRREREREREOikldCIiIiIiIp2UEjoREREREZFOSn3oREREdpPr7skFfW8kyZ7KN2XzWFIylyL/zkSHJSIi+8Bq2Oju6UtB3XZC0UCiw9kvlNCJdGJWw0a/5KEMShmFP1LH7IL3iZjhRIeVMF5bCmf3uZphaeOpCJQwu+ADlpTObfGaGFgwibZh/8kck3sGx+R9D5MoH2x7kYXF0zExO+oUWmQxrBgY+3R/c1w9KPbnt+lcu6IcVw8GpY4ixZ7O8vIF7PBtarTebnFyTp9rOLv31TisTgDGZx3LtYPvYadvM0tK57KibCFLy748IPH29A7g2LzvURksY8aut/BHmh/pbHjaRA7LnkxR3Q5WVyxha826dt3rdEc2yY60+GNfqIrSQOE+7++7DAwcVheBSF18WYo9nWA00OI5HigWw0rUjMQf2ww7A1NHUhOqJL92W6d/f7VbnJzc/XzO6HUZO32beXTNb6kIlrRq23RHFj29A/DYklha9mWj+9f2OBwMShnNqIxJ8WtbWLeDIv+uff7Cbbc4sGAhEPXvc1wNXFYPvbwD8IWrqQqWUxOubPc+IfaZk+PuTg9PP7Jd3VhdsZhtvg2t2M6gf/JwDss+gQlZx+O0ulhYPIM5BR9SFiiip7c/deFattasbfHzyGtLIcfdnTx3b7p7+tDN04dunt6kODJYWvIFH+98lR2+jR1ynrtzWt24rV6shg2bxYbNsGMYBoV1OwhFgy1ul+3qjtvmZVvN+nbHkOXK40+Hv4I/XMvi0jksKJrO0tIvOuS5crAwBg8fv/+/iXRhXq+XxQtnM37ScRrlUpplNWyMzjgCh8XJlpq1FNXtbFcC0Ms7kGPzvsfg1DH0Tx4W/+IJsK1mA4+s/g2bqlc1u22fpMGkOTKxWRzYDDt2ix27xYHNYq9/7MBmcQAmm6vXsKZiKXWRmib7cVhc9E0eQpIthbJAMaWBAqpDFS3GbGDQzdOHASnD6Z88gt5JAzHNKP5IHeFoCAwwTZMNlcvZWL2SrTUbmj1usi2NPsmD6eHpS56nN1mubrisbtZULGFu4UeUB0p44riZ2C2O+DaVwTK+KPiIeUWfsql6dfwL2Xl9r+eifjexrGw+7259hlUVXwOx+2UxLBhYMAwDCxb6JQ/jxO7nMin7xEbXG2B7zUbe2PIYC4o+w2JYGZQyklxPL7q5e5Pj7oHVsGFiYppRNlav4rOdb8S/tB6efTJ1ER++UBVOqwuX1YPb5sVl9VDsz2dZ2TwAkmypPHLMJ9gsNurCtYSjQQzDgtPqwheq5ovCj3h+w9+bPK8GpozkioF3MCxtPNtrNvLSxn+xuHROk+s6NvNoLh9wO7nuXiwvm88nO15lfdXKRvfAathItqfhtnowDAtRM0LEjBCt/6sN1+C2JdEnaRCDUkczKGUkNaEqigP5VAXKsFps2C0O/JFaVlcsZkv1OjJdOXhtKQQjfnzhamrDNYTNUIvPo1x3T47MOZVBqaPomzSEEn8+XxZ+zPyiz0h1ZjI282gcFgdR0yTJlkK3+ueIw+oixZ6O2+ZptL+dvs0sKJ7Oluq1ZDhzObPX5WS7uwOwrGw+y8sWMD7rOAaljMJmif3+aZomvnA1n+58ndkF75Nfu7XRPvsmDeGw7Mn19z2KaZpEiRKI1LGpejUbqlYQNSONvsj0TRqC0+qmxJ9PIOJnQtZxnNTjfAanjo6XqQ5V8MG2F/h4xyvURWKfNZOyT+SiflPplTSwUQy14RrWViwl05VHdbCC5eULWFg8nfzabc2+99gMOyPTD2NC9vGMyTiKnPpr0GB7zUY2VC3HMGK9NLp7+lIRKKE6VEGRfyflgRKCUT9Oi4fKYAlrK7+hf8pwBqaMZEDKCFLt6eyo3YwvVEW2uzvDU8eT6szkPyvvY3DaGEakTaS7t2/sWplRqI/RMAy212xkUclsPtrxEtWhikY/wlgMK7muHmS4cqkL11ATqqI6VEkoGsBt81ITqqKXtz9Zrm5kunKxGXbKgyX4QpXYLHbcVi+1ER/bazZSG67ml+MeoU/SYIJRP8V1+VgNC9nuHjitLgDC0TD5tVvZ4dtEft02nBYX6c4sMl155Lp64LJ52VC5gln57zC38KP49XNaXIzOOJKJ2cczIGUkBrDDt5kNVcvJr91GRbCEmlAlvnA1vnB1o4SyJUflnM74rGPIcOZQ5i+iIlRKMOqvfy3V8EXhR9SGv339NiRy5/S5ljRnZny5P1LH35b/hG/q32uuHvQTBqeOZnP1anzhavLcvchz9yLH3QO3zRvfzheqZodvE4FoHS6rh6gZZXXFIpaXLaSobidD0saQ6sggFA0SjAQIRv2Eo2EGpY5iXOYxdPf2xWpYmz236lAFpf5C6sI+bBY7ma5ckmyprCj/ihm73mK7bwPdPH3IdObgsnrpkzSYfslD6ebpg8WwUBUsozJYRlWoHLvFQVHdTr4pnU8w6mdQ6kgGpoykOlTBxqpVbK3ZgNvmoU/SIELREC6rmzEZR9HD2xebxR6PKRKNEIz62VS9miWlcyn1F+ALVZPt6kFNpJJsVzdyXT3Jcfcgx92DcDTEjF1vU+LPp4e3H72TBtLD049unt446p9PDVaVL+LjHa/ydcksImaYn435FwBlgSKiZpRMZw4DU0eRbE/d6/PCF6pmV+0WSv2F+MLVpDoyyHZ3J9vVDY8taa/bryr/miUlX3Bmnyv5pnQeC4uns6xsfouJ18X9bmZg6khWlC1kRflCNlevJc2RwfF5U8j19CTX3ZOhaeOxGE17eEXMCPm1W9lSvZZNVavZUrOWEn8+vZMGcdWgu8h19wSguG4X35TNY5dvKy6bG5thpyRQQIk/nxJ/IcGoH6thxWbY499lkh1pdPf05cPtL8SP98txjzAy/bD440DEz+qKRUTMCPOLPqMyWMqwtPEMSxvPn5bdjS9ctdfrtb+1JcdQQtdOSug6F5th54ick7FZ7KyuWExh3Y5WbWdgxL9se2xJuK1e3LYkPLYkTNNka81aCut2NPqylObI4qTu53FSjwvIcGbHl9eFfWyr2RD/wLQZNhxWFx5bMh6bl/+uuj/+Rfp7va5gbOZRFNftotifz8CUEYzLOrbRB2FVsIINVctjX5wcGUTNCHMLpvFV8UzyPL0YnDKGvsmDCUT99PT2b9P1ipoRKoPl+CO1BCOxN02PPYU0RwaW73wYByN+wKivSQIMAyP2XxgYGIbR6uOGIkH+sfLnFNRto3fSIK4Z9BOS7anxL5QtWVW+iMpgGcvLFpLn6cUJ3aaQ4kiPr8+v3UZpoJDaUDUeWzIjM759cw9GApiY8S9vbbW9ZiMzdr3F1YN/3GIZX6garz2ZwrodLCyawek9L8VudTRbNhgJEIoGcVrd8WRiT7ZWr+P1Lf/j6+JZ/Ouo93FYXKTsVsvSYHXFYl7Y8A82VK3g6JzTuHzgHWS6cpvd50fbXuKZDX8m05nLid3P44J+N7Z4/Lqwr9EXvj2pCpbjsSU1+sLUIBgJEIjWYcGK1bBS7M8n1ZFJ1AxTWLeDIWljm2xjmmabnl+BiB+LYWmU+DeImlFK/PlsrVlP76SBbK5ew//W/JbRGUdyRs9Lmxx/c/Uadvg2kWRPJcWexoCUEXs8tmmaRMwIM3e9zQ7fRmwWB8d3m0Lv7yRlu8uv3UY3T28gdn0qg6WkObOaxB+OhghGAy1+cYuascQyakYJRP2U+HcRjARIsqfQJ2lIm65hS6qC5STZU5v9ArevomaU9VXLyXZ1I8OZQyQaxsTEalibvCdEzUj8vSmWHNKqWGrDNdgtTuzNPCcBItEw1la8DhsEIn7KAoW4rUmkOjLadG0jZjie3EaJYppRTEy+Kp5JlqsbvbwDm31t7+6fK+7FarGQbE/nsOwTGJw6psUECuC9rc/y8qb/8MCEpxiQMrzZMqZpUlC3LZZI13/Z7gj5tVvxR+rIdffca9Kx+/3trMLRMEV1OygLFDE8fUL8fMoCRcwv+ozTe17a4nO2IlDCsxv+SjAS4Ojc0zki5+QOed3u8G2im7t3s8/xYCTA+qpllAdKYsnO8rsJRQIYhsFdI/9ED2/fRmW/+6MnxF6LbX1PME0TE3Of30ui0QjT89/CYXGSbE/Ha0vCYljx2JJId2Y3eq5993n1p2V3sahk9j4dtyMpoTuAlNB1HkfknMJlA25r9EFU6i+g2F+A2+ohSpSdvs0Eon4ynblkubqRak/HZfPEmwjsSV3YR2HdDmrDNWS58sh2dY9vUxEspdRfSC/vgGbf7HZXE6piXtEnfFE4jaNzTueUnhe2WPaXX1/NhqoVANw+/PccnXfaHvcdiPjZVbuF7p4+OK3uFsrUMb9oOoNTR8e/RLYkGAlQG64mzZm1x3IQe3NeV/kNG6tXsaV6LWf0vJQMVx4RM4wB8YS5NfuJmGECET914RrKgsXUhqsZnXFE/A25Lhx7LbY2wdj7MaMtJpO7fFtIcWSQZE+JLwtFgxTX7WK7byPZru70SRrU4pfBcDRExIxgM2x7/cJYG64hv3Yrpf5CqkMVBKNBBqaMoE/SoPivvoV1O8l192jhPL5NfHb6NtPD2y++LmpGKarbQZozG9duzw1/pK7R4z2JmhHya7dR4i+gd9JArIYVl9Xb4nM+HA0RNaN7fU20tO13E8IV5V+xy7cFixGrVU1zZFIeLMEfrsVqWElxZJDr7kltuJo75p3D+KxjOTz7JCZmHx+vkf2uqmAFt3xxerzmcFL2idww5BeNfijYXSQa5quSWdgtDiZkHQfEfqDAMFpMFr7LNE3CZojaUA0VwRJ+9tUVHJV7Kuf3vaHRPQPwhatZXb6EDVXLqQiW8Hn++/RJGsSwtPGMyTyKXt4BpDoymk2em1MdqmCXbwtlgSL8kVr8kTr8kdp4IpjuzGZMxpHkuHvE92ma39aoNSiq20lVsJyBqSO/vTb1NZMNz6ct1WtZVf41Kyu+ZnvNRrp7+3JE9klMzD6BpFbURrSHaZr4I7UU+/Pp7unb5EeTYCRAQd02/BE/qY50lpXO5+2tT9LT258+SYO5fOAPCUdD1ISqqIv4CEb82C0OMly5zb5eTNOkNlzNztotmGaUPE9vku1pbK5eTYo9nSR7apver6JmlLpwDXWRWjKc2W1KcvzhWipDZRTV7STJlkq/lKFA7MeJFHt6/AeeqBklFA1QF67FF66ixF/Ag9/choHBuMxjuHTAbS3+EFEXrmVxyWzsFgd2i4OR6YdhtzoxTZMSfwEryhawoHg6ayqXEo6G4q+vASkjuHnY/ZiA0+Ik3Znd5IeLYMQfr23umzwk/rm2tWY9vlAlOe6e2Cx2viz8mFA0yKCUkZza8+Jmf8CB2A9tC4qnEzUjJNlT6e0dSJQoNsNOTbiSmlAVgUgdoWiA6lAlSfZUslx5dHP3JtWR2eJ3g0DEzw7fRnb4NuO2upmUc9J37uG3SUR1sKJRM2eIfe7URWoJRYNUBkopDRSxqXoVr21+JF6mX/IwBiQPZ1LOiaTY04maUZxWN6mODNZULGFJ6VyK/fkA/GLsvwlHQ5QGCims20Fh7Q4K6rZT7N/F9poNBKJ+Tu5+ASf1OJ9UR0az59QWxf58Pt7+CguKP6PEX0jf5MGMTJ/EyPRJDEgZgcvqjr+H7PBtwmrYyHTmtpgQQuzHmc/z3yPFnk6WqxvdPL1b/d6213jrdrGqYjGrKxaxpPQLKoOlHbLf9lBCdwApoUsch8VV3+wr3KhmLN2RTa+kgfT09qeXdwAeWzIZzmwGpY4CoCxQTGWwlD5Jg9v1K3JR3U5K/Pk4LC76pQzb46+f9yy4lG2+9VgNG3eM+H38jd00TYLRAOFoCJModouzUQ1RRbCUurAvnliFokEiZiT+heGaz4+NN9+7ZtBPOL3XpUSiYTCMRvHs8G3i4x2vMLfgI+oiPnp5BzI4dTSDU0fTN2kIUaL4I7XUhWupDpXz8Or7669lFmf1vpr+KUNJsqVSF64hbIZxWJykObMwgNu+nILVsJLhzGFI6lhcNg9RM4IZ/3XZpCRQwPbqDVSFy/d4TbNceQxNHcewtPEMTh1Dtrs722s2kF+7jc3Vq1lR/hU7fBubbTaW4czhuLwzOb7b2fHrVRf2sbFqJeuqlrGxaiXhaAiPLRmvLRmPPRmX1U1NqBJ/pI4hqWOZmHU8XnsyJf4CfjT/AqL1TSVT7GmYmITNcPzLh2maWAwrJiY2w87JPc5nbObRDE4d3ewXh83Va5id/z5flcykf/JwDs8+iXFZxzT6lTAQ8bOq/GuWlc1nXeUyasPV+CN1BKN+/JG6FvvveG0pfK/X5ZzR67L4/rZUr2G7bxO57h70Thocf87sqNlEd29fLIaFUDRIYd0OPtnxKl8UTsMXrgZiTerO7n01R+edjt3iiDc12+7bwHbfRioCJYTNcLzZZcQMU+ovYLtvU7P9XzKdufU/EPSlPFBc31dmB6X+IiyGhUEpIxmTeRRjMo6ib/KQ+mZT5WypWcvq8sV8UzYPm2FjaNo4hqSNZUjqWJLsKYSjIZaWfsnqikUYhoX3tz23x+cXxJrpJdlSqAp9+1x0WmKvORPIdnWLNZtydacqVM7ikjlN+lk4LS4u6PcDzux9Zfx1FowEmFv4IW9sfozSQCHH5J7BFQPvJH23HzvKAsV8XTyLgrptZDnzyHTlEYwGYn8RP8FogILa7XxROI1gM307DCyc2+daJuVMZpdvG69vfoT8um17PWeAXFcPjsw9nZ7efgSjAaqC5WyoWoHD6qxPXrZTVLez2eM2x25x0Ms7INY8LlAEQJ67F+nObHbUbKQyVMbglNEc3/1skmzJfF0ym6+KZ+KP1OKxJWFgxJ9v32U1bIzPPJZJOScSqa+ZLazbUf+eW8DYjKNw27z4I3VYDStum5eqYDnlwWIqg2WU+AvwR2rra03TSXGkk2JPA6Cgbnv8hze7xUEoGsRq2Ojm6U0v74D4wAlfF89qsX+NgUGGM5eyQGGz70V2i4MMZw6ZzlxcVg8Oq4slpXOb9DlzW73x5rMAY9KPpF/KMMJmkGg0is1ix2l147K6cVrd1ISq2FW7hW01G9hZuzn+WnNaXAxOHcOI9MMYkT6RTFcuVcFyqkLlVAcriJhhiv27mFPwEeWBoibndVjWZKYO+1X8R6mqYDmf7HyVT3e8TmWobI/Pg37JQzmj52XkeXqxvnI5K8u/jr+2Gn5sBBiaNo5wNER+7dYW73tzbIadASkj6O7pS4k/n121W+PX3WNLon/ycFaWf73XPqMOi4se3r5Myj6R0RlHUBEsZVnpPL4pm0dh3c597nNqYMFjS4onrnaLA4fFhS9cSYm/oNHzI9fdk9EZRzAq4whGph+Gx5ZEWaCIaTteYdaudxiVcQQj0ieSX7uV9VXL2Vy1ul19vAyM+PGN+tYzrekDajVs9E8eRndPX7p7+zIoZRS9kwbitLrrP//C9T/imISiQWrD1fjCNdSGazCATdWrmZX/TjyR3HOMsZYSJtF4s85kexqZzrz6ZtI2tvs2UlC3HYfFyfisY1letiDexcNpceG0eshwZpPl6ka2qxtZrvr31oifqlBF/eugnJpwFS6ru/47QApJtmQshpWN1StZXbGYEn/Bvl7q/UYJ3QGkhO7AivWhOYUjc06lT/Lg+PKGX2/21LTPH67l3W3PclTOKfRMGhBfXhOqwheuIhgJsMO3ke2+TZQGCrAaVk7tcRE7fVvYVrOeTdVrqAyWMCx9AmMyjuQ/q34Vb2N9du9ruHTALVQEywhE6qgOlfNV8SxWlS+isG5How7VDQMCuKwefKGqRn2GDCyMSJ/A0bmnMyn7JLz2ZCD2ZfGdrU/x7rZnCEWDuK1J2C32Rl9KvbYUINa/x21N4ry+12Fg8HnB+/ulo/PBrF/yUCLRMNt9m9r0Qe20uunh6cvO2i373OHfbnEyOHU0I9ImMiBlBLtqtzAr/1221qxrpqyDUemH0ytpIBurVrC28ps9dhLfG68thWPzvseWmrWsqVjSJC6vLZmIGSbVkcnYjKOYV/QppYGWP8SSbKmkOjIoqNt+wAaDSLKl4rK59/jhamCQ5+5FZaisUT+hA6130iDO6nUlm6vXMDP/nWYH9HBYXGS7uuGyethcs6ZV/aNEDrQsVx5Ten+frTXrmVPwYZcdCfBg0dD/s8i/q9MPtCP7jxK6A0gJ3f4zJuNIMpy55Ll70i9lGH2ThrTYzGlPomaUGbve4rXNj+ILVfHM8XOJEmVh8Qxm7nq7/te99r0M3FYvETPS6l+3W8NucTA242h6ePsxv+hTCuq2d9i+RUREROTg1ZYcQ9MWSML0TRrK8d3Owml1xzq3YzBtxyuUB4rpnzKcHwz9ZYvtuIORALd9eSahaBCbxc7h2Sdiszgoqt0Ra/5jEB/xqMRfEK+FMDD40YILqQ5VtKnZx97s3mymo4SiQb4qmclXJTM7fN8iIiIi0jUooZMDymFxcVTuqZzc4wIGpoxssv64bmc1WRY1o5QHitlSvZZl5fPZ6dtEqb/w2+aGEfhs15utOr6JqZouEREREekylNDJfmW3OOmTNCg2ElPKcA7LmhzvF7Y7f6SuvkOtQbozi4La7Swt/YJvyuaxqvzrLjX5o4iIiIhIR1FCJx3Oatg4s/eVHJ1zGj29/Vscij0Y8TO74ANm7HqLTdWr48sthlUDB4iIiIiItIISOulQfZIGc9OwX9MveWh8WW24hrUVS9lcvYbN1Wu4fODtzMx/lxk732o0+mMDJXMiIiIiIq2jhE46hNWwcX7fGzinz7XYLLZGExhvqFrBH5bdES+rQT5ERERERDrGQZnQ2e127rj9Js6ZciYpKcmsXbeBv//zv3w5b8Eet5v+yXv07NG92XVbtm7jtO+dF3+8duWiZsv9+W//4rHHn97n2A81BgajMg7nioF30idp0LfLDYPtNRuYX/QZS0rnJjBCEREREZGu66BM6B76/f2cdsrJPPvci2zZto3zzpnC/x7+J1dfN5VFi5e2uN3vH/oLXo+70bLu3btx1x238sWX85uUn/vFfN559/1Gy1atXtsh59DVeWxJHJ83hVN6XEh3b99G66pDFby88T/M2PV2myZ1FhERERGRtjnoErpRo0Zw1vdO5w9/+jtPPv0cAG+/8wHvv/MqP777h1x25XUtbjt9xqwmy26eej0A773/UZN1W7Zu5d1mlkvLMpw5nNf3eo7LOwun1QXE+sgtL5vPYdmTmb7rLV7Z+N9m+8aJiIiIiEjHOugSutNPPYlwOMwrr307r1gwGOT1N97hR3fdRl5eLgUFha3e31lnns727TtYsnRZs+udTiemaRIMBtsde1d3fN4Urh18Dy6bB4DqUCWvbvovcwo+xB+pJdvVnWL/rgRHKSIiIiJy6LAkOoDvGjZ0CFu2bsPn8zVavmz5ivr1g9u0r4ED+vP+h9OaXX/euVNY+vVcli+ZxwfvvsZZZ56+74F3YWmOLH425l/cPPz+eDIXNaOsKFvIpztfxx+pBVAyJyIiIiJygB10NXTZ2VkUF5c0WV5cEluWk53d6n1NOesMgGabVS5espSPpn3Gjp07ycnO5vLLLuYvf/wdyUlJvPTK6y3u026343A44o+9Xk+r4+mMjso9jR8M+WWjRO7THa/x9tanKA8WJzg6EREREZFD20GX0LmcrmabPwYCsWUul7NV+zEMgzPPOJWVq9awadOWJusvu/L6Ro/feOsd3nj1Be6641befPs9AoFAs/udeuO13H7r1FbF0JnluHpw7eB7GJd1THxZQe02/rPqV6yvWp7AyEREREREpMFB1+TSH/A3qgFr4HTGlvn9zSda3zXpsAnk5eU2OxhKc0KhMC+8+AqpqSmMHDGsxXKPPvYU4ycdF/87dnLXaqZpNWyc0+da/nz4q4zLOoZQNIgvVM1rmx7lRwsuUjInIiIiInIQOehq6IqLS8jNzWmyPDsrC4Ci4tY185ty5hlEIhE+aKH/XHPy6wdbSU1NabFMKBQiFAq1ep+dydDUsdww9F56evsDsKJsIU+se4gSfz6hqAaNERERERE52Bx0Cd2aNes4fNJEvF5vo4FRxoweCcDqNev2ug+73c6pp5zIwq8WUdRMf7yW9OrVA4CysvI2Rt359U8exn3jHsVqsVEX9vFl4Sc8tvb/Eh2WiIiIiIjswUHX5HLaJ9Ox2WxcctH58WV2u53zzzubpd8sj09Z0K1bHv379W12H8cfdwypqSktNrdMT09rsszr8XD1VZdTVlbOylWr230enc3lA+/AarGxoXIFDouTk3qcx4j0wxIdloiIiIiI7MFBV0O3bPkKPpr2KXffeRuZmels3bad8845ix7du3PvfQ/Ey/3h97/h8EkTGTJiQpN9TDnrdAKBAB9/OqPZY1xx2cWcfNIJzJw1h135BeRkZ3H+eWfTvVse9/zsV4RC4f12fgej0RlHMjL9MMLREN08fbBabHye/z4ry79KdGgiIiIiIrIHB11CB3DPz3/FnbffzNlTziQ1JZm169Zz06138vWiJXvd1uv1csJxxzBr9lxqamqaLbN4yTeMGzuGCy84l7S0VOpq61i2YiX33vcA8xccWkmMgcHlA24HwB+pJcmeytqKpTy2Rs0tRUREREQOdsbg4ePNRAfRmXm9XhYvnM34Scc1mQy9Mzg69wxuH/F/BCMBHFYnpf4Cfv7VlVSFDr1+hCIiIiIiB4O25BgHXR86OXBshp1L+t8MQCgamw7izS1PKJkTEREREekklNAdwk7ucQE57h5UByvw2lOoDJYxu+CDRIclIiIiIiKtdFD2oZP9z231cn7fGwB4edO/WVu5jFx3j3hNnYiIiIiIHPyU0B2izup9FSmOdHb5tjAz/12iZoQdvo2JDktERERERNpATS4PQV5bMmf2ugKAt7c8RdSMJDgiERERERHZF0roDkHHd5uCy+Zhl28rNw67l5uH3Y+BkeiwRERERESkjZTQHWIMDE7pcSEA1aFy7BYHLqsbE81eISIiIiLS2SihO8SMyjicbp4+1IV99E8ZDsB7255LcFQiIiIiIrIvlNAdYk7pcREAu2q3YLc4WFOxlA1VKxIclYiIiIiI7AsldIeQTGceE7KOBSDP3RuA97Y9m8iQRERERESkHZTQHUJO7nE+FsPK+soVeO3J+ELVLC6ZneiwRERERERkHymhO0TYDDuTu58LwKbqlQBsqVmrwVBERERERDoxTSx+iJiUcyJpjkzKAkV8uP1FttWspyZUleiwRERERESkHZTQHSJO7XExAJ/tfJPCuh0U1u1IcEQiIiIiItJeanJ5COidNIihaWMJR8PM2PVWosMREREREZEOooTuENAwkfhXxTMwgJO6n0/fpKGJDUpERERERNpNCd0hYHxmbKqC6bveYmjaOG4cei83DPl5gqMSEREREZH2UkLXxRlYSHNkArDTt4l+ybGauc01axIZloiIiIiIdAAldF1csj0Vq8VG1IxSFaqgb31Ct6VaCZ2IiIiISGenhK6La6idqw5VEDHD9E0aAsDm6rWJDEtERERERDqAErouLs2ZBUBFoIQMZw4pjnQi0TDbfRsSHJmIiIiIiLSXErouLs1Rn9AFS+P953bWbiEUDSYyLBERERER6QBK6Lq4hiaXFcHS3Zpbqv+ciIiIiEhXYEt0ALJ/NSR0lcESPtrxEmsql1Ibrk5wVCIiIiIi0hGU0HVxqc5va+hqwzWsLP8qwRGJiIiIiEhHUZPLLq6hD115oCTBkYiIiIiISEdTQtfFNSR0bquXKwbcwbjMYxIckYiIiIiIdBQldF1cQx+6PE8vpvT5Psd3m5LgiEREREREpKMooevC7BYnXnsyADmuHgBs0QiXIiIiIiJdhhK6LizNkQFAMBKgV9JAALZUr01kSCIiIiIi0oGU0HVhqfEpC0rp5ukNaA46EREREZGuRAldF5buyAbAH6nFYlgoCxRTGSpLcFQiIiIiItJRlNB1YWn1c9BFzSig5pYiIiIiIl2NErourKHJpWHEbvOWGjW3FBERERHpSmyJDkD2n4YpC+YXfcYfvrmDqBlOcEQiIiIiItKR9rmGbvCggVxw3tl4vd74MqfTyf33/ZzZMz7ik4/e5tKLL+iQIGXfNEwqXhksoTRQQHmwJMERiYiIiIhIR9rnhO7mqddzx+034/P54svuvvNWLrn4fLxeD93ycvnVL3/KUUce3iGBSts19KGrCJYmOBIREREREdkf9jmhGz1qBAsWfh1/bLVaOf/cs1m2fCVHHnsKJ506hbLycr5/5WUdEqi0XUMN3UndL+CaQT/BbnEkOCIREREREelI+5zQpWekk19QGH88auRwkpK8vPzqGwSDQYqKS5g+43OGDhnUIYFK2zX0oRufdQyn97qUcFR96EREREREupJ9HhQlEo7gcNjjjycdNhHTNFmw4Kv4soqKStLT09q8b7vdzh2338Q5U84kJSWZtes28Pd//pcv5y3Y43bTP3mPnj26N7tuy9ZtnPa98xotu/D8c7jumqvo2bM7+QWFPPf8yzz/4ittjvdg5LWlYLN8e3/84VpMogmMSEREREREOto+J3Q7d+3i8EkT449PP+1kduzcxa78gviy3NwcKioq27zvh35/P6edcjLPPvciW7Zt47xzpvC/h//J1ddNZdHipS1u9/uH/oLX4260rHv3btx1x6188eX8Rssvueh8Hrj/XqZ98hlPPfsCE8eP5b5778HtdvHYE8+0OeaDTboz1tzSF67Ga0umNlKT4IhERERERKSj7XNC9867H3LPj+/g1ZeeIRgMMnTIIB7535ONygwZPJCt27a3ab+jRo3grO+dzh/+9HeefPo5AN5+5wPef+dVfnz3D7nsyuta3Hb6jFlNlt089XoA3nv/o/gyp9PJXXfcysxZc7jjrp8C8Nrrb2GxWLj5pht45bU3qaqqblPcB5uG5pa+UBVeWzJ1Yd9ethARERERkc5mn/vQPf/iK0z7+DNGjhjG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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, _ = nse.plotting.plot_history_metrics(\n", + " history.history,\n", + " metrics=[\"loss\", \"cos\"],\n", + " title=\"Training History\",\n", + " colors=[plot_theme.primary_color, plot_theme.secondary_color],\n", + " stack=True,\n", + " figsize=(9, 5),\n", + ")\n", + "fig.tight_layout()\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Model evaluation\n", + "\n", + "Now that we have trained the model, we will evaluate the model on the test dataset. The model's built-in `evaluate` method will be used to calculate the loss and metrics on the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "# Convert validation dataset to numpy arrays\n", + "test_x1, test_x2 = [], []\n", + "for inputs in val_ds.as_numpy_iterator():\n", + " test_x1.append(inputs[nse.trainers.SimCLRTrainer.AUG_SAMPLES_0])\n", + " test_x2.append(inputs[nse.trainers.SimCLRTrainer.AUG_SAMPLES_1])\n", + "test_x1 = np.concatenate(test_x1)\n", + "test_x2 = np.concatenate(test_x2)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m288/288\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 3ms/step\n", + "\u001b[1m288/288\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 3ms/step\n" + ] + } + ], + "source": [ + "test_y1 = encoder.predict(test_x1)\n", + "test_y2 = encoder.predict(test_x2)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
INFO     [VAL SET] MSE=0.0202, COS=0.9626                                                           4122487501.py:2\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34mINFO \u001b[0m \u001b[1m[\u001b[0mVAL SET\u001b[1m]\u001b[0m \u001b[33mMSE\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.0202\u001b[0m, \u001b[33mCOS\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.9626\u001b[0m \u001b]8;id=131897;file:///tmp/ipykernel_43488/4122487501.py\u001b\\\u001b[2m4122487501.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=360226;file:///tmp/ipykernel_43488/4122487501.py#2\u001b\\\u001b[2m2\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "rst = nse.metrics.compute_metrics(metrics, test_y1, test_y2)\n", + "logger.info(\"[VAL SET] \" + \", \".join([f\"{k.upper()}={v:.4f}\" for k, v in rst.items()]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Export model to TF Lite / TFLM\n", + "\n", + "Once we have trained and evaluated the model, we need to export the model into a format that can be used for inference on the edge. Currently, we export the model to TensorFlow Lite flatbuffer format. This will also generate a C header file that can be used with TensorFlow Lite for Microcontrollers (TFLM).\n", + "\n", + "For this model, we will export as a 32-bit floating point model.\n", + " \n", + "__NOTE:__ We utilize `CONCRETE` mode to lower the model to concrete functions before converting. This is because TF (MLIR) fails to properly lower the dilated convolutional layers." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "W0000 00:00:1723654589.802947 43488 tf_tfl_flatbuffer_helpers.cc:392] Ignored output_format.\n", + "W0000 00:00:1723654589.802958 43488 tf_tfl_flatbuffer_helpers.cc:395] Ignored drop_control_dependency.\n" + ] + } + ], + "source": [ + "converter = nse.converters.tflite.TfLiteKerasConverter(model=encoder)\n", + "\n", + "# Redirect stdout and stderr to devnull since TFLite converter is very verbose\n", + "with open(os.devnull, 'w') as devnull:\n", + " with contextlib.redirect_stdout(devnull), contextlib.redirect_stderr(devnull):\n", + " tflite_content = converter.convert(\n", + " test_x=test_x1,\n", + " quantization=\"FP32\",\n", + " io_type=\"float32\",\n", + " mode=\"KERAS\",\n", + " strict=False,\n", + " verbose=verbose\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Save TFLite model as both a file and C header" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "converter.export(\n", + " tflite_path=job_dir / \"model.tflite\"\n", + ")\n", + "\n", + "converter.export_header(\n", + " header_path=job_dir / \"model.h\",\n", + " name=\"model\",\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Evaluate TFLite model against TensorFlow model\n", + "\n", + "We will instantiate a tflite interpreter and evaluate the model on the test dataset. This will help us ensure that the model has been exported correctly and is ready for deployment." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO: Created TensorFlow Lite XNNPACK delegate for CPU.\n" + ] + } + ], + "source": [ + "tflite = nse.interpreters.tflite.TfLiteKerasInterpreter(tflite_content)\n", + "tflite.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saved artifact at '/tmp/tmpha_srwfb'. The following endpoints are available:\n", + "\n", + "* Endpoint 'serve'\n", + " args_0 (POSITIONAL_ONLY): TensorSpec(shape=(None, 800, 1), dtype=tf.float32, name='input')\n", + "Output Type:\n", + " TensorSpec(shape=(None, 128), dtype=tf.float32, name=None)\n", + "Captures:\n", + " 126079060503120: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079060505232: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079060505616: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079060505424: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079060505040: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079060493328: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079060506000: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043553232: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079060499856: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079060493520: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043555728: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043554960: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043556496: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043555344: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043557456: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043556304: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043557072: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043555536: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043556880: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043559568: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043558992: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043558224: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043554768: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043559184: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043561488: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043558608: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043562256: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043561104: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043563216: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043561296: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043562640: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043560144: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043562064: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043566288: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043565712: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043565328: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043565520: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043565904: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043564944: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079043566096: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064343952: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064343184: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064344720: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064343760: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064342992: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064343568: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064344336: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064346640: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064346064: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064345296: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064342608: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064346256: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064348560: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064345680: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064349328: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064348176: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064350096: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064349136: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064347216: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064348944: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064349712: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064352400: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064351824: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064351440: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064351632: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064352016: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064354320: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064349904: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064355088: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064353936: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064355856: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064354896: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064352976: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064354704: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064355472: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064357776: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064357200: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064356432: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064353360: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064357392: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079064357584: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063818512: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063819856: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063819280: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063820624: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063819664: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063819088: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063819472: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063820240: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063822928: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063822352: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063821968: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063822160: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063822544: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063824848: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063820432: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063825616: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063824464: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063826384: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063825424: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063823504: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063825232: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063826000: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063828304: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063827728: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063826960: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063823888: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063827920: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063830224: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063827344: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063830992: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063829840: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063831760: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063830800: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063828880: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063830608: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063831376: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063833680: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063833104: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063833488: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063829264: TensorSpec(shape=(), dtype=tf.resource, name=None)\n", + " 126079063833296: TensorSpec(shape=(), dtype=tf.resource, name=None)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "W0000 00:00:1723654591.708606 43488 tf_tfl_flatbuffer_helpers.cc:392] Ignored output_format.\n", + "W0000 00:00:1723654591.708617 43488 tf_tfl_flatbuffer_helpers.cc:395] Ignored drop_control_dependency.\n" + ] + } + ], + "source": [ + "converter = nse.converters.tflite.TfLiteKerasConverter(model=encoder)\n", + "\n", + "tflite_content = converter.convert(\n", + " test_x=test_x1,\n", + " quantization=\"FP32\",\n", + " io_type=\"float32\",\n", + " mode=\"KERAS\",\n", + " strict=False,\n", + " verbose=verbose\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "tflite = nse.interpreters.tflite.TfLiteKerasInterpreter(tflite_content)\n", + "tflite.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m288/288\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 3ms/step\n", + "\u001b[1m288/288\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 3ms/step\n" + ] + } + ], + "source": [ + "y1_pred_tf = encoder.predict(test_x1)\n", + "y2_pred_tf = encoder.predict(test_x2)\n", + "\n", + "y1_pred_tfl = tflite.predict(x=test_x1)\n", + "y2_pred_tfl = tflite.predict(x=test_x2)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
INFO     [TF METRICS] MSE=0.0202 COS=0.9626                                                         2850812944.py:3\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34mINFO \u001b[0m \u001b[1m[\u001b[0mTF METRICS\u001b[1m]\u001b[0m \u001b[33mMSE\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.0202\u001b[0m \u001b[33mCOS\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.9626\u001b[0m \u001b]8;id=955392;file:///tmp/ipykernel_43488/2850812944.py\u001b\\\u001b[2m2850812944.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=274114;file:///tmp/ipykernel_43488/2850812944.py#3\u001b\\\u001b[2m3\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
INFO     [TFL METRICS] MSE=0.0202 COS=0.9625                                                        2850812944.py:4\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34mINFO \u001b[0m \u001b[1m[\u001b[0mTFL METRICS\u001b[1m]\u001b[0m \u001b[33mMSE\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.0202\u001b[0m \u001b[33mCOS\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.9625\u001b[0m \u001b]8;id=777899;file:///tmp/ipykernel_43488/2850812944.py\u001b\\\u001b[2m2850812944.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=903182;file:///tmp/ipykernel_43488/2850812944.py#4\u001b\\\u001b[2m4\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "tf_rst = nse.metrics.compute_metrics(metrics, y1_pred_tf, y2_pred_tf)\n", + "tfl_rst = nse.metrics.compute_metrics(metrics, y1_pred_tfl, y2_pred_tfl)\n", + "logger.info(\"[TF METRICS] \" + \" \".join([f\"{k.upper()}={v:.4f}\" for k, v in tf_rst.items()]))\n", + "logger.info(\"[TFL METRICS] \" + \" \".join([f\"{k.upper()}={v:.4f}\" for k, v in tfl_rst.items()]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## ECG Foundation Demo\n", + "\n", + "Finally, we will showcase the foundation model by running across lots of patients and plotting via t-SNE to view the embeddings. This will help us understand how the model is clustering the data and if it is learning useful features." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAioAAAJSCAYAAADtQe4fAAAAP3RFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMS5wb3N0MSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8kixA/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOydd6AdR3m3n5ndc87tRfeqF0uyLLnIvWGMMcY2YDA9ECD0ZnoIIZQQSEJCII0QPkInYEiB0Du4dwPu3bItW7Z6vb2csjvfH9tm6zlXVrmS5wH53LM7OzO7Z3fnN++8845YfewpCoPBYDAYDIZZiDzYFTAYDAaDwWDIwwgVg8FgMBgMsxYjVAwGg8FgMMxajFAxGAwGg8EwazFCxWAwGAwGw6zFCBWDwWAwGAyzFiNUDAaDwWAwzFqMUDEYDAaDwTBrMULFYDAYDAbDrMUIFcNThisv+znr7ruNl77khYXpvv3Nr7Duvtt4z7veHtt+xumnsu6+21h33225x577zGdw5603su6+2/jLj/x5S/V6z7veHuab9+8nP/yflvI6lPj0p/6mpd9jX7F40ULW3XcbV1728wNS3mymq6uLd17yFv7vfy/l1t9dy713/p4br72Mn/34e/zTpz/JH7/iZbS3t8WO0e/TL//H53LzftHFF7Huvtv49je/ktrX7D4P/p1x+qn7+pQNhzD2wa6AwXC4cPELnsdnPvW3lEo2//7/vsQXv/z1GR2/c9curr/h5sx9W7du2xdVPKy58rKfs2TxIp594cVs3rL1YFdnv/Ced72d9777Ev7ff3yFL3zxq3uVx4rlR/DNr3+RhQsXUK1Wuevue9mxcyeVcoUjV67gxS96AS9+0Qu4/Y67ePiR9Zl5nPesczjt1JO59bY79qoO199wEzt37c7dv6tgn+GphxEqBsM+4DWvfgUf/8sPoZTibz75af73ez+YcR6PPrqBj37sb/Z95QwAbN+xk4sufjn1RuNgV+Wg8s//+HcsXLiA3/3+Ft7/5x9haGg4tn/hwgW89MUXMzk5mXn85OQUHR3tfPAD7+NVf/KmvarDV7/+Lf5wS75l0mDQMUM/BsOT5J2XvIW//quP0HAcPvjhv9orkWLY/zQaDR59bAMbN2462FU5aCxduoTj1x4HwF//7T+kRAp41rsvfvnruVapK668mi1bt3HySSdwwfnn7c/qGgyAsagYDE+Kj374A7zx9X/C5OQU7/uzv8gdutnXWJbFK17+El78ohdw1KojKZdLbN22neuuv4mvfeNb7NixM5Z+8aKFXHX5L9i0eQvnPyfbJyRv6ETfvmTJYt7+1jdx/NpjqVTKrH90A5d+53/46c9+mZlnb28P737n27jg/POYOzjArt17uOrqa/n8F76ce279/X1c/Pzncc4zzuLIlSsYHByg0WiwYcMT/OayK7j0O/9LrVYL07/0JS/kM5/6m/D7VZf/Ipbf6974dv5wy21Nr8H8+fN421vewDnPeDoLF8ynVqvz8COP8JOf/pLv//AnuK4bSx+U+6Of/JxP/v1neOclb+V5z72AhQvmMzI6yg03/o7Pfu4Lqd9ib9F9o9777kt477svCb//6Cc/b8kaNzgwJ/x79549e1WParXG57/wZT7zqb/hA3/6bq66+trUtTEY9iVGqBgMe4GUkr//5Md5+UtfxPDICO941/u54867D0jZpVKJr3zxc5z99KcxPT3N7/9wK+PjE5x88om8/rWv4uLnP5e3vP093P/Ag/u03Je/7MW885K3cP/9D3L9jTexeNEiTj7pBP7p05+kr7eHS7/zv7H0AwNz+O9vf50Vy49geGSEq6+9HikkL3zBRZzzjKfzSI7/wzlnn8Vf/eVfsG3bdh5/YiN33n0Pc/r7OfGEtXzwA+/j2eedy+vfdAn1eh2AJ57YyI9+8nOe+5zz6ezo4DeXXcHk5FSYXyv+DsevPZavfeX/0d/Xx+YtW7niymvo7u7ijNNP5ZSTT+LCC87jne/5M+r19LBRd1cX3/3vb7JwwQJuu/0OHn54PSedeDwvffHFnH7aKbz4Za9mfHw8dsynP/U3vOwlL2xZYIAnRo45ejXHHL2GBx5cxwMPPhTuu+32O1vKY4vm6/T6176a//jS11o6LslPfvoL3vSG17Jm9Spe/rIX8/0f/Hiv8jEYWsEIFYNhL/j8v/0TF15wHjt27OQtb38PDz38yAEr+33veQdnP/1pPP7ERt70lneG1g/btvmbj3+UV/zRS/j8v/0jF73w5ZkN697ytre+kXe+5wNcc+314bbAqvCed13Cd//vR1Sr1XDfJz72YVYsP4Jbbr2dd7z7z8LGure3h69+6fOc/+xnZZZz7/0P8MpXv4G77r43tr2np5vP/sunOefss3j9a1/FN775HcBrpG+7/U7OOP1UOjs6+Kd//tyMnGlLpRKf++w/0t/Xx/9+9wf8/af/mYbvx7JkyWIu/caXOOcZT+fd73w7n/v8F1PHX3jBeVx/w0285nVvZWJiIqzrpf/5ZY495mhe86pX8NWvf7Pl+uTx0Y/9De9519s55ug1XHHlNXvlTLt9+w6uuPIaLjj/WbzvPe/gouddyHXX3cg9997Pvfc/0PKwmFKKf/v3L/Dl//gc73nn2/jZz38V++0Nhn2J8VExPOX4jD8tNu/fmWec1jSPCy/wxub/+pP/sM9EyplnnJZbp8WLFgJQLpf5k1e/AoBP/+O/xhrkRqPB33/6n9m5axdLly7huc+5YJ/UK+C//vt7MZEC8OOf/Jz16x+jp6ebtccdE25fsGA+F15wHq7r8tef/IeYRWFkZJS//uQ/5Jbz6KMbUiIFYHR0jL//1D8B8Lzn7rtzu+i5F7Bk8SK2b9/Bpz7zL6FIAdi0aTP/+C+fA+B1f/LHlMvl1PETk5N89GN/G4qUoK5f/fq3AHj6WWekjtm5cxePPrqBnTt37bPzaJUPfeTj/PRnv8R1XY5adSRvefPr+dxnP8MVv/kp11zxS/7sT99NT09303yuvuZ6brn1dhYsmM/rX/fqGdXhO9/6au69fsvN1+zlmRkOV4xFxfCU47bb7+TxJzbm7j/nGWcxd3CwMI/f/+FWzjzjND71yU/wxre8g3UPPXmxUjQ9ORjKOH7tsXR2djI0PMzV11yfSjc9Pc2vfn0Zb3jdazjzjNP4xS9/86TrFXD1Nddlbl//6GMceeQK5s+fF247/dRTsCyLe++9n/XrH0sd8+CDD/Hguoc4es3qzDyllP6wy4nMnTtIpVJBCIEQAoAVy5c/+RPyOeN0T5j+8teXhcNJOpddfhXDIyP09fay9rhjuP2Ou2L77733fnbuSguORx/1znv+vHmpfZ/93Bf47Oe+sC+qP2MmJif50Ec/wee/8GXOP/9ZnHLSiRx7zNEsW7aEhQsX8I63v5kXXnwRr3vD25papv7ls5/ne//zLd725jfwf9//ESMjoy3VoWh68vT09IzPyXB4Y4SK4SnH93/4E378k/ygX9/+5leaCpW3v/NP+coXP8fTzjydS//zK7zxre/kQc1nYG9oZXryvHlzAdi8aUtumiee8Mz38/20+4otObFcxn1LQqVSCbctWOA1zps259dz06YtmULliGVL+cLn/4XVR63KPba7u6ulOrfC/Pneddq0eXNumk2bttDX25spOvJi3IyPe9elXElbYfYXb3vrG1m5Ynlq+z/98+cYGh6Obdu0eQuXfvt/uPTbXjDBRQsX8EcvfwlvffPrWbxoIZ/4q49wybv+tLC8O++6h8uvuJoLLziPS972Zv7Jtz41w0xPNswEM/RjMOwF09PTvP2df8pNN/+e/v4+vvWNL3HM0WsOdrWeFFIWvw7UAZrZ8fnP/ROrj1rFVddcx2te9xbOfPqzOe7EM1lz3KmsPenMA1KHmeAqdbCrEHLO2Wfxspe8MPWvo6O96bFbtm7j81/4Mv/qW3rOfvrTYuIzj8/++xdoNBr8yatfwYIF85/0ORgMSYxQMRj2kmq1yjve/Wdcf+PN9Pf18a3//BLHHXv0fi0zmOq6eMmi3DRLly4GvABnAcGQRmdnR+Yxtm03tSLNhO3bdwCwePHC3DRZ+1auWM7Ra1aza9du3vO+D3Lb7XcyPDIS+o0csWzZPqtjVFfvOi1dsjg3zRL/em/fsWOfl78vef2bLmHNcaem/s3EufjGG38HQKlk09Pd3Ffl0Uc38OOf/IK2tjbe95537HXdDYY8jFAxGJ4E1WqVd73nA1x3/Y309fbyza9/iePXHrvfyrvn3vuZmJigv6+PZ5/3zNT+SqXC8y96LuD50QTsGRqmVqvR39fHnDn9qeOecfZZlEr7biT4ltvuwHVdjj3m6MyhiDVrjmLN6qNS23t7ewDYsXMnjuOk9r/ohc/PLTMQY5Ztzaiuf7jFu07Pf95zMp1lLzj/PPp6exkfH+fe+x6YUd77muAc7Rme40xYtHAB4N3byeGiPD7/H19mamqal7zoBaxadeR+q5vhqYkRKgbDk6RWq/Gu9/4511x7A729Pfzn177ICcev3W9l/ff/fh+AD//Fn4WNCnhWkY999IPMmzvIxo2b+O1lV4T7Go0Gt9x6OwDvf9+7QqdU8ETDxz/2oX1az61bt3H5lVdjWRZ/84mP0tnZGe7r6enmbz7+kcyhpg0bnqDRaLD6qFWphenOe9Y5vPH1r8ktM7DiHHXkzBrKX//2CjZv2cr8+fP46Ic/gGVFImDJ4kV85C/eD8B3/vt7sUBzT4YPvP89/PrnP+QD73/PjI7b5p/jqhmeY8CaNUfx7W9+hQvOPy9TmK5ZcxR/+dEPAp4TcaPF5QZ27NjJf/3Pd7Esi9f9yav2qm4GQx7GmdZg2AfU63Xe86d/zuc/9888+1nP5D+/9h+89ZL3cOdd9+zzsj7/hS+z9rhjePpZZ/Krn/+Q3//hViYmJjjppBNYvGghQ0PD/OkHPpyKofK5z3+J0087hT9+xcs447RTWPfQI8ybN5e1a48NZwctWZw/pDRTPvn3/8jRa1Zz5hmnceVlP+MPt9yGQHDmGacxPDLClVddk4qlMjQ8zH//7//xhte9hm9940vcetsd7Ni5ixXLj2DtccfwxS9/nXe9462Z5f328it52pmn88//+HfccNPvGB0ZA+Ab3/w2j214PLee9XqdP/2zD/G1r/w/XvOqV/DMc87mrrvuobOzk6edeRptbW1cf8NN/MeX9m4RwCzmzh1k5crlzL17ZsNtN9x4MxOTk1x4wXn8z3e+wYbHn8B1XG6/405+VOAgHiCEd/3PPOM0JiYneeCBdWzfvoNSqcSSJYs49hhv6PL+Bx7kU5/5lxnV7Stf+yav+KOX0tfb2zTt29/6xsJVs3/xy99w402/m1H5hsMXI1QMhn1Evd7gfe//Cz73r//IBec/i2989Qu87R3vS01nffLl1HnrJe/llX/0Ul78ohdw2qknUS6X2bptO9/+r+9mhtAHuPuee3ntG97Ge9/zDk464XgWLFjAhscf5x8+/S/87/d+wJWXNW/oZsKuXbt55avewLvf9XYuPP9ZnHfuOezevYdf/foy/v3/fYkP+ZaKJP/wmX9l3bqHec2rXsHa447BcVweevgR3v/nH+HXv7k8V6j873d/QGdnJy+6+CLOPeds2traAPjZL35VKFTAG1J7yctfw9ve8gae+YyzufCC86jVatz/wDp++jMvhH7WUNSBZvfuPbztkvfy7ne+jeOOO4aTTjwey7KwbKslofLww+v5k9e/lbOedgann3YKCxcs4Nhjjsa2LYaGhrnu+hu57Iqr+fFPft6yNSVgbGycr37tm3zog+9vmvacZzy9cP+DD64zQsUQIlYfe8rscVk3GAwGg8Fg0DA+KgaDwWAwGGYtRqgYDAaDwWCYtRihYjAYDAaDYdZihIrBYDAYDIZZixEqBoPBYDAYZi1GqBgMBoPBYJi1GKFiMBgMBoNh1mKEisFgMBgMhlmLESoGg8FgMBhmLUaoGAwGg8FgmLUYoWIwGAwGg2HWYoSKwWAwGAyGWYsRKgaDwWAwGGYtRqgYDAaDwWCYtRihYjAYDAaDYdZihIrBYDAYDIZZixEqBoPBYDAYZi1GqBgMBoPBYJi1GKFiMBgMBoNh1mKEisFgMBgMhlmLESoGg8FgMBhmLUaoGAwGg8FgmLUYoWIwGAwGg2HWYoSKwWAwGAyGWYsRKgaDwWAwGGYtRqgYDAaDwWCYtRihYjAYDAaDYdZihIrBYDAYDIZZixEqBoPBYDAYZi1GqBgMBoPBYJi1GKFiMBgMBoNh1mKEisFgMBgMhlmLESoGg8FgMBhmLUaoGAwGg8FgmLUYoWIwGAwGg2HWYoSKwWAwGAyGWYsRKgaDwWAwGGYtRqgYDAaDwWCYtRihYjAYDAaDYdZihIrBYDAYDIZZixEqBoPBYDAYZi1GqBgMBoPBYJi12Ae7Aocy8+bNZWJi8mBXw2AwGAyGQ47Ozg527NjZNJ0RKnvJvHlzuf7q3xzsahgMBoPBcMhyznnPaypWjFDZSwJLyjnnPc9YVQwGg8FgmAGdnR1cf/VvWmo/jVB5kkxMTDIxMXGwq2EwGAwGw2GJcaY1GAwGg8EwazFCxWAwGAwGw6zFCBWDwWAwGAyzFiNUDAaDwWAwzFqMUDEYDAaDwTBrMULFYDAYDAbDrMUIFYPBYDAYDLMWI1QMBoPBYDDMWoxQMRgMBoPBMGsxQsVgMBgMBsOsxQgVg8FgMBgMsxYjVAwGg8FgMMxajFAxGA5BLLeM7bSDKw52VQwGg2G/YlZPNhgOIfpqy+itL8HyH12FYkoOsb1yP0q6B7l2BoPBsO8xFhWD4RBh7vTRzKkvR2KF2wSCdrefZVNnIox1xWAwHIYYoWIwHAKUG910OXMBT5zoCAQSm3m1ow9G1QwGg2G/YoSKwXAIMFBf0TRNhzNwAGpiMBgMBxYjVAyGQ4CS2164X/h2FYybisFgOMwwQsVgOARQorkCUagDUBODwWA4sBihYjAcAoxbu1K+KToKRYOqeaINBsNhh3mtGQyHAEOlDbi4mVaTYNue8mMHuloGg8Gw3zFCxWA4FJCKLW13oHwnFOX/L2C49AQTpZ0Hq3YGg8Gw3zBCxWA4RKhZE2xou5FJa5jAfRYhwJLYogupSrnHSmwk+fubYYsuektr6C+vpdte8aTyMhgMhplgItMaDIcQg+7RtMtBhIz8VQTQpeZRqXezuXQLSjjhvh6W0yOOoCS8WUMNVWVK7WKcLVQZQdFoUqJksHIynfYSlAocegX95ePYU7uH8cbj+/YEDQaDIYERKgbDIYBQFv3OCnrU4uz9SEp0sMg5nZLV5U1Vlp7FRaloiMgWFbrFYrrFEpRSTDHEbvdeGkxk5jtQPokOyytTCN0AazFQOQlX1Zh0tu6z8zQYDIYkRqgYDLOcDneAeY3jEUgUKnv2jxCAoKy6QEmw/GEhQPifCH+bUuG+duawxHomk+xi2FlHjdEwS1t00GkviY5PoFDMqZzA5KQRKgaDYf9hhIrBMIsouR30NBbR4QwgkNSZok32gxBxgRKIDlT4t/AtJwrAVZ4Hmi5S9GPxBIxSCgR0yHl02PNwcXCoUXNHsGRHwYRoz0vGEm30lY9luHb/PrwKBoPBEGGEisEwS+hsDDKvdgxgeQJBCGza/GizmvDQRYeUnoXETw+ezwrK3yzIFivh8cL75+/3nG5tbKsdgTZslGNVAegurWCktg6Fk5vGYDAY9hYz68dgmAV0NAaYVzsWoYmUFK4WQ0UIT4SonLRB+gKhoSD0Y0nuD603SssjByls2u15hWkMBoNhbzEWFYPhIGG7bfTVl9Dm9FFSHYTKQxb0HzKGcHKTAspVnr9KFrL4eBVkIoj5tWQVJIWZrmwwGPYPRqgYDAeBOdWV9DWWJLb6losiUQBNBUoit/z8soaFNOtJoE8Cx1yl9EyjRAJouJMt1clgMBhmihEqBsN+wHLL9FYX0V2fh1QWDVlltLyN0dI25k0fQ6cayD9YuYDMFhct+IwECEBlDOvEE2kzgfR8VWKYKcgwEDealcV1XaadXU3rYzAYDHuDESoGQxIFlXoHHdO9CCWplaaYaBtCydZWJy45HSwaX4vEDn09bLeNOdPL6Z9ehrBslMiZZtwMV4Hd3LUsrGnO8I7nn6LlkxQzQqBcN3Z84BLjCZRASCmElMzrOped4ze2EEDOYDAYZoYRKgaDhnQt5u1ZQXut219Lx5s6MyCWsKN/A1Nto/kHK7CcMvMnj46JFIicUwWWJzasDLGhWzdyh39059YCEQKeoNHESOg8mywzK4+g/ET6uFjxTCxKQkXOYbDzTHZO3JiZn8FgMOwtRqgYDAEK5u8+kkq9AwjERdBQWywYOQpGwREO45XdjLfvRgmXhqwyZ3I53bW5SCRYVtNyYkJEyvg04+DvHAOOUpHQybXJaCIlFChSxEWOLoiSs37871lVENp2FfxHQltlPu31xUzVNuefu8FgMMwQI1QMBp+2Wjdt9c74RgEIiZQSpbzhGssV9E7Np3d6gWcAkTKaUpyI/NoUqfmipIZfiCsF20JZ0gvU5m8KQqV4dQMlvUUK47ODiMVKSTGT+iaqJABXy39O9+ls3bMbV023nJ/BYDAUYeKoGAw+nVN9KNxogxAgJcK3TARWBiGE9w+8wLCBlaJwyCYHmeM0m3RgrZRCkRJsDlB++izH2czhniwyYqVkWVPCbb5Ycm3hWW+C6LjSYuHA8+lqX9O8TIPBYGgBY1ExGHykkoQSILQSeNFZ89a7ASKzxowLlE3ik/jbrbhQCnejCQc9VkqGY+xM0QVJ3jZli8zzFkLQ27UWgPGpdTMu22AwGHSMRcVg8Knb2nCFNmW3UKRA08itKZpNGU6SZ3WBmBeNPvQUhb6fWdV0lPZ2iIkUIXAlUVTbHHq71yKE6QsZDIYnhxEqBoPPWMee6MtMxISerJVjgiQzFThFJLIK/FZaVSqZNfGHk8IhJV8wKUhbcDKsPULB3P7zWj4Fg8FgyMIIFYPBx7Hq7O7dlNqukoIiGBaSMr5Wjm1H1o88oSNEtD5Pq2Job4dubJkfPl9LpxLfXSk8p9wgsFsQ5C1Io39PhPRXwtsfhN8v2T1UyvNnXH+DwWAIMELFYNAY69zFUNdWbUvCP8Xypx8HMUaE8L7b/hBHln9IYI0IYprYlndM0Zo+AbEpxdmEa/Ikj2sihKKgcBIlhCdQStITOLZMOOFGUWVC8ZORt/D/h4wsMf19Z2LbvYXnYDAYDHkYoWIwJLBVW+x7YFFRWZFcAxHSbLaPHqJeP7bomEDQkGHVCbKFtMUD4gHl8lZOhoQFKC9tIGIEqiyb+qaEsVXCIirMm/9cBgbPx7xyDAbDTDFvDYMhQEHHdB9d1TkkJwArFCKvgW5lCEff7TjxfcnhIkFkbQmm/WoB2GLDNeHspCZ1Cv1NQFnCs4ok461k1lugLFoK269XKVYHf0OlMsDA4LNazsdgMBjATE82GACwnTILhldTdtpDFxKl8ESA63oNdrNpys3Q5xMnfTxi+YrEZ3qPEkR+JEG2UkRlJCw80ZCNJmCU0tIn66AdVxQsLoPYUFTCobdcHsCyunCc8ZbyMhgMBiNUDIcGCtrHO2kf66RUK+HYDuO9o0x3TT6pKbgAQgkWDq3Bdive92C77iTbbFinaSMe5GW1vgJyzqydIMBbMOSkhEjMwonXK9evRA8qlzdBKM+QUnDOgsDhVq9MRHf3sQwP/yEnY4PBYIhjhIph1mNXS8x7YhG2U4pt7xjvQkmXPfN2MtE3ttf5d0/Oo+S2pXeIpPUhR1i0IjwS4mFvZvLEy/Q/gjV8ssqPRbct8Cnx1+rJKkIVHpe2MMWGpPAFS6xOYNkduXkaDAZDEiNUDLOa0lSFBY8vJrO7LwUSi8GdC6lU2xmauxMlW49NYjtl5o6uoL3ek8pXD/gWkidWXNdzes3bn/Rt0fNuRdwU7cscOtLqq5eXZwEJRAXapz+M5K0rpC2QmDVCpR8n8ESPn6lQSbEiaP0XMhgMBiNUDLMYe6rMfF+kiKxWW6nQEbVnfA4dUz1sX7iReqXaNG/LKbFozzFYKm6liTmm5jX+WdsdJ5qiHCtIm5bs+7oU5qNTOFtHtL6OT4ukwvGH5Yt4Ik0kqaRwSVl1fMuMtqB0qX2Q3rlnIa0yrltnenwD1cmteg0MBoMhxAgVw6ykPNHG/I2LswVKgAKlrWxsOTYLNi9l8xGP4lpu/nFA3+RCLFWK599kiCQXLWqrt0qhb0GxrHi6LCFTlGeCcKXkJtOD0wcqEPmzdmLyIMOXRWSlC7a0OuMpcEwWIKRNm72MYBXo9q5lOI1Jdm2+DOWYVZcNBkMcMz3ZMOtoSaQEuC7KdfFWMxZIVaJrtDi4mNUo0TM1N51/1nBPq+jDK7pIKYpAmzE9OIyB4ltKkvFTQp+UoKxgxeYigllAOekE/pRlbRXkvHRRJYFiLahNJhKEEW7980quAi3tduYueT4IKyMng8HwVMYIFcOsQtYt5m9c3PoBfgOs/IZbCEHneL5QKTXaWLLnOETerT9Ti0oQcTYQAa6Kx0DJI4hua8nYP2FbUawUXzwoW+JaAmVLlB/eXtky/fTmiAwB4PhB60j4ouD7kARDSE1Ej25dCQxIWYfoTrUKPFEj8vMXCKRVpmfglMLyDQbDUw8jVAwHHwVWzaZzTzeL1y+PwrDPBNfv3iuF3ShnJqlMd7F497FIZaGUm472GlgeZoIuDAKrRcFqx7G0yQi1IrI2eFYIGYa3D9cRCkLbJyPcBnnq+Wl1TGqE0ChieUJoxgLNDyCn+6xkBqPzyw6H1ZpEtG3rOiLK0GAwGDA+KoaDTPtoJ707+ynXykBxQ5aJHtQsDHUfiBboqPZSrrXTUeuj4nQmDlbhCsMiGEZpZf2dgGTamdTdKi7HC0OvUME1UUqbhlxQZo6zrhICEay/o6dNLlrYREjEv0X5hTN+Es62M0VIG2m14zqT4TZZ7kYIiVMbR8gSdlsvSrk0pnaDajL+ZDAYDnmMUDEcNLr29DBn+6C/3N0MRYoAISXJ3rcAqm1TtFW7mDu8Atst+eHvi4SB1rq6butiJWtqcCvr/jQRKfi1CRb1i8VDaUZBnBYlRGiNiUWPTR4PuUNI4XHJOoUWk4wsw09VaCkTeCNE5e6llLrmYbcNIK0yQmrrHYX+LgLXqTG9+0Gmdt5HUkYZDIbDh8N26Ocdb38z6+67jZ//5HupfSefdAL/851vcOetN3LDtb/lYx/9Czo62g9CLZ+6yIZF//YBgJkP8wBCWnHxIYT3XQgmusZZsGcVlmv7u5rnH/lpNHGoDfLKEjO2nZ7pk0WToaFwCCU2VZq9bovD6cG+QPKmNZO2fOjWkcSnAlyhR5wNziUQRSKzfmEeVvx7Zh1RIKBrwclUuhZjldpBRtcz/B39ITpplWmfezydC08vOn2DwXCIc1haVObPn8clb3szE5OTqX1HH72ab33jS6x/dAOf+afPsmDBPN78xtex/IilvO0d7zsItX1q0jnSFd8wE2uK9IcxwFsosFQiWrRPMTCyyMuylWm8gTOsd0S2dC/wJwmPDZxjWziXvGj1AZHl4sn7anjRZQkD0nlWlaCgxDCQ9hlztBWgJFHI/pgjSlQOIltPuZJIBGr7dcdcr6zAIhNZTYQuGv3vev5CCNrmrGJ66GGc6aFml8NgMByCHJZC5cMffD933X0PUkr6+/ti+z7wp+9mdHSM173x7UxMTACwafNWPvXJj3P205/GjTf97iDU+KlH28TeWbCEJUNLSmQQiBpcgfBD7bcwhCMlosgCkuGUmtqf/K5Htc2yzISOrfkLHMYCuelWhBnaP8PSg5WPWxBSoWiQFKwPpGKGJxUIGeWLMF+JKf0YfbhJeWliM4NEtM9LENU55pwbVFJ34lUubf1HMrH11oKrYTAYDlUOu6Gf0049mec+53z+4TP/mtrX2dnJ0896Gj/7xa9CkQLw05/9gomJCS567oUHsqpPXRRUJvW1dVqzHAjLauJrkiwnaM20f1nWkKLj8/xNkiJGinhcFCGiIZ7gn/9ducWrMIu8umVYMorqJIJ6JTMvGNoK7R5B5Nuic5ciEilBWVKgLBEFpRO+cNHNMNIbCgr/xYahgvOMplNHlfLLtnyrkJ9GCIksJR2lDQbD4cJhZVGRUvLxj32IH/zwJzz08COp/WtWr6JUsrn33gdi2+v1Bg88+BDHHLPmQFX1KU37eAdSb51aHeGQInJWDRrbPLEhZejfEAUcE/5MGuLDCq1MJQ7Is0rk1SMrby3uC8QNBQLiCw3qQkllJSZ1/cJdSX+SGaBEsW9PzPICOYLG+4/jTCFVG0XRcePHJSww+mcyHZ5FxW2YiLYGw+HKYSVUXvXHL2fRwoW88S3vzNw/d+4gADt27kzt27lzF6eeenJu3qVSiXI5is/R2WlWgN1b7Fopmv/Roh+GKNnRMI0+BTdPINh21NDqRYiE626rYqXIAbaFWTxhcVo9snAB4S8JkC1yiIsUPT99OjJEwy9JMdPyFOR8wuKb5SUVyLK/JlCT2VCQLU4KRIqXpaQ6sqHFmhsMhkONw0ao9PX28r73vIMvfvnrDA0NZ6Zpq1QAqNXrqX3VajXcn8Ulb3sT7333Jfukrk91hCtamokTpi+VEHaGL0mucNDS6kMGe4M+fFOUZgYoW2ozWFRMvAig6oxQsXu93X6aVPn6qFYCzypDFOPEDpxAiA2r5J2T8q1PRU6/RYJGAVhB/STSH2GOOeHmZaxVM/O7vll5ztP18W00JrYX1MhgMBzKHDZC5f3vexcjI6P81/98NzfNdNVbVbdcKqX2VSqVcH8WX/naN/nmpf8dfu/s7OD6q3/zJGr8FEYUO5PGqJSb98KTeVlWNNwDrQ3tPBkh08L4SuggK2WudUOhUDhs4w/YjXb65Wra5ABCBK2+fhzp8wtm9RDNoIlFnQ1ae5fIx0Ob5ROW0MKlEH42mWrGIntV58QIVrhcUfAdf4YQ0c6Um5G2O1A+kzvva15hg8FwyHJYCJUjli3lla94Kf/wmX9l3ty54fZKpULJtlm8aCHjExPs3LkLIJYmYO7cQXbsSA8JBdTrdeoZlhjDzJENuzVdYFnIGVor8EWoCnTKk5niq085zkL3mclp3WOWAMsimHkb7PCmDnsWFm+7xUKezi7nHra7t4YL/3XKJQyWT8I7pXRZ+jCMsjyHVvTp2UFDrwdHEdBwphDCQspSKG6EJhCyzixlTdFn4ECu0NFdamJDU3gCJbDkhImDz7xbQAoUiu4V5zD8wE/BbeQkNBgMhzKHhVCZP38elmXx8Y99iI9/7EOp/Vdd/gsu/c7/8PkvfIV6vcHatcfw699eHu4vlWyOOXo1v/7N5aljDfuWtvEOekb6Wkuc1SvPQ4hYPJW9EiiWjIuTZPwUpbxFB8P02hBTwjoRihjQrAFaix7OlNGCsPnllOhkof00djp3MeFuBmDC3YSqNRgsn4zAzh56kb4KEmGBaWJWCcW24ctRqk5357H0dh0bJdOtM9phceGlZ+YnahLvTs/H86cWkWDRhUpQVmIqciwjBQIJVplK/wqqux8uLtxgMBySHBZC5eGH1/Ou9/55avv73/dOOjs7+dSn/4WNGzcxPj7Ozb/7PS+6+Pl88UtfDwPCvfiFL6Czs5PfXHbFga76UwrhCuZuW5B2BM1CSmSrs0Qsy4sKqzOT4RzL8v6lpvJmHC9F5MSbEUdFKQV2wuk35zQUeCH7bTvuN+L/Odc6kWl3Fw7ekOSku81bSVlFDsGxGUJ6xk2ur1IuU/VtKOVZCccm7seSFTrbV0ZDLio635ghRottEvspW/XEDQ70j/GEXXYS7xqRLYC00y51LzBCxWA4TDkshMrQ8DBXXnVNavsbXvdqgNi+f/v3L/Ld//5PvnPp1/i/7/+IBQvm8aY3vJbrb7yZ62+4+QDV+KlJx1g3wpUIBCoW8SuBENEMn1Zm5QQB4PbGihLEN2k2cyd0qMiZaRTEUUnWVR/vSBZNgbOsn6LXWs0e5x4AyrIXKSP/qvgMGd0KFPmAZM20UcoFFKOT8Wn6w2N3MDb+ID3dx1Gy+1E4NNQ0pVIPdqknOj70GRHheYT1CcRNCwiSU5DTx4V5F3n2xlIaDIbDjcNCqMyE+x94kDe99V188APv5aMf/gATE5P84Ec/5bP/9oWDXbXDnnI1mt6t98p1MSL8kPah6HBVZMXIasxtG6GnnwmBSCnKP+sYMrSHZWXrrlaqlSw7EERAp1wQChVbdkeH6HnnHIujvJWkpUAIK3Q6briT7Bm/lbozoh1mARJHTTE0mo7wKkQJkMyZdw6l8pzU9Q4MMErTcFmnHl4jqTmoJIZ8Mo9pIlQak7vydxoMhkOaw1qovP5N2dOJb7v9Tl792rcc4NoYlIg35V7bJEK/kFyx4QYNecJnxLaKRUqR+BAi8jFpNv04mW346UeDkXqY933bs7dkGYs2HKZRaM7cBVaIcAhKwPDUfQirRMnqxXWrTFU3U21ETuNtbUvo6T0J2/biAjmNacbHH2Bi4hHtTAmHiPbsuIbu/pPp6FyB0ESbN8Moqk+WISm8bslhnL28ZvoU5trYlr3Kw2AwzH4OuxD6htnLVNcEqZWSfQtAU4uIHom2VPJiqwjp+ThkhYRXGcfqw0gznU2kVxloMM0eaz1VMRaFi0/SQhG5Z60Jsr6SFzF5urELV9VbE0VCoGxBX+/J9HQeR3vbYro6VzGn9wzKJS/wYf+cc5kz8PRQpABYdhs9vScxZ+CczNop1WB85B5GR++hWt9Nw53GVXVcW4SOsfpsntTxln5uYab555GXj/bpSkH3mudid82bcT4Gg2H2Y4SK4YBRbZtmum2K2Pq3vtlfBWHl8wh9STKmDOcdq4hm6QTWlcDxVneGddWMG8uaNc6I/QRby3egdNGTFW4/h9DnNTMCbZRPp7UIgYXCYaT6UNO8FXjxUzTfHeE7JkurjcG+Z9LTcyptbdkNuxCCSmU+HZ2rknvoHjiZucteTPecEyi1zUGW2sASNOpjcV0TXFq8qceuhRd4LlHvwFDWbP2hrOB2ygJKwstDSjpXPQthlVPHGwyGQxsjVAwHDgE7F22lVtEC62UMu2SKDoEnMISIZt8G03GDmS9ZM2Ag6n7bdny4J/iMldfaMMSEtYeF6jSWqfPixwjts6DxjXw1ckLza9dFCpuS6AJgtPYw4/VNhXVT4bVJ10sgkFLS1REN3WQhhKC7+9jYtu45J9LRs9oXPp748T5tSnZ3RiZov1NBWaF5JH29VHg+hDOE3GCYSUYXWwgJ0qY8sCK3HIPBcGhihIrhgOLaDtuWbWS6azomEoLGL/g7hiAUGOGucOpvhn9L0Djif9qWFwguKUoCMWBZfj6+aAj+zkAB03KSudbxtDOAQMY7+8m2VvPjUEJ4/4J9ljWDIago44n6E8VJ82bd+BV1g3o1sSJJGVknpFWho3d1prgRgU/MXgzhBNUKAtvphA7DMkoogu8ZIgzA7p6/V3UwGAyzl8PamdYwSxFQctoQtp0IwlHg+Co0a4mIhjQUQY884WgLnkCxrGyn2mCbP8so26ohvTgnwfAUiilrlPbSvNDXJrPGwj+XQJTkrRcU+mikr483zdcr11V1amosqjpOsdBIzmLSinXBC9Q2QwfWSscScn8fQArhjaCRkXWztYW0P5Sl3QcZwz3hAfqMIZ1WY+8YDIZDBvNUGw44vcOD2I7fWw+Gb/IawWDYQEo/WTxmSsySkbSWJId5kuQFbtOP8fdXxTjbyw/QXp6br6f0v3SxkFdG0CbL+D8lvGm+yp9NNNp4HF3NlO1ef12gAjJEioJCkRL3P1axLZ51pdhiIsLobImUvnrJq3Fs1pAU0bXI00W5elbgTA4X1tFgMBx6GKFiOKC0TXXSP+yb53ULSLL3bMnIIlL2FyZMiJQUejsYWFKKaGZV8K0gmyt3sLntNnqsZXheHk2O88tVrUx7dqNy8IeGsCSULH/IyqKnfTWD7acjhbe6d1tpQRQhNrD2COEJGyu/PNUkEJtu4BFCMDUV+cI0GmOhQ27ReQ9tvBLHrYb5gC+8NItZbMYOeOIs5nOSk33iXywzn9ru9cV1NBgMhxxGqBgOKL0jA9k9a314x7a86ce2jSj5Ad1k4LjZmkiYaWyUIjrdBSx1z6FDDRSKFG+Yx/eJabH8sOENhoksP0qudqwQgg57EQs7z0WKMkJYCOmLFAQueAsR2jIUI8mGPPT3aGEmUvA5OnJnuL06sQXXqebOzFLKpTq5hUZ1N3s2/MqzCAWLI2rXQs8fv06xKctN6uVKfKtQIICC8hWNid241bGcHAwGw6GKESqGA0plurNli4THvhEbzcvJp49l2Kpt5sKnhfRCTydl7lRdIQSWaKenvIpaY8gLg+831tgiukyBVSZWQOv10Y9TSl+N2GVk5y1AelaWUi7KbTC2+w7vu1OlNrGVmLkjMKj4/5T2d7TUdTGuRbQQYni+hMNgtbHNlBYejT1nqfFVMRgOI8zTbDigzFh2hFNhZ3jkTGahFMZvwbdwzKz4pvmmykl4vOaIla7SciamHyOsUDgjJiFORFzzKO1LUa1CdyEB0mqP7atObmJo2zXUq3vCbUopqhOb2b35MpzGeLh9fNstzbxovHD7UsRn/SQOCSxOrkXGopH+eQqBKgnKS46nsuxk2lc/k65TX4Y9cERh+QaD4dDAzPoxHFCm2yZpK7CqKBRCRjHWdYESrFWTR2zekOOkV1TOolm6YCVkKZstN+Ona13RFA7H5BRmyTKOO8XQxJ30d56Umt4cHBbmraIVCIJFCvOuYVJWdPccx/Ce38W21aa2s2fqciy7EyHLOI1JlFsliduYpDaxlXLX4mijXtXULCx8f534JpUzNBSen58GQEgRWWfsMm1Hnc00gsbuDZnnazAYDg2MRcVwwBCu9IKESQthef/C6cG27S0wGESNzTq+iVXF89vw0zQTKUGU29zM8H1NZPi1qPQs0SEg16oSC/hWmCCxWTkoXCaqj7Fz9HoUbsKfJes88KLC+jNpkn4iMedU7UQrHYtAZF8jpzFBozaUKVICGtN7UmV4M5pEvMygyMQ5C3JEiiA6F7++MasM3r0iELQdddY+81UyGAwHByNUDAcGBQt2rqCt1hUTHMKyPHEi/JWTg6Br+vTiVpDSC2VvWai8lYwhCqFvWVo4fd+JN3Bkta0wCm7LDqjBEFEWrpstWCw5MwuMUrGotNXGLmr14WiIJT36E1UwmFWECKdAh3XXDxD6ITalUn/L9UtSHXk8cvq1CZ1gQeWKptR06oyhIN0/JfiJ4gn0/ZLK8tP3+hwMBsPBxwgVwwGhc7KPtlpiyCcxsyX4FIGVJQ/b8ht5X1iU7HAlZa9xEtHwR3iM7UWnDaw4MZ8Qpc06SuybiUjJitqKZlnx/ylLeI12kUjJ2TVaXRf77riT3jUtyspN1DX41OO3JEQKAFJQahvIz7gJbm2MxvQwWFrBWnReJeIzd/S3UbA/jLMjEmkyzje61tpGBaW5y/f6HAwGw8HH+KgYDgjdE/2e/0kzLw9fMDT1Rcmxtuj5iyA/aB6q/skODwROIDn5BFF0AS9dM2tR0oVDKbZPXE9DTabKLbqugWAL45i0epp+vuXKIJOsa54+h/r0LqyO3sh6EqiJ4GKI2J8Q1Ff6w1WJiUNNJ4wF/4kJshlY5gyZqPY21MojUG1tiPEJxKOPI+r1g10tw1MEI1QMBwTLKWdbU5LRZFtY+0a4bmtr5ATjAjInjH5W+iRNjhN+EhwXLBlrdFXiEwB/ApEKzkHPX6loqCmIO6IUDWeKrZNXodCnCweHOMkS4vsBtxRYM3JPI35OvqgSCCqdi7GGu3HqexefpNyzhHApAb18/W8V+/CEip1R573Qkk9mDSKDf/+cfhLu2mP8e9K/X886HTY8Du0V6Ov2Pmt1eGwj1k23I5/ENXfm9eMeswLV0wnTVex1jyOe2L4/AxUYZjlGqBhmjGxYdI12Y9fKuNJFWZ5Dp2s5THaO4dhOLH3nRB+lRjmMeJpqOBJrwbQc1K1ZGhHvkXuZExMCsc+iNYGyigg+hSbB9OETRejcG8UN0Z0wVPzT369QTNS2MFZ7hJo7VHia09WtdLbnT8N1rcR55pEMyKZNe56z5Lns2fTbvRIrwiolNmQlCgr1/laS/OveytSrmB4WNMZ2t1pdg4YCnNNOQh1/bCT6w1lVAlYtD/2dBHhDq2vX0Fi7GvHYJuyrbkI04u8CJQRqsA8sCzE8ipiuedulpHHCKtynH48qlwjvRCGoHbcCRsYpf/cK5FTaeVtJgVo4gCrbyD1jMDqB6vNWGxcjEwjXCNVDHSNUDDOia7iXOTvmel+E8GbxiGgdlzm75jPaO8Rw/y7aqh2Ua+30j84PfUeC40KCaLQhM2yJ8gh9TXSLDZEVQ69HVr306rgKpAinR+s1VODtC9YkyhIkGVmmTicQKUrhUme4ejeOyp9REzBV3UyjMY5ldaRC3Ht1a16XlNXHijYqQEqbOUue44uV8eyMCksg7mdSkMyzvGQPUYW/Zs4tktUcKaWYeviGlmv7VECVSqgjV6KWLfV8t6rTUC6hLAvaKr7fVwlsCxXcU8E1l/4XEY3hieC309S6WrGE+htfjrz+VqyHHgOlcE84isapx0GnH5/HcZEPP451013ULz4bFgzEl3kIHI4k0NdF7R0vxvrd/Vj3rMddswTV2Y7q70QtnQsdbWROG6vVEaMT3jumUoLpGvKhzYhtw55I2jqEGOjG7WpDdJZhsobYMYIYmdrn192w9xihYmiZ9rFOBnbMizZoDaM+rNMz0k/P6ByEFb+9UgFIg1DzGZFOC9eVSa4OnEWwXwhv1k0wiweyLSoFKKXA8cVKsA1QwaydpPhpqrNEJNC00xdAQ02yc+r3LYmUoCY7h65jsP8cSnZ3/mynvKoE5wL+VGBtqxAgfe8WadE15wRGtt80o/xdp4olW3jNBMYvETV4mZcxU1np1i19JE0x/cjvoGYanQDV14v7nAugrc3bIHSBQWqoLSb1g316bCO8Zz8980yAZeOe9zTcY45E7NiBe8qx8WfdkrhrluOuWe7dZ2G+SitL+1QC56xjcc46NhRMItUZ0e9ooGKj5vb6uxR0t+HO69GSqtCZPCpewVQNblqHfGwnYnQaMdHq82jYHxihYmgNBX27BiLHTZmzQKAQXg9LERcTwerHAbozaYZYaCpWooTp45PfbQtXKGI1aNF5NrCYeC9GEb7/lEU0HTlcJRm9lczNM2YZ0JxaJhrb2TX1u9zj8nDcSbbvvoyeruPo7jraK15odQ6KSVg1VNDYh+vx6CeuKQcAIal0LUHsLKHc1pwo7a55KFyUCMZ04uXrhCFQmsXK0eqdzE4FU56VAtdl6sFrcEa2tVTXpwKulKjnXgiVSnjhwvsw+ajp+4W2P/mo+RtiYkVHgVowiFoy6B+QsJiEwkKEm0LhlLwVUt+1hMr/HmSVaXET+tPgC3DiIsXPTnSWEc853k+nvHvu8V2IH92KGJ1OZ27YrxihYmhK50g3vbvnUGqUw20pgZKc1qs7MVpW/L0hKJ6a6zf8mZFog2nAev76zB49lom2irHci1k9ShKfdqxUZEFJnI4KWs8sH5QwoQjrEyYJ/pCCdjkPMVVCsTezKRRT1c109R2bsYdUVNhoWAjfQpWRZaphkPQuuwBhl3Ebk0wPr6c6sgGUmzq0bcHxtC843rsu4fXLqHXQow8brajSWUsABVm4fuMZCTD9N5JUH7/DiBQNJQTq+c+Dim9JCbWBSnyPPtP3TUH+wThhosFXWflnHR+OMCUtM0mi3zn+SAfjlGHNoyOC+yt8Hn1xpFRGPMMg38QFEQqWD6Le/xz4+nWILcP5J2PY55g4KoZCenfPYXD7AuxGKT9R1gycYHjDn8kTe3UIbagkC90ErJeRE6sE8GKpBCIlMcQzc4milZn0pwFwXYJYLbG8dae9YAaTfh1EYsHAAN+iIYRFe2leen+L1OtD1LMcXsP3rSakdHGQ1Yho2kVo26xyL1apA7ttgK4FZ9B7xAWIxNCO3b2Q9gXH+0UmetDaJfLC44uUSAn/0to43fjkraBMJLISv5FSivLS4820ZB8lBO4Lng9z+hM7QKQi/2noVpS9fIiE/79sByItnW4YbVpWMjN/qCj4uRPPWKRPkqaT5GtIaa+OHHOOlPCWc6JZaYYDghEqhlzsWom+3V7Ar2Rzr5LWjDwB4Vs+ZryoIEQNvZTpBilWUSuqQ8oPRXg+Kln1KkAf9chOkOi1BQdlWVESL8gwjLwlwI6LIfEkH8mR4dtQSsVXOE4IgZgVoqARCid6aNuiyUBerBu7rZ/O+afGjmubf2xqheVYXfw89ZlFWcl0cQLedYtESv79IIRA2GWs/sWZ+59quOedC3P6yPuhhRLp5xvi98ZMnZ9mypPJ37euhX+Hf2j3fCp/b3/WqHH+q8o7RlkWfPyF8OazoaOcl9iwDzFCxZBL10hP9rizT2jWbyZCAqGgm+dbES7Cn01TNEyUFWk2loeWJmt4KjffFs4ro6iiPL3GWUSRdTPyrzkjMyozdXx1ByPDd4TleRXLOI+C4ZhM/E6xSr3YJZXe5QirEm6zO+cWCtOYSClIE4oVQbS+T5NZTOHxSiHL7U3THe64c/phyRIIPGPz+hPB0EkRRUbQIP+9InEvJhVqVnrd7JZU06m8ddmrZ7636si/O5cPwAefA3M69zIfQ6sYoWLIxa4X9BaS4qMIXdDoYqVIKEiJkLK5JcbOMO/rZWVFgA18XMLqKc9hDk1MBOIowyJSRBgaBc1yEvwT+AKFVGh4pRQNd4q6O9pSOUVMTjzCrp1X4bq15oln0LiInPRCSOy2OTPLxzuwMI0gLozCX6yFODpCCJSZ7YN6xtnpjVmXvdX2O9NfNnh6Cvbl/dSRmvb/K5r4s3gVTTq/xscLlT+WpEC4COl6S4j5/3TfnL2LSyfCT1GyPMuKYb9ihIohFyWd7B2ar0auiT+ZPhmBNvDnyDo+GO4pQkool9KNXdIvJCDPsdX1Vx8OFkMMFibMyif5t4YnTIQ/hVlFa/n4oiQUKah0Hv5HrTFUfM4zoF7bzcjInd7spCxRGFNU2XkoFW+7lF7nAlSj2vy+aEXf+umUhb/6c+u9dqUUjaHNrSU+TFG2DX293pfkdUtaxpIWkWZWDUXG8GIySTBDMKP8RD10oSMQ3jSwmAFE91crqJT/KAMIobJn9RCkEdFxwV+Ft61+UQRCuChcRG8Fjl1YdKDhSWJm/Rhymegep2u0N74xERslfIkUNWBBo58c8vEDqSWnMVNkSQmGcLJWKpbhGyq9L+lTQ/DV9xUJpxoTt6ZECf1slH8+RO8sKbyYKuE5kDo+XOsn8GPRxYo/m6juzjSYWjH16q7MayH8KiihogYq0YtV+vegMcnxTVWuQ2NqV/i9NvwEbXPX5NYrvA4Ft0zUHCTaSxHFskk1rOhNjwI3R2g/RXDPPA2SAkTHv7i5M3NcIgdVHaX/WWAtAZRULXSHfeuGiCYQC4T/rPn7BQSR5YSM1yHt9U0004f0IxBz2g2WrdAsLMU+KgohosItf16A/JNTECOTuL9/AvfGx6CR4Rdn2GuMUDHkMt0xyXTbFJXpNgpjpxQh/NWQ/b9DlEr5aYhSwcwinQIhMVO/khSiSR6+1SRw7lWQCPiWqE8iawWeQLPidReQXnDwSeI0xqlObafcNi/8zcK2SG9sstoaraerXAdlWwghM50sq+MbY7FVprfdTXnOkQhppe4VfXHEjPYlrCOgTVlNJAzKV4lPAiuQQtX27bU81FAAK1bQVBEmRWr4o/gbXRLWkoz8/E0qubHIkhKvKUhfKKAtsCkUws4WD1EtRHQDi2SKZo+y8o2kgUiJOiO5Lm8iO18BWHPasS5aAxet8c5k2xj1Xz2IemhXKh/DzDBDP4Z8BOxauA29axKFwcdrrINhkiyk9CLC5u0LyxHeasitDiPlOYc2EylNZqI0Q4E3S0cXKUlLUZO88nYrpbBkJWfv3jOy6/eAm9kBBdBnpwZDPfp+162z5/Ff41SHfVFGJOaEN6RV7ltOuW95lKdTZ3z9lSinEZuBpJTr9eAlqWnaMSs/fho/TopQZL+pksJF29bY9kjhdTncUYODnn9WC8I9EgbatjvvhUaVKD4+4Y+jUF4Qv+C6h/eE9rcU8eOCG0yvo1ChL5d+rBIKJVwoWEs0tJiEv71KvQJa7bOERlpLYVkq93WmW2ICkRNsV4l0QgjEgh7Kbz4d+2VrW6uIIRcjVAyF2E4JaZd8YaE9mVkxRnQH1lIJYdvZ1pdAmKR6Zqq5WNlXlpOgyBbzCkVKMn2WQNqruil/JeR9i+tMkRkqQx+jDxoJrcFRKFzVYOiJ3+I2JpgceigUDskhLYCuxWdhVfrC7c7kbkYf+AlTW26nMbaN+th2pnc8wOgDv/AaOb88Jf3gbUG5ElyLKJx/EGclsKQXXVotH9HZM9NLdVihTjmRhEqApAz1v4aiI3BUfWwD9h13Y/32Wmg4aYtFUpRom73fKWErC1WwVnYggCxtaCjW2hO6jTUnsIQkrkEL/Z68+0l/nQUubOG9FS8FSzqU7LSlxftbYJ+5DHnSolZOxJCDGfoxFBKMXwv9rVE0XRhoGjclJjYKB4Xj6DN4ZrjScaqK+C+crGnImWH5yc47mTZwwMipR967UwjJVH3fR1IVspRacyncBzFfFOVHmBVC4jYmGNlyA25jgrY5q+lcdFosUrA+6uJ1KV3aBlYzseUPAFidA8hKD259kvEN14PbCMt1nCksuzPKREbXJRxaCvLVP/MsKxre+QjsBSupb34ANT7U9BodbjhzB2Hh/GhD8EOFP5giCPvrOX7jXbhqFXn1Dcit2wGQ23cifvhL3LVrcI9c7llHJycR4+OoJfO9QZrUvU9kpgsFkFcJsX03pV9ciyqXUV3tNJ6xFrV4buLHp2ULZxzv5KLHMfk9+xgpZuJLkn56LekW9p2CbaWXHUf1zi0zKMugY4SKoZBapYojHSy0weJWFgQsInlsURwUPY2efzK962ZPRS4o1wu4ln1MKnx/3nnnDZ7nXCMB8RVi/bKmG9tpuBlRZZ8kKiO8fao+rsJ1ppkcfRghJPXpXdQmPdFkVXrpWOgFdNOvh34GnuleUupaiNUxQMfyp2G190X7nQbT2++juvVeRKkdq6KJFD0fPeOk0zNZzUQ+ynWx5x9JffzWGRx1mHD+s7KFtoYKLBq647ddCkVKeNjYONbNt2HdfJt2rKB2yR+HmaZ+F0FkMQnKdV3EziHEdM1btXh0HGd0EuWHeMn6cWfQ7/DLIB4SP/CdycxH6UlawO+woeeVHm7KOgZAVEpYzz4S56r1LZVmiGOEiqEQESxHq2g+ZVgjc50eAJnohYmMGT7JLkrSCddRnsBIzSJyI8fWrDwDa4AUqYiwicqnnU/zTzSjjqTEit6hTVi4QQhK5UHK9gC1xu6i0maOcqhObqPcPj/XyiWEYHL4QSZH1vkbJOWeIyj3LvXjo0Q9cP0UVTIAm9VJ9+rnpIPCWTZtC09AyBK1oQ3+Rr2O2vemfkYU/iBCBB16geydW5zXYYizYC60Ffg6Ca0fkXR2tWRrDXfg1KEiA02Qd+wzcYx1r+c3pGwL55SjcFcvwfN7Q5vqHGUiggJaFStCoNxoGEuEnQs9UfQktrLmqV4lXaQIobBkM+ks/P96sWTss47AuXr9zBS3ATA+KoYmDOxaiMybl1pEMuZFMFtGiFj4/dwhIn2AOGufm/G0+6vmFomUME5KQYPoDamr0MlPBXln9PJjViYtA+WSck51LX8qtJaP6zesQloM9p6NFPs+JPfk8ANetTIsXUopnPpEKFLs9kH617yc7qVnU+lZhlXuIgzrr6IPb/XoxPXwPRy9qaXRuSt/X2X+Mbn1iMz/+W9xERaevT+2Swhk9wDW4vxp0ocbqlKG889rmi7miBoerGB4pDVN4DgwMYU3xEnKvymGHxjSuulO5O5hlG1R+6NzaZx9ApQsQkuF/y7QH3vP97qZdTY8gfDeiRxd9TEl3/9GKCzbwbIVUqrw0c2/rfzjIMxTzGi4yEMKhd1XRizomvGxBiNUDAUIV9I51Rf2+lvG8qLKhvFOrOy1enLzzWmsFMEwQzDjRovmZFneGLptx4O1ZQ0ruSq3sQspWVHdbRkGTEg2sgKy1xLyrwOWRPn/QrEWvNhl1GgIIRHCpqNteZOKzZza1HZGd/4Bb8w++gfg1MfYs/kKAGS5m57l5yOt/GniShE5ukKmKIxWQ47SBYb2UvdCnMk9UfKZnoyuB1XG5uB6+n+Xjjw5HfvnMMW9+CJodYp/lrHzgYdaPtS696Hmz5AAsW039i+uw77dE8uNM45GLRwMLat5HZV4fJYcGRGIEaUQW/ZgXX03YmKCINBbMhqt9zpI5uNbV/x/MXnt+9hIXKR0kcJBSscXQv5E+6Y3sD7VWdH57jORC0zI/Zny1HiCDXtFudruh1OYYXMyk4ZBzzq5Ho82U6AhatSsKRxZR0lFt7tIM0FnvcTyXoB4IiSYMptML8j3W4l9UaEjqQA/XD6aJSjKMxjyyXIWTNa8rTyf8anWGoyZMD32GLXJrbR3r8Qu96KUw/TEJmqTW8NatA8eQ1N7eFavObk/COKn28z9+0hUuph+9Pd0rr3I2xVcP/z/FDhqZxldUoHpROKaSgtr7jKcbY8Wn9chjALcZ58L3a311sNYNhBdzE1bEOtan9Jt3fUg7oolqMH++JCwH8TRuulOrLvXITQRr4TAOXGV/xtnPAwa2l2ReIB0S62/f3yK8neu8oqfruK86LTkCYf3hUIgtYBtSrmpEe2oZp71pBSL5SJCi4qI1SPrPJSWzj+JNpuuPz+bsc9cj9ptlnhoFSNUDLlYbsvzAyPKJUSzWUFhAVr+QcwHrTzlWxpG7M3sbo8aGlu10V0PvPBouY5K4pubiYaggnLD9YhU5jAPRO/LwKoTNKxuYC3KCkSnHQtpYRKIHO+w9Cq2+xLXmWZi+P7c/ZXe5flDcT5J/5PYPn1/ljOnAqujH3dyiIn7L6dj9TMRpbZwtyvjyZOE1yrDYqPXIUwsAFchKh2F53Soo04+AZYtyby3UmmTO8cnkPevQ9y/DjGDhW9Ew6H0kytxTl+Lc+wqqHhDlmLPMNat92I9ujF9UEcFOtoIhozy3U98a4o+ayiVMBiHVIjHd4Rb5d2Po9rLuM9eGxfMUiDueRwe2466aC2iowJEMVNibnP6eSIBNyFKkiIl+NSP9LbHX4W+9UYKut55OmN/f13m2RvSGKFiyGW6MkksUmQegcAo5cRNyUpvacMzOYGpgnJ7G4uZbAzTEFUaYsoLBhXYcmeCpYmUoB4BSZGhW3Uy6q/0oawwLYXWBpVIk8xZKZfqvnamnQmiNV8kpfCGrVLHN8tfYHXOASFxx3cyfvsPaT/+eVidczwvgEQ8jdSVF5oQSsxUj5Ufa2kEqnr49lzd/j7UicdnjsLolyV5a8rv/wxRnYZaPfdnU4BaNA9nzQpURxtiYgrrwUcR23Z5OqLRwL75Tqzf3w1dHaHvSu5t4AQLmQYfOVKq0Ek1sU8I7DvjM2ms3z+MvOdx3LXLoK8TpqrIezchhrwlKtSj2+H9z4GS1eQV4rvBJvotukOtFN7Ib2Sf9cUIer8rfT6yvx05rxN3x0RRBQw+RqgYUpSr7fSODtIx2Z3f+Ab+H/7fIs/xNX1gdHwgUpqggIXV48G2UCgc6lqjVCAotLpGKxeLsBYqOC4wzYaWkhZ6lkmRgvY6EnE1krwqsUvqlxn4jExMPda87P2E25jCKjWxPoTW9/Rv3Up/XAiJbO/DndyDqHQiuweiKLValplDZcGn7turW1Wy7lXl4ux8ooWaHXooQJ33TH9NnDzTktbnD6+TghVLEXfnW9eUJak95xmwYjHBhVUo3GOPRDzyBKUrbkL4Du3CdWG0+TpVYrqG2LYbtbA/PrSj/2i5FpRkBT31YN3yEHJzWtyLyRrWH7KHssR4DfXFq+Ed56LaSs3FSkKoeN9VeMmD16DSbtpknlIopFDaq0VQOnMx1Z/v+2HewxHjTGuI0THRw8JtK+mY7EEgs98Xtu2ty5NcQDDWciSP9LcJbY9m4SjC65T4LwAEFmW9uxLlkcjHG4rwRUo5rcnD1G5CTWTkqwIrSrAScnKYCrxefhiXIvrUX8d1dxSEH9Q+nAXl9TT3jN2K4x68NWqmdz2QPSNHI/iJFU3nY+Qcr7B7vGBkVt/C7N8+uH1S0XLT9SDrU8Md2wNOPb3jMMBdtRJ6ugmHIXWSj6H+qRSqvb0w7/ozTvVEii7i/UzUqmU0nn5Ky/VUZRvVXvZGSh/Z7HdqYim0erd4V7kK+5d/wLrqrpbroSP2TMBl982ob6V/CUeV9D0i+VoI3lkKS3hWlmgypMJe3rdXdX8qYiwqhhDpWMzdtQTQeszx7oI3qyawSiS7GalpxzlvAZl6opuja4eiAHEJi4hLDWVbSLKHpfR+XVMU3pTcRKyWUKQE5SfrLEA5CldV2TZyFZXSXLraj6RSGkChmK5tZXzqEeqNkVZrsl+YHnqYypxV2JXezP0KvDeG1jDq104owoUEc1EghIXsGqSy8nTt4KaHeS4ngYBJ3l66VSX4U7moqdEmFTo0Ub09qLOfpvlWzeBgKRGT+cNhqq2COvbIaENS7Chwjz8K9fu7EPVG8vAQZ9ViGqcfg1o04G2o1b3nJ3h0Yg+fZg5rwZoi/7AO697HmyQsRjywFXXxCYisldijwkJhoa/v47unNcWWTmioDXP0+0X28l4G/u4ZTF+/ickbNqEm86/lUx0jVAwhXeP9eBIlblEQ/qeCaCVkHc2qkhvoTScQKa2GntSHmVohzBsmxW465ZLiOrUgmlLtYKYgyclDeYu47Ri7EXCp1rdTrW/PTnswUS4j639D19KzKXcvjt0HqWGWZCc+Gp4vbGiEEDjTI7QddRbe3NHWq+dKvIXqgrpk5k9UMSFwD8MQ+gpwnv3MmQn9BGJ9/hCjs2JxsfjxrSvOqmXYD2TPpmqcuobGs06Kmx3KJUB5/w8eOf3BasmZ1zvevn0fLDo5UYNbNqDOWJE7AcCLteJmON0KXKVSIkSvZ2BBSaK9nqCrQsfzV9B+9iL2fO423KHqkzypwxMjVAwhlZpmDhZajAP/yRLJqK8ZDbzw/S3yFiP04ojMYDaRbaXX+GkFX2BNid10iWW5yTxrSGuiKd4w69eheV1AUXcOrsWkJZTD+BPXUe5bQefip+H1JGUkUpR/HfQhLt+5trkzsUI1qriNKrKjN+VcnH8c2sq6iXITn4rgdlWef8rWw3Ba8ry50OtbvWZkEvQPeeSxYotKX08L9zS4CwZp2AJ36XxoKyMmprEe2ACj4zTOPdFPl5FR3krYumjJLV8gb38YMT7dpIIt8ut7vWHhU5ahQmdfL/AcO0ZRk5PINYPB5hTBPZx5mgXDWPH0AtlTpu/tJ7DnX271Im8bYhihYggJ1//I8v3IsmhkDQHp36V2nC5qgmGiVq0p2bWl6G2qUNSZZFxsZ0A1kCLnVve7RC20l1F9ZrwIyaFHbfgx3PoEbfOOo9y9EPA7vZDphxMj42Iq/+ipx25Gts8wOqcWYE4EMyxiQeeyD6vffxM0ajMr6xBADfrLGrQqlIPjUIipKuL6mwvTidFma04pz7J1wkocVkZblYt7zHLU2FjO46nCe2cvH2twXexr7m5SvxngKvjxHagbHoETl0BHGUamUHc8AeNVKn97YcGj7qnEdJW992iRUAmQQmH7CxvaSzpZ+NlnUr1/D0PfvA9Vm3kE3MMV40xrAKBtupNSveL1niGKLJs1LKI11IVDKq6KRE7W7JycY8MUtpUWSKEPStOBbLZad9BnHUm4XnyGsy1SehagwpwSDXS4I8NRI+t45VJrHHpDEI2JHYw/djVTQ4/gCDcKm6/3fnUKeveNsR1MPHgFjZEtqEY9fkwO4XVPJBWQXU5wi4EnPpWTn/khjDswQHrsLQcVv47iht81felb6zcmnlP/n8SbOpw3UU/4y2B3dyXGPKIaNB1lbeJvI6++G+HshwZ85xhc8QD87C649iEYnUYeOw9Rad6Xl0JhCTf8J1Ceb0rhK0phywYly0X/IYUUtK2dw4J/eDqyv2DNpqcYxqLyVEdB/8gC+sbmeT2uIgvKvkC3tuimXj0JpId8ZsgYWxm0j6ddDGaKKSVEtFZNE4uKwB9y8K+JwBtaCFdBDuqfFyhOSKrOMP19Z2Lb3ShVp1YfYnLiURpO82mdBxvVmPJnO7ViAQOUYnrLvTRGt+A6DtQmUE5k2XCGt3pDM/pslayRQvCj/Sa2gee4WyCWFC6yfwHuzk0tnuWhgdPXC0cu9760YkkJ0vhRk9XyJbBxc/Eh0zXEI0+gVi3T4guR/ZmFr0uEjPustTRPTAUHJztHLmzevW98U1pELuxp4nPnWUOCGUDBkKPnm1swBI7CEg62DMRbMo1AVCwWfvJMxi9/gpGfb5jx8N7hhhEqT3E6pnroG5sHEDlPHohhDSHjM4r0B9GSxSKlQOQE+zusRVgyo0fiixJdpGSNXMSP8euU3B/0NF3/nyQmVvSZKl09R/nfvbqXy3Pp6lyN41YZH3+QiYlHgdlpAajt2UDbwhMKnVh1FFDdkm+el90D/vzOjAOF5nIiou+p31tF6VOERrfDa3jOWbEMde4zNOskTS1SgLZKskKtWAbX/75pWaWr/0BtwQD06sN0qqXfX4+RojfCrQ6tike3opbPi5ayaDjIezZgX3VXGLvlgFAvfh6DKcegjcKJ6EZV/s0rwrhKwRvAEzjNh68F3RcupbKkkx1fuu8pLVaMUHmK0zs2l1T02VZe8K6LsJvcPkV2XpFM17zIkKQzbmI4SSmFVbTic0Go+6BqkRiJzkF/TyhbszrlrBUCeOH1bd9io7+0/b9tq42e3pNo71jG7l3XotTsm6LoVseo7nyY8rxVtPJDKbc4bklp4WqUchH6hdNH0QTa2knEf5AigapjSZgzD9E3FzW8s2mdZzuquwv1zLPjG1sRKbovDwJKLb7yGw60V9JWwgILWJREgYu2bJTSd+YfrxQ4LqUf3QQlC7VwDggQW/cgpg98LBzngR2Unn90zl4vgFux/woI4aDfwAKXstWITXUuGOBEIWg/rp/Ff3ECOy59mPr2wzfKchHGR+WpjIJKrWPmIsVPp1QTY25eoAFtGCW7XiouPlqoiy4oivxmwt55szQQE0RRmHYRFylZ1Qn+sKXv01G0SqyXvlTqp7vn+Nw8DzZTG2/x/mjhZ2mMFy8DILsHfB8oT4yElhN/+nEYOC8mZrV/UNxQ+ulEVy/2GRci5i5pXulZjnPSCS04eHhkixRC0dE4/QRUd/EKvu7yRd504lQnIjHcmUE8wIFvhUkE78v2MRIwNIZwXUS1jtywHfnY9oMiUgDU9nGcdTsygyA2eYUEOQACSyos6QkbRDDl2dsvhYslFLb0/llC91lR/n8Fbcu7WPqXJ9K2sntfnd4hhREqT2WyXhYtT/8lNhySIgjqlpVfM0tMCxRFUA36LrnMcEggFDf6+kQzyiC/rlG7K+joWIHIm5100FG+X0mTVEoxteF3xYlcx8+RsAFTgUDR20ER/6707S3gxfwR2Mc/HeTe+zsdbNz2Ns8vpYm4jt1lCZGiUJ6VSoB7ylrqr3kR9ec8A1UuZebnHLcynmNWy5xxWyu/rNDjOU9gZr46FAx2U3vXRTTOWoMqDMR2YKj9152onf56PDPpPGUQn6SlIr/0xGX1hpTiKzQLIRC2ZMHbjn5KttpPwVM+/LEaNn27B1n8+EqWbljF/C1L6RjvSr1Y2mqdZK3Z0pJFQ0qElAjbijfgUnjfU2un+/nZ2QsQxpLq6TNwXadYh0Dkg5K5swWSddSdClVx/cLhi+CzyfmGo9fSptK2qMUKHnjqwxkr4iZQrgP14mUAGrs3hssGFA3TB65LKtEjD7YFbSHap0o0iEIIRKmMXJAfS2c2owD3oguzh0dF+k9vTatogydQlHftAjHoH6FWLKV+8bMjp3C93Ln9XiaZv42uPLJ2CZRU6WRZ1rDkqIcloLcD97zjqb/3Bbjt5exyDhTVBtV/uY765Q+jqpHPilKqBd0ifP+UCCm82UCai1z8COEPD0mHdrtOm9WgIh1AISTYfWU6j5/zpE/rUMMIlcOM8nQbi59YQe/wAKVGGcsp0TbVxbztS5m3dSloM/t6xgezM3GDRiTnSUy+2IQvTgKBkvX0QUsiBfz3WNZMHfB6WZb3Jo71svU0OXZZz4m2+JbP8u/ETlhRmjhqCvDXBSpOF9YXr0FVAvoGn0Z7xxGFxxwspjfehnKdXGuWUorx+3/dNJ/Gtkf8JWcLppkGjVzWz6UJFq9g36dFP06vl+sgOnub1ms24hy7Bnp7og1JC0WShEhJLjcQPB/e9RKoeQM4Jxwdf4aEgLZK8fAaKvb8hc+d18vwn+EmddX3hzGciJ6Z9jKN1z8rvxIHkMblDzP9icuY/rfrqf3sfhrXb6D+yJ4Cy653VeKvSkVZRkM76VeDwhYOZcsbKvJ2i7hbj+tSWVo8bHc4MlvtzIa9QQnmb1uCUNJzVPSHXoLnoWO6m8UbV7Fl6XoQ0D5dMN7puqk1bYCZr9MTCBfLj33eygIZAErhCgfp36JKCJRteT1kLU1wjt5rwUFIGyyJSszmiTnHai8XvUOfdKANGz7NOqRbS0TyeH86oivx/FNyysnFf2n3DpxBrboLx2m+BLyw2hDSxm1MFjf8+wBVn2L8gd/QueZChB3v6bquy8SDl6OqBWvrCEFp6VpKi9cgLP93dVWmoFMKbf0kPQ/tUyWGgpICJjxGopzZ56TcjMaxa+DMU4tvnFAc+LjKf8ZU7FplDpn5s9Scp52Ec/wqxI7dqEVzoauDjKuYKFZoQzxaXXwzl2r1FaGnkQnnVCFgoAd3sBu5q1kQugOD2jqGs3UMB6hLAa87gfIJC7T3ZHTdvJk94ZFIovPLEikloafP7mgJKeg+bZA9v2hu3TycMELlMKJzvBvLsYnbhOM3fKlRYv6WIxidM4RstoJcYFkJLCF5M21apdWItH7gqI327yjTha3aGJDHZpi+o6deoRhWG2iTA7TjLYIWiolkeHdNrKREiv5d4AkObZuShEunRqMO3gtKKZcpZ4i28rx4/bTjRcbf0fn4/xHQ1beWkd2/D3cIaaHcqLEtdy6iY3AtpXbvXF23zvTweiZ33YtSDuWeZZQ65oGA+sQOaqNP7BMh404NM3bn97F6l1CeswwQ1PdsoDFSHJsDBJVjnok1oK27FP5AIqcRJb+RDvbpwiUnrRACd3S4Sf1mF43Fi+CMU8lUXqnnAHAVYsdOlOPA/AGolJoPuwQbhIKeTlRvF1FhovBZDe/+hMUAAItUDJViXMgwxAY4Zx+N+O2dB82pNhdXMXXpXTRO2EbbS45G9nrhEKTvYyJC4SIQuJQsJ7efJmNWlux3a2Bsqixoo/u0AcZuLXZaP5wwQuUwojLdTjjVOO65FSEklXoHnROO1z400xt6GHydVoTKTNb00VZeViiqYhRHVpmiSg/LARn502SYSgSCHnkEm5zrWGw/E0u0ae9cETcpk/hbKxu8YRslSa/SHGsYk3URIG3amJt6v+v9rOQwFRD3YfEb7kr7IqxSFx39x9LWfQRCWrhunerYRhx3ms7B4yI/D0DKEu39a2jrX42yvEUig/1t/atwGycz+sR1uPVxlFN/0qLFGdnE1EjrwdSsucuwB5fGNwrC3zQVbLhAeIT7VaRzyLneAfaaE2nsOjSCv6m5A3Dhs/KfnSwBt2MX9q8u93YLQeO1L4GOtrhlMItQOQt/mC2/kxM/zPdDiVUKsFQ4Lbm1J18F2tyz/moxX0KL2fFLcY5bhLhnI/KKexFTs2tZhPrd26nfvR3Z34Z91BwqawepnDCX4JpYwotAm3x16O8JL9x+PC6LZjCOhriVJ7wHLl5ihIrhECfpeKff9Xgvmc7JXkTgdl4kOoqmEQdDQ3kze/TIrc2ETdjwe13podJj4a4O5noNWjBEoAsLpUK/G4sSCodNjWtYWnkOUsjIMVd/O+jl+X8HjZvnxyJS6ZXwxEuqvvomvJ6mCIK/NTtX0MRKPCNplelf+lyEtAiWNZCyRFvvytBkL5CxBl75Vq8gvpSIAlkgrDZ6Vz4HLC+0fHXoMaa334vbxPF1X1FauBoVDCf655jslCv/urUobWeQEGR3P2LOfNSeWbhqtYYSAuc5z57RuaEU8uY/hF+FUohb7kade6a/gSZjj36CZJqYc4S+UaCSklvgiZRY2mYnkbYiKN2vQxLVwbZQJx2Bs2wA6+vXIKqzyLoioO3U+bSfswR7QQdCCiROuCKzFG6qnwTx17IgHd4pq7MTbKss6ECUJKr+1FgPyAiVw4haeRqhv+nzLAg+UcPmvw3cjJs+T6joT5P+xCUXIvRK8rZl5Z+sE4od1gO40mGOWo3Aoix6PAtH3rlIQrFSopMaY1TdIdrkQNz0nBQs2j4XPOHmz2AKenjZ55xff2/sHs1XIDokzM0vW2k7hYhbXZQFQtpE4ceT5WrHB3/oP1Xi/ERQngIhLSpzjqQyZyW10U1M73gAZ3L/9s5ER49vYdM3atVXhAHekhaSPPTrF9uecaxSLnLBMpxZLlScNaug3MJMF91JamQUOTQc5XHkMtyzTyFr1CifLDtUUFZyCEjFPijpgiORZS6BStXKFLpFhXSeQsCcLpyzVmFf80BR5gcOKeh903G0nTgP5SqE9N4A+psl55UDgBAKK7Cm5Fyv8PWKF9HWcX0FNxMxe4hjhMrhgBJ0j/YxZ/d873srwy3+sEa4tk+wDbKtDkksK/7kBfmkRArp/PMQknG5ne7SYjqYiwrUh8jIU887iO8hwFIVYIwxZwPt1mBcSOWgAGzpzdRJ1jtceDAwU0CzN0SekUqTbbioaCkALa0ClO9mFBNZGeIz1nSIeP6anTher8BqIbzXabl3KeX+I5jadi/TW+8qPK+9ptQG5UqulckNGt1kkLcmCOKiJMsgoCAqt2OGqzYfYFzLgqed3voB/n0j/3Cb91UIGqeuRZ12POg2j0JrSiKzVvYL7doXRWcN7kF9iCicGuR9RoZUhbCaG14RAk4/EmaJUOk4d4k/zENoQQHfskrQVwk6FSp6VflWpLJ0ouc2Q8gE6H2+kuWCUnSf2MvoLUP7/JxmI0aoHOLYNZt525dSqrd5G/QVsoqwrHxHt2aWA5ETaTXfpb05QjIhdyHLFdoZ9BuYFn1cNG/7BtMAVKw5cWGUHPoJZgpZvrjKm7asH6cUWlzwQgJdE7YR2gvYtUV89pO+1lE4nVSzpCTrkllgkKawbxxD+P9VStG+YC3O1DD14cdbOHJmlI44PjIZJYRFGCMlqzdecBKBCEkeFkxTTsYTAYFYuBjroj/Gvf8O1OMP7e3p7Dfc007K/Y1Di1rwT+E50I6MITZtpbF6Be4zTvOiyRIkbpXEGFwKv0ARS03wK2S/blRoIdF9LYT044/Eggd6IgXyTj/xS7eXcW2JbBzkYQ8BHc9amtqs8IMry2SsFe86BvqtLKMhodbmJojo+gg44m1HsqXzcXZfu3MmprNDEiNUDlHaJtsZ2LmQUiMwE7feiLa+MnLGWyMQD7G3j9/YJy0SLaIQbCndhrIUi3l6VHTQGrUcj8Shzji26KC3tCrnlBKrmkrRXNz5U7xVKIiK6+JKvDD7Wt2QwpuKa2ccqwmtpIUgc3A767TIeFdlCIPM+ipAuLSvPIt2noZbm6K+6xGqOx8GJ+kLICgtPYHS3BVglVD1KepbHqCx8zEyHXSlhb3gyJSwVRD5/GSdnj60kbErjGirbVQqIVBiasZTMMIuYZ1wBu6CJbi/vyrnihwkVq3M3KxAC9RGdL0sUCND1F91MfT1pA8kbXXKHcbJfSVEIiacjqwgcqzQ02ndfl9QJZbhSp8DkXgpvs0T+b/yDNT3fo9wDl4LLbvKWP1tGXsUQaC39DlFN2a0VpDSXgFFF0E/V4ESkmWvXcaSP1rEtp9vZec1u3Crh6fPign4diigwKpbWDUbFHSN9jB/6zLsRkb46+AlUkTRysQJRDhoLBDCm00SNjquZ4L0Ar3NQKTEhJJASG8GQSfB0FW0rxVU8JYrlRksnUi3vTw2IyZ9TsITHfoKvgV1V1LgBo7Hzeqii5Rg2CnIOy8cpZY2FqxuLwxTYXY525TfuLvC++eA112R0nPclTay0kVl8Ul0HfM8hB2tQC0qXXSe+UoqS49HVDqRpQqyvZe2VWfRccpLsBeuwepfHDs/UWqLYqbo/7SeYS5B26T9UwKU5f0Lj/f/Jf0bwn3JPAE5bxHy2JMLCj8IlNLPc0yk6OcT3Ccrj4Ce7HhImsEi+iN2PYKL4Q/PiNgvFKWSynOULYGwFaKkwHK9uCcxtyM/DwHJ6YReYDTvn0gOFzV9dWgCxa+XWD0fnpW3YOCBIS/YW/NGVcRFCpGWLijN832JpRHe67fNYvErl3Dc3x1DeeAgR/LdTxiLymxFQcdoF7275mDXS4RTjoUIx0LjPgza49HEx0S0alERFMdCkDL61ypBw21ZvnOt990VDdoJIuUmymtixAiiugoh6JRLPN+WZsdA6NfhWaKzz1NJvGi4qfmz2bg5YqQV64g3Y0hEloK9GULTy0vWTeD7v2hdXcu/Avqt5JcrK920H3EGk+uvBwQdJ14E0o6lCT8rHVRWnA4S3No0tcduxdm5AeXUE8NXic+iExDpnzHmaJto7ILRhKZ9bAVKKMTKo+H+O5qlPiCojvZ8dal/Jo8jMCM1K0DPI/i9VdxSo5Wj8MRJKBAhvMCBMSWVf8xCJtI7hRveeirxPLUyWh1WUfiJn3EU6vHdiEd2tHbgPkaN16lvGcde0BnzT2ntPLw7Nfx5/RvXt6uSvn5gy/SdrYh+jFJ/mVXvO5L7/3p2+O/sS4xFZTaiYM62eQxuXUCpXvZEiu78CulGVX+BB/ezrvif5IJafqHeP9v2en+Wv87PTPLW/U6kRAmoilGUUJTpzX7KVX79wz6WJt5EC0NgeiczN29BtDBaC2+fsB5NPeLySKy81OJ1Da+BLja07wpvVpPSO+yBValAPAkhsPuWUl60lvIRpyDsSq5ojbWBpQpta56BNXgEOA2UWyPdmyf9PXleKqo//jkUvbHCBrSFyybwguiJuQubJz4AOGsyhiqzLEKpJDMRs0L7UGlLTSBKpPK6sLHyE79f1vsGcurrtcLKFeFv6q0XqfztzR6NqAAhFFL6lhlLwOvOhFVziw7er0xe+URMpEBLtx+uisRZ3Gc+ebQnZkrSzbhGvqxRfjoJnUvb6D56djuN7w1GqMxCOka76BrRxpx9i4XI7MrkoIsVfeaKECjXLVijwierJyd9kZISSS1WSre+CP+RlJJpa4wBdUy+5QbAJX8hsERQunCZ+QJxE6RUKKq1PYBKXRMV+K60gAKKAv0W1SeWywyNKGGOIv49GEIK9we20xlaaYQQlJccT3lRczN70P4J3w+ovOJUrPkrvHD7e2kdUtI7F1eQWrcmM33x7jQdB3fdFGdwDo1XvQxOOj7fChd+C4ZWksM0zfDTBwsF6nF+RDKlyt0HOT9jC4JKTxTzuU+eX1bdw7IVluUipMKyPMFiWQLx2jNhSV+zCuwXpm/ZxsRlG7yaOt5ws+u0IrxEyok2HCnGezMF05ztHJHiPeKeCLSEwhYulnBYdckyZGnvnrfZihEqs5DuoT5U7AHNmWWjE3ZVNDGj8Ge0WN6/YMXjVkNbB+UK8kWKXnYRtp3yjQmmRvfII+hkXvM8tAV3I0uKJAi5n65WlqlU3++icNhTvYNtY9eiVCM6n/g8wmJi5vGCdAXWGy+b5kHlktlBNBwSWh50kSLwfDqexJMuZvqaEN5vKysd2EuPLvQXCtJnCeOY73JLjeFeUK3uh0xbwznnLNQLnwcd7Vrr7X2E91LwR9Y1Eq2N+ghteABB5GycJzrI2Lef2j0hXKSdtiIkHyShL1qICC0SAFgS8dazYX7B2mX7kfFfPMruf/oDU7/bSu2xEaoPDeGM1XKedW+bJd3cV4Xu2hb4sWhPe5hOopC4tFkNbOH4ofuhvd/m1M8dQ+cR7fvsHA82xkdlFlKerkQm3eQsFZFj7E3OwglXGc5IpxQ4Tr5TbdBA63bJDEe/WLlFFEwz9vtY2hh4fl4CUMHsIkgEOCPVnW6oKWzVnh4K8oXXdGMXQ9V7qLvegmebRn5Oe2kJHaVFVMpzEbQ1fT+roB5CIBzNKuOfR7hfj7GgVMpkr5RKDxsV+BqFosQmPvtFLzc418S+VEZNLBQEgoEWfmv/lgrEk+joSV3/mKUgVj/tM2iIkvVTGcclsmg67yEQA40GaueWZqn3C87aY1BHZc/yCXRF+BtmWTiC14EqFqFhh6dJjJrM+yVZraKfXjHjbm/oa4LSrAuuVgmllSuQUt8XvyWVlIjzj0b9zy0zq8Q+orFpnLHvrQu/jy9oZ8HHgpg48Zs2iFTrKoklm92t3mKGSrsjJK7/ClNULG9WXvK3KXVYrP3oSm774IM0xp0nc2qzAiNUZiNCZXeVpB5TOoMgRLkQCLvJT1vy9+sxPMKGQnjWl3C2ipX/lgoakiKRUeRs26wh1YoJe4F6KP3gU4po9o+PTTvjzlamne0IJDU1isJFihINdxJHTaXKmapvwlFV2rqWUvjWjtUpqAPe4LMlo7ro05GT56e0DylwhQqHToJLmry2+q8fiJRm4iHSGfHro+uPwlMNLRpNfh8/rdJ6+l6Y/4QFL6/XrmUWDqUFlYxPFGtah1bOyV13lyfYDzAKUCeubU2Yx+KNZDz9epseS6eJYRuIWST8NElrld5pz9K0zXRqoYjURUfiFMI+WFYF/GES6SaiLyTEjBSooxdAewmmDn54/ca2KUZ+sp6+lx4ZvgvCsG+OYHLzOLsvfYDlHzgOq7uU8nPxUFjCxcpwogWwpWcBzh6OE1htklP+/ihu/9jDNCYObbFihn5mIVOdk1FPKHjJy2BqcJOukT/Uo4Kpw1n7pdcFF1J6gsa2vH/+EJFIBoNrNqvH1cpJlpk3XBSgm7yb3Y22FQ+UFhvsDjIhsgRJSae9mHZ7AePOE9TcYeruKFVnd6ZIASiX5jJ3zjMhZfOIk+ztqqBcPXBc2BOOW0qCWUpK+o2xBVjC+z3811n4qg7f3dp1FcFxoknLoR3ikm9RyfqbyEgVs4Ak92uiJLDsxKYeJ4Yt8/KKV5bIf8G/RrHBgKQVXNPaobhJNrx4jbcSCuU6OPffgVp/kGZH9PdBpUW/naRIiV3brDTe/7x7REUiJfF8RZa+RFlBIbF7wTte6Nc2q6Lp0YnYBpFqcAX6NObwNw9vfK9cabk5r6D4ikNCCuiYPdNzx67czK5v3E9j60RoLFXTDcau2sSOz91FY9s0m764DlVzvRhLId65SxQl4cS26X9bIst3JY7dV+LkT6w85H1WDhuLyvFrj+UlL76YM884jcWLFjE8MsJdd93D5z7/RTY8/kQs7cqVy/nLD/85p5xyEvV6nWuvvYFP/9NnGdLWyziYjM4Zpn28M+wVKUDGeqTBS0G7uW07croNtmW5lWv7lKui3UV3fCsh9V3/QQqEULLcZt2x0HrjWycCi6jAc2oN88yvR7A5FA2+yOu0FzNSf5i6O5Jfvl/YYN/ZiXrnpdTKCT79l6zXUMtCa0e4arD24o9+N8IIq2EaorwFeHFEZjKnE6IudPIQ/6fLHELIaMhijVyiF50XHyVsZINjWqx2WFZQxyB7/+/A+BheRz3f4EcSfpPmuriP3ofa8AhMH5jFGDOxW49jFJASKST+Ds7RSm5X2elz89N/SOUd33J3VleHsZp7IfJFenvycJlIU3x7x8tTroKJ2bWy8tQdu5i6YxdWbxlRkjSGq9CI6jy9YZzHPnkX/ecvpOf0AawOG2mDLdzQiTYYTg6GfwR4IiVVmoptd5XABdrnl1lwTh9brhra7+e7vzhshMpb3/IGTjn5JH7z2ytY99DDzB0c4E9e80p+9IP/5o9f/UYefmQ9APPnz+O/L/06Y+Pj/Nvn/oOOjnbe/KbXsXr1Kl7xqtdTrzcO8plArWOaPQt3MGer52Ca3YGJ3soiGMbJeqq14aD4sS2iD7G0gp0xTJSYdZRZD12ASBkJllheipaj74rwPyil6LQWM9xEqLRXFiOlHW9Hs969JBoOH1f3MWkm/nIIG2LfGTYMMa/tC8ueaf55Jn2/pxwTGUozTOi6LdEzT/b480Yt8ywAedUEoqBuyWM0weIK0rFAsspVoEoW4tgTYM1x8MSjqHvvgPpBaNhGx1p6plIGiiKBp/x7Rg8mU3S9k2myMlSE1zZVVf25iJWTfGD8oHAi3d8RqdWWM2pReJk0kaIU3L8Vpg/+sE8Wzkj+fVbfXWXH/21g548fZ9HLFzPvgvmp16TApSQT11VEP4DExRZxfxfLj/jrAksuGjRCZTbwrUv/mw9+6GMxofGrX1/Gz3/yPd7+1jfyFx/5OADvePubaW9v52WvfC1bt24D4O577uNb3/gSL33JC/m/7//4oNQ/yUTvGNMdU/TtmEPnZF9oHchCQe4+wBMreY61lmb90AUFRG+IQOw0o9RkmCcg8RSqjG36Z/Tqa553qufpn4olmpuEO9qWZ5eSUWzQgCvwh3A0kZJjhZkJMctB8LOEsS1avA5El8AbahKxa6LAC8MuY+157NzCDSKaRZT3M4RZJxrAZsdl5eNmWV5i1oPW84xZcwBhWagjjkQMzEVd81toHNjGTVRrsHEzLF2c+1uGt5DUfrCicxW+RSXrmuUJkhasFYW3Wuq3SUor7wdKzr4TVv6QxcyMhJHfBwrUlQ+2euCso++0fpa9YRl2h+1dr2SfjvgyAwLXXzReIDSRknXtJNA2mDMZ4hDhsPFRuePOu1PWkMef2MjDjzzKypUrwm3PueDZXHPt9aFIAbj5d3/gscc2cNFzLzxg9W0Fp9Rg9+IdjM4ZyhcirUaFzfJXCRq9pK9Hsqyk70WqDv7+wHKjD/00W1hQiLTfSUZdRXAOTaZCh506XawIcFTznnO51N80DYSjECC8SLTpVZeDRMV1VYm/9XzRPwmmHDMzK0ogpIQvcoJpqdK3VgSrNMcPiRftx/OLDa00EWIia3+rggLtXPPSBaJDr1NOGYF1R9nxExVSQlcPrDo4YditG3/vTY3OvUcUSno+Na2S8qoKb6i9wR9uaPl4TaSEv4f3t/7ukr5IyRqRzgv8lq5DVJYQXgIxPg2NQ9NhtPu4HlZcsgKr3QJUbPKfZ03xLMkOwo+a4GKHM/284Z4gbZLwWks4/WOLaRs4NG0Th41QyWNwYA5Dw8MAzJs3l8HBAe697/5UurvvuY9jjllzgGvXGqO9e1B5b5wZtFvpYzPeFvpbRBcbUvoOt77wsG1vpdZK2Zu6bGuziLLyznuKEt9160rYyAqtU1ggAKKGPux2BBlTKRfHaSmXBrCsSmEaHVeAEyxomHluzesKnjXDtbyGVNkCLD/UfSIrT2Sky8nKPSaAApGCdk2ylEle/fSNBYKgSXZNCUVK0sEzK12WU2eqjXZxpydwhetF5M36iaRErDhqL2v85BBT01g/+gU8uiGaeaQUTFcRt9+Nqk7GV39uQSDOmGb5JdfkaZqRitU39QqQLjIxMpwlVoDMEPuxqqG0x1sge0uUP/gs5HHzW6nwrGLxyxdF18vfFrt22kVyAUuoYKKjH0uluP8SjAb2ru7kaX+9lFL3zH2kDjaHprxqkRddfBELFszn81/4MgDz5npryezcuSuVdueuXfT39VEqlajX06bgUqlEuRwNH3R2duynWqdxbYfRniF6Rvszek1P4u1VtDihLumTFo9S4Zrsaftt3rCOnq+U4fBW1BiJKC+lSbXguxDp97eVqJPwOnYVq4+y1U/NyR6n7WhbhlIuQshmrgBeebb/JmmW2K9DGBVYG8Jz/dk+yd9QSKGJrqBxzn8TZVbBv365KxQ3aYCSu+Mr8OYfH07L1s+9yTFBMjc5oS3rGJGzPYFz9c8Qa0+B7qWF6UR7h7eUwL5YYmKGiOkq9rU3oa672RP99QbC9Vpq54RVZNj/C1G6UIg2Zl+zvO3hzqL9yXQqEpe6wg2Oc12wVa57WXSKfoEqWrAv2qPCFPFjvO3SX2DUfs0p1P71WthzEJ2lZ0DXmi46jugEgkizRalF2PfRAnw3FZNe/G2BlFDpszniOX088sPd+6T+B4rDVqisXLGcT/zVR7j9jrv48U9/AUCl4vWYa7W0EKlWvaGBtrZKplC55G1v4r3vvmQ/1riYoYFtgEvP6EC4TQTdyqZ3qhdXRSnlLwQI2HaxX4t/XKzR0bfPlOQMJN0SEUzN00VKsqxgCElfLEQXL4H/RY51Q6Fos+fmChUpKwSvwWZnpwKREr5Mi7ozgKP8IRSvtqHFJGsBw4RfjgoEm0MqaJd+rcJ2NnkNkpaHvcDTWbEB8kLcqWFEV1/ieO1LgjyJEB6XtJwUlS/wpuZPjHuOsk2cHlSjcVBEio5QCqrR0KQ72O9Fq50BmSLFyz3qUmvXMrq2qYfb+5B47weVl6+fVotvEsX9ATTFIgSIDTtg+dxCwR3m6U+7TXZCols7fjPZgc+LEChHYZ11BM4vZ/fCfOV5FRb90RL6Tu7HVSrVv8onsCXpwq3g4fI3Cz9InJCClS/sZ+uNo0xsm52Ox1kclkM/g4MDfOWL/87Y+Dh/+mcfwvUb56ofLrtcTjsWVSqetWR6Ojuk9le+9k1OOeOZ4b9zznvefqp9DgKGBnewadlD7BnYxkjfLpyK8sJHNztWD/4mpXdMkc+JTrjwoHZ8sxd7pj+M9oaUQnuNJdMl0mf9HRvAFWDLyEcmWRXwfEia4DhTsdok6xdYOFxbtPCyTRNGWQ3Ehl3ci1f6H0E6l/QFC4fnRP4wVF4BTfRVzKKTVEgZVVfKxR3bzfRdV6JqU9q0Sn+/SB+qD4Fl1keRW14ujuerpjZuKFwpXLkubHxsBhkfGNyjj2w5bRg3BbJFnFKeUE7sU8FFVdpxgtA6EruNUvedf2wiCJt3KyqE5SJLCmkrpA2iBCye4wdlbH5GelW9v721bKR0kX6oeG+dHycSKUF6SyJXH7xFCluhY0Unaz5xHL0n93udSH/73vT/pHCRhQ+Hb3Eiuk7SFpz3L8tY8oyDs+TA3nDYCZWuri6+9uXP093TxVsveQ87tGGe4O+5/hCQztzBQYaGhzOtKQD1ep2JiQnt38ExLTq2w1jvEMNzdrKnb2uxVUQIKJXCNOEnaU/8TMolwgX/Yr3aveyiC1CaH4pnVZAofygpaBhb8mdBq1KOxSfWGxOSaiPf3Dk5vQE91LteWqwxlSK+o6i3HvwLksjgNezvLThW5Hzq8eG9zqVqQTg2qWBe+ULbnZVO26Zch8a29UzfcwXUpqje9hvckR1eHpb3T2+BYkIow1IU/BOZP0TO+QS7d271/ti5DbVzmydIkmlcFxwH9VDaX+1g4w70tZQuFh4/dg21H3Z0HOsnV+E5f+hDKb7JIvT38acRp3zfU8rH+7CyhnKUFy8ly3++EnWW8m9Xb8pt3D1OYfsLEAavIu/vgqGSJ2lB3K9IWP6uVciy1N7LM8nAW98n+I1lTNhldwMsFJYQ/nH+syDgpHfMo2fZ7AmQV8RhNfRTLpf58n/8G8uPOII3vfWdrF8f7y3t2LGT3bv3sPa4Y1PHnnD8cTz44EMHqqr7hImOYSYn+2iv9ngvnuBJVsRD4Gt4PiCq+bOcN1snLy6LbunIcyCV0rN+CIGrGiC9dY6DSK2ZlWrlKXZVJCC09IJAGCnqjheRNo96Y4TxyUfp7FiB7gekm59Vln02JxheNCTlZxJGho2C+KVCyzchrItuDWliGfG873Ly8o/VDccx673ITh9DQe2Je6hvfgAa0fCFqk7S2LwOd3ocuWRldJ6aUEkSDmVp56bfSsoiDHiXRWC/UWPD0babr0WcdhYsWuZP+1SelWVqEvWH62FiLCe3g8iC5haBcJXjLAtKowEbt2Pf9zDyiW1eQ/brm2k8/+mJ9KqFrmrG0FDmar7ervwwR95dpftXJLoS6EIFQAg3Y7KhfuMHn1FllOOi1u/JPZuDTc8JfZT791YcKGxcf5TbO39XCSypsPxhcCc2LKSw8ESdwpvKbAuFEE5ohTn6j+dw15d3UJ92cWfxSNBhI1SklHzuXz/NSSeewLve+wHuvOuezHSXXX4VL3nxxSxYMJ9t27YD8LQzT2fFiuV869v/cyCr/OQRsH1wA31j8+kZG8Ry7TCselED6PWmClo3KaK1gJrWIRBHKr5N+66CPO1I4Ehhe012IFKKhlOaWB5yGz3fauOoKrsmf9/0VIbHbqdU6qVcmhOKlcAiEvqlJMtWkcUk1WZYRL4zVlgpb/ptC0Mv+mesTD1dfRolFKLcQSCCguMCq0he/kFeShBGesX/HjtOG6qL5aFc3NFd1J+4O/Z7i765lE94JqLSjtLWvC865XDKsdLOO6hHYltWTLPI8qM5ZAM06qjfXYfq6oYFi0FaqOE9sGNrTk0OLo1KuXWLQEa60n/+CDmdnopvPbwR/us3OOeejFo8F8rCW3RcBY2afmclrmrsfsg3ZwkRj/WRXWH9ztbuGamLEt9aEPzWmflFwscLfBZ1VJzfbcit48Gm44hOVMNF2JECax47xjs/iaIsHT9t8EwJfxJAYFgLZgR5kW2D16ofmsXPSYSzhxad3M6yry5FKJdNt0xx/w9GGNk4+xTLYSNUPvKhP+P8Zz+Lq66+lr7eHl508UWx/T/7xa8B+PLX/pPnPfcCvv3Nr/Dt7/wvHR0dvOXNr2Pduof54Y9/djCq/uQQMNyzneHuHZQaFeaNrKTstjgjSRcYwWfJbh77RLeq6O7nsXp5b5lweMdKiCd9xk7Q4ueVWbAv97Xpp685w+ycvBFXtfbw7Rq6jsGBcynZc7zGs8l6Op7VBi9wmvaeD4OraXWJHB6Fb7YXmY13SqRo5SsAF5z6GJNbbqU+4YntnhP+CCnjvlfNpvoGs2ziiwgmxFBqqMv/Uyka2x+jtv6WuEjp6KF8yvnRfSGt8Likj29MGEntb5FIlAz+Fty2KvH7B9d2ciJ9suNj8MghEBDslGPZ67EL10VkiJQAOTSGenwrzvLB8McIn8nYhdQ7HcnqqMLqtWQgFHHLSZizLzp0F7RWEAJcx1tZufGje1Dbx1s78CCgnHSwmMA5tkiU2TiaSIkf3VCSEq7X20BgS0+kJPuOQnkxV6Q//KP894+DwBaCJWe0s+iUdq75u+3sfmh2LUVw2AiVo9esBuDZ553Ls887N7U/ECrbtm3ntW94Gx/58Af48z97r7fWz3U38Jl//rdc/5RDAqGol6YZ69jN4Hhnk7SkBUb42USkxPIp7PJEaUpW9rutlTdS3FaczgIKnWXHaxtaFilecQ47d11DZ8cKurpWI62u4vQQ1V8KbVtQwahuQkT7RSBW/LGMzL5sqpHw90kXJRVWz3yUhMboVpzqCHQMpIZYkkooChxHVK4WiCEYkkJpjRiasFCasClXwI0H2bKOOAaEjPn7oB2r1yVlNUmKFOLpU/tTCs9TL2rL4xyqqBbWAcqMqeS6yEc35VuspKT+snNQR8zPvna6xSTzeVSxdOnHXuVsTyP90O6ZJfjHz+wVpBC1BrWv/h61ZbS1Aw8So/eMsPAlSxJbBa6SSOEmrp/3UFi5IsU71hMcwRo/bmYAOAsnFC/RZC5PdHqeKwJLgLQVT3vvIL9835Yi49kB57ARKq9/U+tThx9Z/yhvfft79mNtDh4NO3vWUgwhCsLiNxmTCI5v5W0SWE1aDcGfhzYNObUSL+RWV+EyUd+wFwW6TEyuZ2JyPYPzLqRU6S9OLkibC6Dp9QmGaZzGFNJu9+ucaMD18xXg2oC0EHY3lfZjaVtwHG59iuru9VidA1FDEqsbxBxSrChN5kKEROIpZvnQ6iaEwBpcgrX4KJxtj4HjiUFrwXKQ0TyEWPyVrMuhCY/C37OlnrpAjY+Gs34ORcR9j6COKw48GYrcAF9YWrfnOwY7p62OREpL6L++fm+HrVxKlCgXZGGLEq1Pk340fKuCdsflix59nRvfz6u9BLXZH5l26vFJxteN0rmqG6H5vCkEjpKek7ByQXjDOLZsYNE88F5DQVl4s6OSZIXY1w1p2ogrQgg659rMP76N7XdPP7mT3YccNkLF4DFVGo0cDrMUcbjeuCatdYpmkFjWjAVHzOKQ2qnidcnNxK+TVnZeYx7uA/ZM3TWjumaxa8cVDMx9FqXKIJGTre+cphooGcRg0cioU3K3fpXHt95CbXI7QloIIan0H0X73ONSoszVRnZE4DQNyHK7J1ga02BXPKuYJNHOiLC9CQVHGAwuUVfNfOI3K94wkT9zRynCmTqlo8/AXnMaqjqFaGtHyOLJkilcCoenZoJSCjUxu3vUzbD3jFBrNPwVlrPvocA10ovPI6HewL7sJuTOoZz04JysReAtbPQiIRJZUXRrikq8I6L7SuiHpsrw7yRZdHf4YiW05iWHQxSWjELwh1uV8kIxVQ6N5uyxL63nyA+spmNZJ8pRCEsLBKlcyrKhjb6rFh4PT264SlGS6RluFklLTUQgD/UfTClF75KSESqG/YcSLpPlEToa/V4gKR3NvI8UXiwVR7uxAyESEzD+p11wq+Q8BUE5mU9ITKQUP4oq528QoSjT+39KOeyevp3J+ubCfFtDsXvn1Uirnc7uNViyQr02xOTEYyhVZ2DZ85GiQuAv0XKHVcN1pkA1UE4DBUztvJvpofV0Lzsbu33A60EHIwJZlhsfKcs40+PIjh7C2oh0WuESBZLLw29tFMSmFscETlglCW0dnpDLuwg5L0lobhkLSY6PJfMTArVoCeLc56CeeNTzSxkbhempJhnPLuRPL8d9eXacJoVCPPQYcnoaZVnIXUPIhx9HFK1z016G7o4wh+IbNVC/GduFJzRik/sCZ1yJPyaoMg0xADJvtlAMzaISZuF9zxIpYd5CoWbpyslJnPEGD/39/fSe0Eff6XOwOiyq26vMP6OLjjlW2nfH/8yTrYDnyJySHB6yiUXGe7QV0p8z5CBwarNo3AcjVA5LJipDdDb6cxsigbeYnoAomJuVCIsfihWROfU3RUKshLe5XSBCkjM0chDgTTOOd6O8+rt+KHoZzR5ylUPJ6Uc0tqPUvhkGcJ0pxobvTFTMwi73pBPrg+0ZhBJQKZzaGI3p9HRK1Zhg9NHLkKVO7PYBKktORlqdOb1Vv5FGohrTKNFN3qyvsBkQNLdkEVlOQpGSIXy8r5G5JuWfWXTbBP/JadxSiXPShWVJAXPnIubO87cr2LoZ7rgFspxsZyH2ziEa3/sV7sXnQWc74ck2GljX/wF73YaZZegketnNLCr6sEpgAdEMsanksXsikp/y5ofgGUfqO1tC6TeOiBriPGNuUK/SyYuoX/FIy+UcVFwYuXOYkTuHw03Lzj0Wzd5JJCFUzm0f2Du9PcEaQE36fZkIvGnMJeEJ3iNOtdl4s6A2MTsEixEqhyHd9XkoKRBu9k2mLOkHH/NNrHlCJHgDzPBeVeDNlsmYPZTKqqATHh4T5GHr06AFrqOgJFIrO0tZprv9KNrK89kxcu0+EytFKP9N4upV8YVA1rkFgyoTO28vzNetT1CrT1ARJ+eKjwAhBHbnYFzQ4b/6fLEB/rtf0VqEXe248HsBMXGSGCGIHevXIdBLrSDAi1uWeBEHAiq0/GgFCQRqwSI4/yK44lcwdWisAWMPjcB3foKqlFGd7YhqDTGxd5YhUWsgNu1ELRrwfvO8LrpSiMe3o7rKiAW9/vRl/xHOeEVkDiWE2xTipEXacbqFNu8m0qwD/vCPwAv25m3LsgwI/xiBfcaSQ0eoJFh80SB2h/QtG2HcYE2KJMVKOCiLEIKyqFOWDsEwEP5eL6XwO4X55UuhQAlcJJZ0WXJymef9TT+//NgenFkwAeiwi0xrgJLb7pnjg1WPLcsbuvFXORZCIl0r9jrPvYtbcL8PGkIv3oiEcrDKcs4BgTgKQt/nOef620TwdzC1L7DCVGRKpHgVUgghKVk99LQf3bT+e41yqFdH/HVw8Kcy4wfe876H10WA6xtnlQDXrTG6+TpqE63F83Cnh8lfz0UnbtVyBV6gOkF4fUMH1/RwduzYmAVF39FS6UTiJK/OMl6G0PPPKMcT1tEXhScMle39S5UT5CsllMtw3AnFlZ+FiGoNuWdkr0VKgPWHB3wfL90qqSVwFfKWdZR+dAN0lsJeuX69m9Y1tKy4WLbrDzf595zSBWlWZnFVG8zmCeMpolKvCBHeMH6j3XVoRFlNUuq2WPnKBd6rwz8XKbw4JxbedGIvAq2r/fOj8wqBLeq0WY5/jOeToi9S4vjXMxt/WM2/vi7eMgfSEvQfYXPkM2e25tT+wgiVwxBXNPxGTbNq6EM6WYKgGc26vVJ6DoCBT0rOYLIAvxGPW1uihjF+bKxnoaK02M269p5Y6WpbQX5L+eSZHH4wDIgW1l/7FJpQQQpvKrUEWaq0dt19qjsfiXxAclDKRTnVaHmEQDQl6yVEdEmy1g9C0xcZpv6miMS/rOP0aonE5gwBFb52RfSpbLKjs+oH6WJl2YriFcMPY6xHt2Jfebv3HLuByvM/hyco/eevKV17N8JxET3tmUM8LSEU0koK6qyD04pUv13Bt6z4aZICJeXHoVR8iEtA+bgBOp67gvbzj8Ba2CRkw0Fk2Yvm+auUZIsxKZT/L/56BS+cfknGr6UIRY4Thtt39XdniPfNFrrvUGC38Sw4x7+kxZhc+xkz9HMYMl7eRf/00lyxEOIqWl62s6kjSbzX3iRx9vEq7gym9H1ChLNO8GcEtFKWlGWkrOC6+96D3Sp10963ShMBxD+1r0l/DaVc2gfXUhtvzeG3MbqZ+ugWSr2LctMIIZnesY7KshOj4Z4cPxQ9gm1+hvih/4n/KDmX3YV016fZT1QgMkJri8ALqKcbA/IEUFFRloWqtB0yvir7GuuOh5HrN+McvxI12At1B/nIJuQjmxPDxBk98Gbjs34iaQViIUuZJpfuSDewwd+eYPGOkblTmhPb2iywBPaSbnredDxWf1sYYK3rRauoPrCbsUvvRU3Nrunr/cd15gxrFTOxucrAEhE+K7qAAeWFy0fbprwBpOBX8MLru4kR4MCKA44QdM+TdA5KJnYVmF8PAEaoHIaMlrfRU12ILSrFCXX3/WZPSUGjl7LaNCP1ElSx2UaxxlMXAWGQuqxM8tnXPipClil3LqJn/hnEl48tOAbNKiQ8UVFqn4Ow21CN1kTUxCPX0HnUeZR6FsYajsCCUt/zGNWt91JauBpRam/p90jWK/NcRCJtcn+wXWbvKyK5flCYjz9yGMR5Uck8Z/hS98pSUJsFA+4HETE6iX3jvcWJWnkfpA+KzQjKKd1/1IvksUCGU2z9nn2TV4vufNv+yuPoPHnAn95NbJX48up+ei85keF/v62loawDhdUWrMs2s0pVN45jLekE4dlNpC9YpHCJTxL0xYe/ZEL6WvqLGwoXqRS2VAgFtlC4wNoXtPP7Sw+uuDdDP4chrmwwVt5OaoVkfVgl8FuRVmsvpYwZPQq8hQb1cPFNUJC5mm04iK3XUfepsBOD5i2Up5TLdG3HPhMqQpbonncGgytfQu+CpyFE9sKPucdnbRMzGYpQTDx8FROP3ogzPRJudatjTG28hckNN/up3Bm98lL1Ev7IAETRoJQmKnRBIjTLR0u97kRRIjG8FwznaKSCxu2FNUW5rjf7p3FoTGE9mIhtQ9kjvZk3lWdFsUpukxgpURbeo55XQNDf94c7WprS7NdbKOyTF6IsGQumFu63JKUVfZTWzGktwwPE1I4agftsq47lylUMrCxjWd5sH++R8Zxuvb8jQSKJLFlZIgWgIutUpEPJ8j3p/OnOAjjuwgrWQXb/MRaVwxSFtjJv4Lwa+i74rYrr0rS7Aon1XjwxoayEFaXFB0yAN9VYRfUTkBu9VkHkMKvXs0mj6BuLGZ3aR+u7CIu+Jc/GLvd6jsoz7nGmcZ06bmPmTpL1oQ3UhzYgrBIgUAm3fHd6DKvcHpkjmtRVJf+WRNOSdVWq+4NoWQZiIzC6xX4XlU6fKtsmnDWVmTbIUyRusxaFUbBqMvfd3TyxAevXd9B487NRKscJU7O4COn7pLSMwFXKf6TjA73ea8rFsqLhnkRx2Tn6VgMpFNLCW1ss5xjluLSdMp/6g7NjheUlz51D35FtmRFli5jYPE1PR3SC3si7SymIQBvuURSHYPQePEdJbH9V5SDuivcsK8ptglVPr7Dumhainu8njEXlcERBhzPg/a07TqZESdiPTSMg5r0Vbve/Zy1cGHftz6sauxp3oXB8r34Xpfx1KpTrDf0oFaYN66E71wr/X/FVoNrYQ7W+s0mq1mjvXYVd7iO5hk2MJhWKCQKlqA6v9855L1FOPSVSAOrbHymuZ4KoqcCbQSPTO1MrKmfkkbk77TMZ3y01i0mBY2zoc6MvtNiqVpyahOuuhOHZ0TjNduS2Yayf3YLQn8XACtJwEPc8gbhvIzy+U/NJaQX/fbNrDNtysKSr/VOxf5FRVYTlF+Vblg5ly8snuKEyX0dSINpL6SwOAssuHuCoP1mAVUmsi6WavUoVD3x1C7sfmPZCNODZUkp6mHzt3dxKX9RFYAezi7T+bZD3MRccXJOKESqHId2N+VRUVyRSmg3w6k+EwLNgFPmd5D1JrrY/qyhgUm1jwt3MpuqVDDXuZ9LdxoS7lZ21O3li6lfsqt5BXY37PW2RChinByAL8tTzDz4VUK4MMH/uC+jsXIMln9w0u/beIwv3h1cpr0HWEiqlcKqjTO6650nVKY/GnidojGyLiaDMheyIC77YtU0Ot3iZFOJ3wLJ3ZGxyJbGot3mZ6qsrx+oUGzPKObxahV/+BHbtKK68IYZ170bs//g18q4NMF1D+LN5RFkgFvci796AXXJnNPQJAtZtQ9zwEEII//Wiwk9Qhf4r6deKt8FKrLjs/Z1TL1fh7D74sXTsTsmKl87N3BczHCdNiEqx/eYRxh+v8tjlI0h/iCu+EGGThyKDiqynyta/z191cGfLmaGfw5De6mLvj2YiJQ8pIBksLhA9Svs762XgzxIJhp70BtpVNXa7D/jJ6ow6j4HzWOzwCWcjHXIJtt0TmZf9faGFRWhbVEKsiKD377V+ltVOb/fx9PYcz8TkekZG7mSmDzGAZXfGX8pK+eH7s4ej9BKSV6kxuZPRTdei3P00+0Apph68hsrSEynNPwrlL3+QGpXRvriCcLHCLILhnb3xQ9EFjMIXRIlZPEXH5lpQ9OGiLAQwNdEkkSGX7jbESUs8h1Rdgc7rQb326bTkXhW0tK6CGx9B3PBQuCvy5fd9U3J7/tEPnRzSsYUTWmDyjtOPEZZk+uYtLVR8/zLvzB5EQYiFyJrhCZAgaNumy4Z45LvbARh5rMY9l+7i+DcMFoTJF7iquCmQeDOEiupilQRzj5TsXH9wZv8YoXK4oaCkOmbW09GTlvxbQo8Cq80OCh1bdS/IZBWE8IKdaTE8vGRl2pnPuPN4YXVs2RWKHOGXm2kd0oe1kucTipwoQWeHZxUZGbmjsPwsXLeKJeMxBZIxZ4POTOinoYsWAU59iqld9zI9fACiZ7oO1cdvp7rxbtpPexGy3BGKuphYIdR0rQmQgjZf4AuerOpANGwzUztu4B+TV2iyTnodRkdmWJgBfIvXC09OxTsKiEWtLUII2DOGNdCGfPZKePYKZGJdIiFUgdgIU4U1U8qlJN28qmWfj+8TN3n5BpztB9+i0nd0a+/oYEkuRyke+b+dPPHL3bH9j/52lOHHqjz7r+dpDsS6udELD5cvRBSWaG3V6Qvf287/fXiCxkFwVZmxUFl73DGcdtopOA2Hm373e9avfywz3fnnncv5zz6Xv/z4J590JQ3FCCWxVAlHNOitLoo70bacif+fpBCIpRGRw2Qw0ycQM8FzEAiZRMRYrx20GSyfADWFEoKS7MJRVSYbm3BUNEXXVbWwcQ968TPrxGd3zYQQdHYcydjYg7juzJxYp0c30NF/dMz3QxW8qXUrkHIajGy8ksb0EAe8d+82cCdHEOW2cMXlpGXEDcRDK9aSnP0xV5TEUFFgQYklavEHbWY0KaoTAE8Ui2JDDvN7vX85BBEFCvEjNttzO/whHn/2SSltimn9lhBeUOsZjkS4Q9NMXv440zfti4VKnzwdCyuxCQV5BP2HxrjD5iuHsNsFy5/VxfJzu2jrs5jc1eCxq8bZ/UiVwdUV/7UczCHyLUpIHOVGawGJ4Hp7T1apRaEyZ6nFKz7Vwfc+PInb2iH7jBkJlY999C/4k1e/AvBe+q7r8sMf/4x/+My/Mj0djwVx9NGrecmLLzZCZT9iuxX6p5fR1ZiLCFaIyHJybYaU4UulWTcp1Who04U9M70oXENGAQNtJ3nDJv7sn/7KsYzXN7KneiegmKxtomz1+/kED5s/dS80ARTUr8npt7cvZmJiZlaNqeGHaO9ZCVY54aiqKTS9QxPuVtQnt2cuPHigaGx7GLt/ofclOfU36cBa0GIIouGaIJl+P4SCJ9iRCJEfV2/pumQxY1kX1s8/cse2meZgANxFfcUJVDBzryCNAEs6YWOc91oQxa8MvVAAbOnOKNSLmK4hto9itwnshR2oyQbOWK1wCYn9jeej0+wEgtk3ij33jFNqE5z7iQV0zbf9vpig0iPpXzmHyV0NLFwsofcdvfButnC8mCgKHOEF5A9C8FeoURYOSuWOu4V1kSjmrbRZ9TSbh248sEHzWhYqz7/oObz2Na9ky5atfPf7P6JRb/DSF1/MK17+Eo49Zg1vftu7GR0d2591NWjYThuLJ05EYiGCWfStuHcnSa6a3IS8Hm4w3FOUl4IwTTg12f/sKi1FINhdvZ3x2uN0V1Z5Qy0yyk8Ewypkv6Uif5gmD5yYuQe760wztOlKehY8nVJbvxYHQvizZLJ7R0IIpobWzbi8fYmzayPO+B5k1xzP1SBZzczxoHQSHf0+CP2CrESC4E9FtoB0KfSLCY9pZagoeWMGv8XSpbAh2+priKMW9eM+bRXqqAWe1aNQDCiUS76fivIsKFIGIkVliItgSnLWvnR5AZHzbRbJGxlKHRJ53Bw6j+9HihUAOGN1xq7bwshlm1D1A69YpnfV6FxSQeQqtOj8lOud1envHKBrvh07JnjndM61cfACuwXX0cINrSXB61kqFyFcP7C+oi2wpohA3GfVx8vXFgoHOOUl5QMuVFoeLX7VK1/OyOgof/Sq1/O1r3+Lb176X7z45a/mq1//FscdewyX/ueX6evNNxUa9i2D00dGIiV8wjU53Qq69UUKzz+lidgJGw+tHK+Ral2k5A7LlJZgiQqKBtsnro8O1IeVYpmqKO/wnJqYUoWk4YwXpsnDqY8xtPG37HniMsZ33s7YzlvZs+EXjG+/DW/sXLsm/t/jO+6kPrl9r8rbdyjqG+/1/yJ2TTOFi5Ykfm3TacPpxTlvklA/5Pgz5fo5BZ8zMPEHM5hCJ13lQF9/6xk8hXHXLsV587NQxyyGSkl7jlTGPwhuhOzAbV7LallecxgMMcRHldNTkfNJiBSyZvkk03o3ti09Z1vbiofvt7pL9F60jPnvXVvo1Lq/2Hr9SIFI8dDjnzgTDeaf0F5wjBcaX1/4URcpYarIAE6ZaL+IPYTx31qisP0gkhJF9+CBnyzccolHr1nN5VdczdDQcLhNKcW//ft/8Ld/9xnWrD6KS79pxMqBwHYrdDj9oSUFCO62fIInOpx2rLU6grRlJe/tkYyrAlpo+yYPfJM3kidW/B6PO0lNjac6SELh9cRdFXttqsB+nJe/b0J13RpTU5uK69mERnUPUyMPMz2yHqc+zvTwwww9+kumhtbRqI3h1Ceojj7O0IbLmNrzwJMqa1/hjOxACa8B0ePQqDwBESD84aGChXeDdAVZZPs8ZwjP2OsymLrcDKEJaIvIYVcIcA7wYPohiOpux33xqf71i94pwdRhPVKB93d000TTYV2kcLCkFx+lZDupMEyRb74nUGaK94g7ma+bbLHiYgk3d60gIQWVlT10n5u/ftb+YvcdYww9MIFy8tS7NtlNgjtR1/ZlCUcAgeurdAt9unIab0jI1b77a//gxkZqJQ5l4URzGfDitVgHeBpOy0KlXC6ze3f2OPt3/++HfOJv/4GjVh3Jt7/5Ffr7+/ZV/QwZlFw/Jkiy4c/y3xDCEyHBPyn98Pla6HxZMPzjbwsfCX35zqBhEYljs3pZLXZaStrMmrGpdXEB5rd4wSaJoNrY7a9IXCBSZNTzGx6+jf0xOO3Ux5nYcQdDj/6CPet/xtjWm2lM725+4IGiPo2z43GU39vVr6X+rgt/58DHJBG3Bi353iwMmCQ8VPliU/jxVTJC6ecRVj+ZXkrY/ORE6VMB99Tl6WcYlRIC2T1zbyghsox4UWdT2QW5KtXiUI9OdJMG/imZqVKvPlEwRBRlfTCEinLh7n99gq03DIdB2/w93nwETYQ88oNdLDypLdwXzGOI/kVpg5xks/P2n7zUNcOzmli+T0rWT9TZ4/JHf1VGHkCx0rJQ2bZtG0uXLs7d/4Mf/oS//uSnWbVqJZf+51cYHBzYJxU0pHGDhjbridUDsQXCJPuNEZl3ixp5HZnoXlmWH5o/I+1MhqA0Gm40dXCy+gST1a2+GIoEUvB9uraTXUPXUavt8Iv0Yw74ETUVKmwJ6/Uhdu+5nqnpp27DVXv4D7jjQ9G1QfvpXP/WwR/O0SLGxixXvooJ/hfPZO+I3XkZ1puZZ+IzPAx7ZpFYnKWoRf2pKTxFEwcj64q30J0WhSD2d+Ko2Ge2BSSzdoAX58PyV/otelXF653d0MbTC0qDba168+5T3Jpi3Te2ctOfPsQDX93M8L1jiLrn+CoETO2oc+9Xt/LYz3bTt7wUOxftdeh9D7bP4GF0Y0NFWWSvdi2E4IjjJSdeeOCCwLWsiR548CHOOvMMLMvCyTGnfv8HPwbgbz/xUVYduWLf1NCQomqN4QoXqTJ0ZuBz0IqTbKtiwreexKww0U6vN5ycapdMF4wzNPF/6WhbjihVELKEbXXhujUmaxtps+djWd5q0K6qMTqxjvHphwHYNXQ97W1L6GxfiWV34bpVJqc2MDW9GSnLKFXHcQ5+7ISDjlOneudlWPNWUF5zZvRK87tmMYfY4GdKWFsQ4A7twtmzGWvRSlSHHwG5oEnIvctEfL/AW36qyME2l6ziH2iySrDBw1GpZ7OZ30j0vOuWl9ZiocQCsPlDDdESeEG66K6xpduChSColy5Sgpdh8bGq4aYDXB5A6qMO224YYdsNI1htkvZ5JdyaYnKbtzzGwFEVbDvbAqJv86xV3t+OkliyaNjTX99HOt4MoLDjEvwi3j8Lh6hREUic8NspF9nc8esDM7TaslC5+prreN5zL+Ci513IL375m9x03//Bj1FK8cm//st9UkFDBkIxbY3S0ejLS9C6XTUQIcHfGYTxQrKcYPPyDRYv9F9ogqg3nluOFFh2B12VVeFxwWe9McKOPdfhqmlclVzfRjE1vZGp6Y2pPGcaL+Wwx3Vwtj2Cu3Q1oquP2FTrrCGeDNOwO7QV99H7EHMGED3dqSLiAwg5OzJuATcZb6UJCiJXq2R+jYYZ9mkRuX477uoFMzpGFykzG8YJBIknJEILDL4llCAKiGcdsPxhJamFyS8qL/KZ8cLKuyoV0ileG8dl4o5drVZ+v+NMu4w/EY+o1rXAU+7N+pxCsyC7NItIK7CF663vo/2GSilvSQJ/XlDQZ2gg/bz8304K5uQPsOxzWhYql11xNXuG3sfOXc1/1B/88Cds3LiJhQtndvMbWmfKGqHd6UNkPbVyhm+PguhN4bCLnV6DM9bpVqQai9jigiim67twhUtHeUEkRLQ6K0uEHaAg6mtgpbGtbvp7Tmbn8LWtnZOhkMbGBykf+/T4Ru0WiFlbNJOHUiAXr8SdHkPOX5rWFCpxbJBZYLER6V2p8vV6NHP0zRIpSsG6BzyxYmiKuPsJeNYx0FaKvQeKXyFJh9qWSyPw4E4OU2TNPlEIrIRfSvPyFG1WIxwmyuuHKde7WUcvn92C1qk3s1R5xP1xBHVl+TFSgn3Rw2wLh4qoRzasxJCdQnqOtb4/kY0brXDupzmQEWpbFirT09PccOPNLWf8+z/culcVMrRGZswUKXNDXhei8EyfUqAP4Shtd/pOVjGRkRIpep38Y0ZrG5hytmLXOhnoOJmyHAgDxCkhfOGiosdJeyiEkFRKg5TsPuqN4ZmdnyGFs+1RGnMWYs1fDvj3UyAokomTVpW2LqyjT45H5tXTZAWRU0QB4Pyv+ns1FByJ4aC82CxhniISxCjlRd9d/wjce3feqRsSiFoD679uwHntM6DdizGkVDNHVOFPPwZ8y0gLxlk/te9M29RD0nsTuG5eP0rvHXkFS1za7Pi0XL1eKhyCBlV12PmNB6ltmmhWkYPK2JZa046nEMoP+RnEPHFT1hThr47s+fq4TdxyFC4C6Qf1C2WOEAgUrqN48KYDN6POrPVzCGK5FTqcOaEZDgShV1uGZaOIUIQohXB8sZLcX7LTNsTgbyujMP1tFYge5VKR/Uw5W2moCRqiRqlkeVUV0ao5yfVz4tm6tJXmG6Gyj6jffyPu0FbsJUdDV38oVlNmD51gW6k9X6RkHROk0YSHPjVat4yo5DE5t0R4TCiUBWp6Gnbv8CyNhpYR20awPv9b1AnLcFfNRx05D1FKdKFDVChiwrD4QT4tvHuUEjNw+hQ4rkQINyPvSKR4t0GDihWkiycOxYpSjN+8neqjo0zcthNVO4jhaVvkqAu7fLFQ/HKviHpGqKqoa1AWjfA5sWg280qkvkVeLN6ab7f89MBZLI1QOcSw3TYWT52EpEQoUIBQ9gJNl8sMCHxHwDeji3TXJZwLl9daEPeY94eRhBvNwImOjV5OAouiENJZVpWgzoZ9h7P1UZytj3pf/j97bx4nx1He/7+f6p6ZXe2uVvdtWZZk+bYlnxjjE2xOAyYJBEIgJCEYSMhJviG/3Mk3NyEQCEkIIYEvhIT7MNjYXLbBYHzf8iXJknXf2numq35/VFd3dU/3zOxqde+j12hnuqurq7urqz71eS4VUL369dYQu+WK2ALKJhuWNsDCZ0GScv53U6DKKasr7pYm3z9qNbj0hZjp/ciDD5RfxJQ0iYw1kHueRd3zLOb0BfCmSwpeQoNSLuqsd2yr/pKZEA1KdJZOaynx9FjK8MTuvKpBNWg9kYuAiQy7P/8MZvToByhOqj0BWa45Ty0KVWkQ+KxWzBpVpYGISeKqaM9s1q/DGdS6M6TlUnFlDIIZjdi58fAtBqaAyjEmc0dXoQgRVAdZwToQiUf8MjfmdufIgyLPui6jMRXFcLQ9PYxGezoTmuoYqx+5nDnHveiI+oPfp3LBNZ2VnwhmLLJlym9vB3hyTcgMl67/nXUW5umnkMGjm9Y/UqIDgavPhDMWwbQqDI4iDz4H965HhsaQ5/cmQNIZwUsCUorJtGLJ2lco0YSB8dY2rcCFSYYWbaRg6kzLVVQeBReLhIpwdhf1zceOF+CuZ0Y5+bJpqJgljEO5xf9bpqTqefikjJKhbgJqUo/vtyGgkWBEZ3gcxmH3k+HYQBgbIufFdokIM4GAfQcjhz8W7pRMWCq6m249gyRsfit6OzGALHlxfWCSsINlPbPNcJSPrpgZyQRjNKPRXkZ1GtOiEQ10Rv7G9g7GaOqNAUbr29seMiUTF7NzM6ae96oqKNeK9fBUOnkpWhSL/6UJeXQgTUu/uP+tWTPOik4M0acthPddDy9aBbN6rF3K7F7MNWdi3vVizOxeODAC63aC1vHtbAYpTpqHB8e4pB47QRBRCRpUgqxtRLqcyT9Ep1qypcpFCFWxuqdMVO3wxf+YDHnqW4NxUDgbFr8mDQtOpM40GcuAlKxYDgRjmW3lAT9HwAde8Mu8GWJ26E+fh2DYu20KqExJiVR1T/qjXaADiBcYsTon//HqscC6bMA4eGmYIXaM/jizbbSxs6Xqx2+JAYwYdu3v3Jh7SiYu0YbHS3K4eJIfOTpkV0xBOZPf16wpbC1l55514gWdNDN7iV6+hvrvvJr677+O+juvI7poJSb20TXL5sAbLkl9dv3ZSQR6apg3vsDe+lsfIzG0p3zIyXYVQxhowkDHLsUWsPgBrV05q0YCkbwaxh5XCSLCIIozMBepamwdoZRHqy0qr0eOrbQKug73fXJv8lvERp4Ncuq3MjEIgbMp8rY79/DWrt7Zmux24YdfPLyqsymgcgyJmezQ787QVbzfThUkHFTvMMYQUWfn6P1sHv4ukRnJ7B+t76ARDTaxOCZuj1YSR0eVxMMp0scOXXssi17/OObAniTSr5MEUPhePa0ASsHkkeQZch+3o6y+NhNQy93m2LFDmAzRi2fRePuL0auXxa7GArN60dedS+PNl2PCAPPiM1uzpCIwpw9OmQsb98AnfwQHRlqaHfm/wiC7QrefeJxBbGw5uzcp5zJgKLH2K9UwoqsSJWyMEkMlcDSds1mxf2tBI1FhdHaTDNG+w+hXO0ny1M2D3PXh3TQa6ZA5nii0bkj3RcUGte3FxOXdd8OO544BRuXd73w7F17Qmla94PzVvPudb59Qo6akTMbTNfGsDZ3BrB05jPv42wMV5wH3Pk691K43520OjCFilDE1QE9tKf1dpxFId9NhO/b/AI3GxBBMBzaWiskZ8FpiKKBamTWeq5+SiUrUoHH3rej1T2AadW+HsUHZkmxp46vWFB3rVHtFI2nm4JLtZccZAxuP7vgYkylGCdFPvwBClY1w5tDC4llE154DJ81uz8Rqgzk5ZqOe3QHv/xbmm62j/PoxIYurT5FoCjfSgi4BomNi/Kbn7WGiwTH0xn3UGPPKtuuMBrRh6L6d6KFji1FxsuHOYb7zFzsBTSgNQnFRY1uJISCaaEaTpA7Hctn6YMbccb78BykTMqb91Xf9CgD33Ht/aZmLLjyfd7/z7Xzkox+bWMumJCsGZjdO9X5nLKJaHxp74hhIwYgXDyW7tG06OAUsuXMlZjCe0tkBHwm7qck0RIRaOJvptVXsGXkYTR1jTMyoHGDL7ptZMPulSFBpfT3GUK3MYXRsykblsEhUJ3rmQZg9B5kxF4gng06YlJwYSPIH5T16bJ9sU59JzSST7uja4Gxa/OMduH7iic4beYyLOXUh9DUvBhJRCnP20s7iQArI1auQa06FfUOwewi27cfsH4bpXTlQkPqjuOBgrV1eTcZTK1veqYtKjo7tI6Ln96PqYwThNG9va6Ncd7LdX13f+tqPcjn1ii5CiZLk4Em072K0bu1SSm6L9lRC5WLr6JYxIlFExjLcQ/sPL6NyyLx+KpUKkT6xqNdDKV26n4rpttaIRtuPqEIAATQnGlRCgZM9IHEgLrHBspzEg72dHFKwYtkNL4uGSutzk4cJcnWJjfEys2+NV71mcHQDY9E+xKXhbDOC1qrzOTD4WMsynYugwi6M0ZjoKKGCgwrhzMVIpYYeGSDau7k9m3UIRZ2+Bpk5JzMxOVzQzsHCxO6RlsHDZkPOl4GOON2M9ZRjX1xj3J5kYwxSvv89ZOTESZ9gFs3Ej4wmOSBgDFCrgInaIxXxbO1nTkNmTYMVjokxzXm9Eqa3k76aQZQ2xL1jTmjfNBC6T++PVUFF9ZrC7wKMbhqksfMoedcnIF39wqlX15L7BT5MBA/OAzaAQCgRGhWzL3lpF2bfxHXY5JNiNKHAnl2KjWsn//payYSBSitju0ol5MIL1rB715Qr6WRJaLrsFxGQIAYrJWxHHqSIpIG88h3S6/B+PYltSFxEu3q9OcEIEAaZLLv5cycsTv60ouipLWMaUdpG/5g8pW+EsNJfen86FgmYNusMumesQoU2yWF9ZDdDux5lbODIqQoqJ51LdcnZiFJpeoGowcgzdxPtePbwNyisoE5aiRSED+0ErIgIOmZK8qG3MxVRsL2pLvu3VD2kNUSRDZm/cSM8/tgJ55acvHu5FXT6Pf7y7HZYPrck7IA1fE3VLR4DQi4Uo9GIkjgmCp1hlIwnT8LHxv0dOoutYghVK+bGn7RTCCViUJVx0IBHoSw6u4rK5YUVRzOSielNQMPm7JFYrV4ISOKYNnmaMwPwvFSR8VRj6rrD5z150jFQue3mr2R+v/Utb+J1r72+qZwKAmbOmEGtVk2yKU/JwUskuSiAojzqO7GuahqADEAlaJ74i8R7+/2i2tUBzaND3KdN2ZLI7+X5w0QQwoyJcLLK9gFYvDpXYYVpPacyNPhUmwspEVHMWHIVYffczIowrM2gf/HlDGy/j+E9h3mpAFROXkNtyVlpM13bVEDXqZcyCjQOM1iRvhlI0GJ4yC7ecrts5Mo2Mbo765Pe6by5zdthYO8+5OZvdFbRcSoSR5SWZEzIl4j1Z998CN5+FdRyixk0QeADiaTmXGV22RIojQoko4rrQAvd3CYgiMGR7oA97Ix1MQXlhLD32A4bpsLiC/fZFSchmkB0AjMilM3X447BRrKtxN5UEYrIJOHcrF0Lto/4bIwIzF4IC5fDlsM4JHVsTCtK2YlFYgSMJL/9T6PR4OlnnuHjn/gUf/13HziUbT+hZFjtJqLevMNgI4kGJQHgQtXp210uYc6VMd8EHTekjQrKQOLtYfP7eKttJxlwL+nf+PuMGWuY1rNyQpfRPWNVE0ix1dvr65m7BhX2FB166CSoUF18ZuEu187aiosOZ4ustJk0/EWxX9Lao8QG0U0HTFxKMY0IbN92cJUfB2Lm9zcvjL2PKIMEBjlpJvLFeyyzkh7dgSFsvICRWC1U4N2cnSqbekVBnVjXZeVIX2lju9+cyLC8ZPabYI55oLLr2YLxv0RUnBU5cG7MAlpSfkQRUZGUPQtFU1MNulSDmori4zQBUWKw7MvCUybtsjqSjp/ci69L2ZPHH/4J//Wpz0wZyh5OEcPuyrPMrZ+W3a68kSKXYtSqXQ7OA92qfFrX0Qnzm6hz4rYlf71sYaWsDNnj+mesZmR4I1p3rm8Ou2bRM+fcNt4Bhq7+5QzterjjeicuQjh/JZUFp7lTu83ZUiIQVFAzF6P3PH8Y2hU3Z/8eTH0MqVRLywigBVDxOsy1XRWAipLra2fr0rqR8X+PHI7ndZTLrJ5yOwOxqg8R4IYLAJCNu+FTd0JDI6fNQy5fSesH4dQthkDlbVR8EbQpYjTivR7CtcnSU9VPwsxAszo7YV+iFmqftA0+oHHJ+Arzkh1DsmdjxIHtEdPnBW2u35SQmfauVKROjdZ5etxQHJSUa3SOmSZFJjSLvfi66/mvT/33ZLdlSpwYqOnpzKwvZ/bYSnobCxCjOBBuZmflSXRs1+HyFxv3lvsB3fwlUqdsSvnI0q65FEbySgqYUhUQ4gLOdX4+ELqnndxBOUBCeuadz4yl1yGqXURKIaxN76zeg5BgxiJ6XvCzdK24hKBnRlb7X4L4gr45h7xdGdERev3aUls0ozUmqmMCCzATj56CESXPviQ1jlPP3VzcWHuUsfaRdI9nMdUQpncX3k8/omxGFs+wgd8GR6HeibtuWkHn5KxJPg4sOQk97x6TC3McH0F6QTaWSlVFKMfOtmBdHPPi+AMFVANNqDSV+bVOG39Uyu3/fKDl/TfGMLgrYnTQxNFsmwq09ATKSzH2NWw/zHFUJsSFbd6ydbLbMSWxKBMyf+wcuvUMkgBvkTCnfirbqo+xP3yeA8FWeqI5BKZKKNPoZ+lBnTNZxRT23s70xsYtofIjiDZxXJQWb4bbpztd9RiCDlQ0oqrMWPpigmp/GybFq/kQe6rJtBl0nXF1m0bQbPcxdviD3emnHkJ6pyMLT8ZoHRv6aqsqGx1GT+/xAGjrugSybqnuvw4HzKRX+fPXju3IA/d3ejnHnRgBc+pC9HXnWOa0gMYq7fZK2dH/urNgZJRMJvbis8V/pQNGw5YTDIHSaE+/q0R3oIm2viyKiEqB1jkxj2pqhz2Hs7swcU3WBsaggFV/dh47vr6J7V/b3O4CjkrZ+miDDfeOsnRNNRMWwomIcN/nhtmzvs5r/6iH2jS7SBQRjDaIsmHz2z8DKB77bX0LT4Gt6w/+ejqVCSvtZs6cwU/d8GrOOfss+vp6CYLm1aoxhl/4pXceVANPKDGwYOQ8avQBIJnlacCCsbN5vnYfY+oAA6HVy8+OzkhshnJVpQawnZxaqfI5o83I5OwSgMTzIN1BR5NRclRnoyBGt+cee+dfOC6QIiI0xvZ3VHYiEsxYRNeqy1uAwmIxxtDYueGQtavFiYnuuwM992nUSSsx0/pgbJRoywbkvAtT04UOF1cCKfPiLr/dsVICUnQE3/3OOC7m+BLd343+pWugJ2YIJkKGBgpOWwDohDlrv54waC2J4W1JSSzfa5KIsxORsCRPqgMyPlgxxp6nonR8KwyhimyIHgeOxX7mv3oJqqLY+sVjMyDgdz5wgGvfO50l51XRjZStFgX3fW6YJ26zKvH/fOcBzri6yrLzQ4IKLFiq6Z8eEXbyPIxJ1YU5sYzM4VWjTQionLZqJf/1H//K9Ol9LRvcNl/IlGRkWjSHLopVDxITmXPqp7K5dl+yvUv6LUjJ32oXXbbFM0j2hCpR3WRM4BQlK7VcHRKfL1mUuTen/LhCSSav1mBFUAwPPdeyKhV0UetbOu4XKhrdM67ynUpl4RnUlp3fdvZI7r9Kt0QDe1s+x0MtZscWoh1bkt+y/DQIg47ZFCe6SDVUAliM5DCQB4oMBnnqSSQ6NiOM5iWqhpizl0K1ijyxkWBva9dqHQj6xmuh4uIPlZXspM8IosSzHSlaWWSt3Q2t3INT5iRIMhq3bGRGJeTqsENKq1VOGrvFGGsMWk3C99v4IT456wgjJda7aO5LF7Lru9uo7znMxhaTII0R+Oaf72fBGSHLL6tR6xEObNOs/e4IB7aljPDoIDzw9TEe+LpVjf7Gf4Y4x6HWpnoW/KimlC2pOu25wxxLcUJA5f+89zfp75/OR//143z+C19m67bt6KngbgctMxsnxyuF4l4kQJfpZ2Z9BXsqzwBg0Fm1i3vnAy8YXKtJrhIkZRI6nhikhI6RSZTJNA0cAqaiMr+bxNARK+NGlpTabUbuBsPw8EYajf1UuubSM+tswkovWo8xfOA5Rg48i4lGCbtmjQukuEGvcQiAitR6LUjppKxri3fLVW8/3WtezvCDt0D9yAeskjnzPFDa2TEGLEgpOyav6sKb4pq6n8Dmw2dYfKhEh4rGz12NmT8zvcgrzqYxMkb4398j2FHM7pnrzoNqxf1q8Rw6obs6A8C+qZtn/w6Z1XWselG+p4hrg43XoWJPEzskxMuvprYLSqKEl0nrKZaKRJkAcApdGNvSfVcCETDjkjnsuDkF4MeabH28wdbHWxvF+jK4F6bPKBqG/Q7kg5Tm/iPGsHOzsHPzMcCorF59Lrd9+3t86MP/MtntOXHFCFXTWwpS0nIwQy+lp7EAlEGokPgK2vCBOYVui4HIH31Uqgw2xpRHsY3b4H6axP25xWlIB7UisJJMYjFg0nqM0dHtdHcvxq3gnAwPrWfvnvuYufgaqt3zkj2KHvq6ZtI751zqIzsZHUfwNqu6gvrAdnRj8qOZVhaf2bGqR3v3yOLP+OZ29VJbeTGjj98x6e0brxhUWxCYgE33u00fKTw2Hp1MOs+llYbHtqupEaF+4yuhpyu7QwS6qjTedh38+y0Euw80H3vWSaR3KaGZmsFeWy1qXt0j2DD4ZUHj7DHufbbB3tKnrMRGMbXDinjHW3WM8gO1Gfc7w+ECCiU6dWZMai9GYwG6KUpt0CIMP67tCsL+Snmh41AevC1i0dt9cwBDSJSAEnv37WgbESAYqmIZJ22ECIUIfPd/D3/bJ/S21+t1Np5ACb8OtXRHs5g3dkbOJqWVCFXdBbEXSxLi3q02Sw8r2adyofh9l+fsabN/ARvKvwNbGD/qbb5KIbVxEUGpKvX6bvbu+TFd3QsJw+kY02Bk+HmCWj9zTn4lQTgtqxZILlGodM2h0jUHo6MOPH1Su4lg2gxEVTqyfxmPhNPnd8Qo2aR9klyPH8dPlCKYsxSpdmPGjnBo+L27YMlJpbsTMxKweXz8HD8T1WC5x+jmq6FjO5N24+JTm0GKk7ifNH7mRQT/+s3m/V0lE2zTXC5NoCNb2IKFzFavm0pG9WJcjShlg7SFKg9qUjdjX1Tm/Wz+K0CodEwIR0l5bcTm/qGo28RZlaWZyXckcpsoBzT2n1jeYg982/CyX3YMmKFCw/MA8u6wgZAIjReOUwAiRkaEB75/+AHehIDKT35yH2efXRykakrGJzXdx4Kxc+h4uelK5pkSBYWMRT4GSkE2Zdz3wpNJKSvjq4o6abPLf+gDHu1GKn8FBvRMW87gwFpGhp8HnkckZMbCK6l2z43N9FqcK2aFxEgpQEquwYEDQIIatRnLGdk9udFp29lqGVL2oKgbJGtnEVTvLKLdR1btYbZvQcx5LfoMxZmS899bnaP0GAPDw7D72E7PoS8+vT3l0d+D6apgVi4guvJM6J+Gz2r6TEi2d6eUp9MGN59FLNPRtENyhrUOoJAYVwo2jH1aBq9Mvr7mck3ngwzT4iSKgYqrO3+NDjRNVPbeO/lq3qNZojrURwzVbnvXswkJ0+/uWYVk+4cx0NVlWHSKZtMznTtqTIZM6DH/zd//I6euXMEv/sLPT3Z7TjiZMXYKIO1VPk6KirkRIs/XBl7Yew+QJF2yVTC4fF35j3/yDg09E/YkEEyoMIFqzksUn08F2XgHMxe/mErXnLielHUou23Nti1kMvgaBSagKcx7dXo5UzBRifZuac2mJCAzt8MfJJIvR9gWbNYc5MprExsSA4k3RSbSsHifcYh29QW5ev1Cd9893mqPPqlV2ullQITGT19K9NqLYUZPHCMpPSYLgFMAYw81qEAThK1OU/7eCpY1cR8Vu/iCjSY7HtvuTtxgna9OHvg0tAeQxHjf7eSlTfPwo1vFdMKWj4Yj6tuPvL3X4ZTzXwLd3Xb8CHDP0KDQhGgqYj9hHCDPAUgn7r6/+pcOvxH7hBiVd/7KL/LU08/wO7/1a/zsG36Kx59Yy+BAs6W6MYb/74/+/KAbebyKmIBpZlbnIMWXVksJkYzNib/dWOVyXKYAJIC3ICtuVzJAxj3XsjPZ4/NHmiaLyOJ1nqs/ilJqv3/+C6nUZjY1r+1ti632Mtl2faDWVFzSTM6TKGPPPUBl0WkxwGoGUCjTPEK7nd4q00QNov07Jr19HYsIcumV2Qkz/yjLvuelYJ/GgUd3vmxZY0D27EFtOg7UzlFU/v45EQPL5sbf8xRTzIwWGJxjoKkbZ95PA8N1mBbEsTVyLy+GMEzDq/s2+ZapLHjB3ZFt7WI6EavuqYVR0lzQieFtEhLe5EMv2Y7SiLe18kza/IWNB9vIY04uvT7ucqaRMFTO8DgPSJQxpaP08jMnqr+duExoVL7BS0Z40pLFnLRkcWG5KaDSWqpR3/hAimA9cXyQ4vjSrHK59C2V/ERdVK7tQs8W0ALGz0ianDf5Y0/hny6xwWh9kqHBdQBUpy2iq7cgoF0Ht82b49PRrMXkaYyhMXII6GATMbL2TrpPu7z5fud/F81H7hGLQbp6MYN7J7+NncjCJdDVnW2f/ze/rdUzyrEkRgAvM2xGrejdB/P4YxNr+1Em6qnn0WctKy9gbG6ecolvioAo7Q6JjwWzeS+yYHrxgkYEvvM4DI0gLz8b+ruTAwUIQ908xHjfywmL2PC9TRcvOk4yrrBZA3pfxRXkDG2DGLBgHEFsAEXDRIS5YdB93/OT3ez6/hEE/EdIZs1P3b4lfk6FPhPx77JHZs3+Wq1CJl8mBFT8vD9TMnHpi+a3LpCZACRNPNj01otdjipTzKTEYlfvLZiUcYiB1C25yEVAwGiT2aYV8ZtRfm5jDI3GPoaGbGrOadNXlq4ax/WeuFG8RUZfEWFk9wQzM7cRPbAzGeDjPLfW1qbIjiPTqPivARMG1C64jtEHv4vZd/gHWpk5y6qeOjEM8EFGq2JeADjjDY4ZkJmANYP09o673UejBLfejz5jacqn+5Iwlm06uRCrZHLb6xHynz+AN1wMK+ZCpNNFRKDgjieRgSEqP3s+BApDlNRRGG7fnS5+BjbGa7EKMgtK4n6eHQbyR7hLKdyX6QdAZKxXEJjY7twkjICN2+JqUkRGQ10jkUZCYXjjMDtu28bee3a30nodt2K09epx91WJbgkiLfmdewLGsGtru1XI5MtUCP0jKC5rb5tCyVejit1Ck66kKbU6yoCUFmLcOdsAGVPmGeRVZJwiWbCzUOCloi8cKAxGYEzvY+7Jr0UkbGJfMmqmNuN4cs1+2QK1vntbh3Y8QjRyiIw0JVWXJOFu3P3x21ckhjT4XlChdtFL0Xt3MPbwHTB6+LxfjNaF/a+4MC0t4AykhtVSfPlF3cTMnt3Z+Y9yUaMNwv+8lcZbXmJZ0txsLg+vQy5YVnK0zd+jgtTd17ncGgNUA0x3FfmvH8CyOXDuEuiuwJ4huG8D4UtPQ119YfIs416Z1N7yEbfcaevQxiAm9ShyUWO9SjLlwyajXpu5t4iwM4DWhlqgk+sG+3rZKdjrSSJQDXjk3fdjxk6sOF89/bDsbNsnNj0Fo4OGatXeIf+1bKl5LNonsO7RwwtS4CBC6PvS3z+d7u5utm6dSrc+HhlRe+mLFpQXcHS3Y1NaSPbVp1il0HEmZbcSKg6VbCBlAto1KJCMSihpZ75ah0AqQnftFG9zGrch7zHkNbVtU4yQqr1y98YYzeCWexnd83Sbi5q4mLFBTH0UKrWsu67fyDLJXHM8uUyfTfXC6xj70U3WnP9wyNbNcOZ5bYsZIQ3uBk3XlvRR7z6UruJd+UStcfxMOMGO/aj3f5FozXKi81dCECDP7yK89V5k9nQaJUBFYpACHoGZYV+BF52KfO0BWL/TfgACIfzNq1Cze1p7onViaOK91kpMEsgt1To7EJIaXhqIjWYdi2IKYp7YfWko/PweMv2pSGvtgxVx13OCSKUGr3yHcN5VEHiheYf2WxYqGUrEEnYTsSmaOecYMaYF6O3t5dd/9UZe8fLrmDlzBsYYzjrvEgDOPedsfvVdb+eD//RRHn3sMMfaPYbkgNrCHE4DEQrTLyjxMiKPrzeZzGqJNJx+J70ymWCkBQ4YJ/0nKZtQeKgDZE0DT7pB8mAFSpuQMBZ4A1zcABONEdUHMVGd0QMbGN27DswhfvmMobF3M+G8Zd7s4rW1FQVbsE2Ugq4egsUriA5XPOvBgfaqHAdSoBCgJPhyHP6GPrMim4/dSKJFIkB4/7OE9z+b2W72DFhQ1sS6mmzgtHx9rk+dNMt+qQYwvQuG6wTXnobMmhaXK+avjKGjscaVUEonbq4paMr2WL+7qxRxJvvyqqFaEBU2wW0q8+px/SQBKwaGNwxg6icGUBEFP/cHwrKzQOWSu06bLjaIm7FZpMGOt+WuBeADG/e8FIbTz4nonW4Y2H/4mJUJAZX+/ul89v99gmXLlvLYY0+we88eVixPV8Frn3yK89es5vpXvXwKqLQSBQPBdvpYmH1jnfheOgWSqEGUnb2Nm+28pY1H6I4LOvuL4SKtSUch8eNr8FqQGtOWqbBKkFHT+VtJDgj457OsimHfups7rW1SRPrmFIKUZH+b48sm9mDh8sMHVPwIdBnU4W1rYXMj+R/jGucMjI4i69aP56BjVmSkjjy6CXP2EvI0YrvX2Kl/5KfXIOcuQgJLbylpl0wugQK535navdfJNIGU7HHW7TgohtokwAg7egmGriBqeX2SlC5unc+oiHBMh8gfr6y6EJafW3bz7PYGAWKyMVSK72XKeqUgJY4wHMA5Fze467bDF/htQnFUfvVd72DZsqX81u/8Pj/1hp/n5ltuy+wfHR3lJ/fcywsuuWhSGnk8y7Dam+Vv/Q80qWsy65FAxUkDxQKAIMiOGHnT+04pUF8tkvub7G/DwAtADtUnwKpkJDIQ+6gWt7PtZO59NNZ4Vwd2kk+uwxh0Y6RNTZMravpcule/NJ0kCi4kHQ6at5eJiCCVkuimh0JmzsrPmdnvRftyYop+tClvMDA2hrrt20ij89wmx7oEtz5kr1f8t7Czd1j6a8jqxTFIibd1tFAR70VtshCy7YpX5KoQpGRFG6E8DZwDXjZwW9gm9H32qNb7BcP2rz/PvhMoqNuaa4Qoatc/DA3Pgt9k7ma2f9motQaFoUadCpqACAx0dR9elmpCjMo1V1/B975/B9+85dbSMpue38Ka1e312Se6KFHWFqT09fNAjIsqm/fcyTvBZw6Pj+sUowA6qdukcDuvRmmhphCaVTSd2LX4C/QJi8RZesN0g2OKHRAa2fvMwZxh3A3qOvclODVaob2Gb1BL8/ydeMbklj7GaPRwcy6YQyYrTyu1W7LtoWx5VlCYpusus2WRrduQ730fqR97mW4nKqZWwbxgBVJRsfYnBSvtNLgieBmRxy8usFpexSJYkOK7B7epqc1+45UyKNWp/ZFJjvGTGhqTujWPrh9g21eO/cSV45Hps7N2KcUSc1cmss8Zl+/H497FEBhtSfj4N0CASQIybt10ECGBJyATOtu8uXN4+pl1LcvUx8bo7j6Mq71jVMZksAVIIQtClFiWIqcKMu28dNy+ojCOfj04gOEtjYuMaUOBUGFMVMwCODdkB24ETNsXKNeQzjdn9yV2LpJ8d5jJsTxh/0I6yk80CRIsWJ7kZCoUn4VQNHnCFBoQu5+iiJ4/dAbATTJ7TstVebk3V4H49FfJdjEG2b8fdettJxZI6aqgf+lKzAtPzbmCGwtaMh06d2xs8FT0mNoTqpaLtMOJ9bwJJSJUERUVUQn0eE3lyD5kk9sOoYoIlA3mZtURGkHHiQl1RvXg6lJY410bU8UkaqMgtpepqoj+Fd2c+39PZ9aF/eNUMR67sm+n9YjqRAyGkAY1SfP9iMdoSWYtbDubywu6Zyc88cAxEEJ/7959LFzQOgbIKcuXsWPHzgk16kSSEdlDnSFKs9c4Lwchy6IUMSdtldeFX9Pfgg1r3yRS8A1EgpQgdufPsT2WSfHa3M7tOX+SkvYW7hOP0THpRJ8Jw26g0rOQaYvOb9mOyZJw3vL0h8fsJOHm8zSSPwnlGYf44tyxujGG3nkYV43lHH5WOgGaBQxR03EiyL33ddq640bMNWfC7B4PpNgJ2tnVSropx7AZ8MLMm6Yb3gqsaMIgolrR6eQU97PM2sU/wrRjbRIDFJRECQtCDEqqQcOGhhKbO8jFREnM2LCxURyAAQtsqkEUx0xJhxTfkFcjiDF0L+xi1btP4ew/XoVUWjb0uJD7bzOojpCkZVGc7VD6DNNjoyZokLJfX/90FdMmTcFky4SAyk/uuY9rrrmS+fPnFe5fseIULr/shfzwR3cfVONOCBHYHjyKQcfEWioGjabOMLu9VXXJZN+p/UlkMqoQLdaWw4QKEwbFdZOWL+yfRqNNPS3kNccI2ba1sJUpW3cl271JvalsPJqKAwCBaV55xqtREaE2awWSyyd0KER1T0/b59pbwpAUMQwtJ4JqhcrF1x5sEzuX5ze2dGttpxZsGg99BsYUPPVnnoHjIVx+iRgBfepCohsuovHGy4hethq9eCZm9ckZkCLixXFsqoSkj6dOc+kN9m+rTt7N5mVKJSxOUAjlZmOGVq+z3ajE6qlCZagEDWphRC2MCIN8TB5Dfj3j7w4EKioiEKt+Kld/2ZvhN6lnaTcrf+XkosLHlTx1HzzzgGmTBNWGza8UJqR04tRDxXu3bDi8bApM0EblX/7tP3jxNVfy3//vP/jAP36EmTNnALB8+TLOX30ev/Hr72KsPsbH/+OTk9nW41ZG1T42y0+YGS1nmpmLdRrTDMhW9gTPsjC4NF08FbyhyXjfifuxShkOu3Byo1sHCDkJGNe09EWLYAIyg08yKZcBqzKjXQPGi8qpHbvgQEc8OPoJ8Nx5DRRnIm4ioBSV3vmM7Xuu/XUfhBhfh+634yAWJMlliyB9M5E5izA7Nx9UOzuSp9daOxWKMVbSuILAg809BtAGMzoCTz8Jy06B/hjUHRhAHn8c1j55TLH20YoFRJedhenvgShCPbmZ8K7HkMHm5Hemq0L0phfB4lmWqVIKE2nMRStwHTxPnpbeCwOIcSQI2dIuHpL9ruPZR5mUoXHGseUTvztJwTVr394/WybNtuyrEIrPEcTxVlq5XDv1RHvSwMTKILuiERFmnd9P2BvQGDj8MUAOlxgDn/6/hjf9vrByddFcEKvbaO1Z5cpqIIUk8aykYdvzh9c+BSYIVJ586ml+83fex9/+1Z/zN3/1p4DtDF//8v8iIgwODvEbv/V7bHjuxEv8NFEZkwG2hQ8hJiCgQsQYRjQh3YTS1Z4x8UNUFtmVQAY0iHecH3PFP0vTEOUviXOzlVJhsuwzkKY27UBMJk8RmazGSV3O3VokHpS9k8fAJRkL2/g3ulapWl9H7TsY0UN7UV3TmtowISnEe4Zw+dnUDwdQGTgAax+D088qnLZM1pwiN3H6EiOZvXuQO25HBgYwDz0MXbFN28jIMQVQjMDYz14JS+dl3kN9wUrGVi8n/N/bCZ7LpjyIXncJLJxhf6gEMcR70zexxSudE3uMb19fSrwaMN95nMrLV8UAv+w9NWlemEy74nMhhJnFtWuDybyG7UK1Q8akrbQt2kguwm1ryaxTlDD9tF5237uv4+OPRWmMwSf/xPCu92sWrMyu7lTMpnRqZO2NwLFY4Nc4AuZiEw749p3v3s6LX3o9r33NqzjvnLPp7+9nYHCAhx56hC9+6Wvs2bt3Ept54oiRiAYp6heHadv0LsGb8HMRlJKuVlGJ4W2yLTkGL+9KDApS1XATyMmqvwtYFEP7wFF+nJcYQPggJTmZK+4b+eZFgdFxPqE2o6JrXqV/CSPbH23dxoOU+vqHCGcuSseLcUhZcLwMmBSB7p6Da+R4ZHggxalx+xIwCZm5zLcLSkCLAdauxTy3AbpqcPqptsyOnbB7D9JojPc2HXGpv/4KOCmX5TidpWm8/grUR76GDI8BYOZNhxUtbPziB+/YuM4mFuN9M7l7mLIqIoL+8kOEFy/MLDaaTN4wSWCwZJtYlGNz7fhnTEcEVRBptnVQsU4lPUdHgXOLmJnxGPQf4/LFf4L3fNDNI/G4HV++U+W1vocSAxu3rIjzAwWGhSdFbHh68rPMt5KDOtu+ffv5r09+ZrLaMiUF0mAYbSKU76VS1Mtit1Gj4wRkeXYjjJMZ5g9znkT58JBupRUQ99Q2L3kJuGglhTYN3mBSqjrK1+/jl3GMicG0WaCqoMc6P2icYg7sRI8MIF09nc449ri8fiUGLPl7ZozBHK7YIiJw5jkJo5VRZXmSaaNHyxkAHcG6ZzBXXwHT+6zawwfPAuzciXrgEdTGo9+9tHH+CsyyFqBDBBTUL1pF9fZHADArFiTqntJjbMlxtKSDKKP7hmDzXsIF05DZ0xDvnc2SmilIKTOiVY41AZQ0Evv5xIjdV0J1EB/FAdrycq5GQSNWdVVYNgZMZPcbYxhYd/jyYh1p2bpe8f3Pa676mZhCy/DjQoQQtLiHQqptdyDFRV3R+vADvsOvbJqScYkhYsBsoskryFmxedZsCaOiXI5MoCJQCbyJILdE80PrF3kSASawAeUydiR+U9rztslx/seen6yZf+b8Xhv8be1O0s4gOK5HRKjNPbVtuw9WRu75uqV7Opx3HEhx4egTDyEnuXulN2fDr0+qKAUnnQxrLoRLLoPuaekkVAZSFM35jNz3QGGuuwZ6e9L6c2o8Zs9CX3sVetWKSb6YyRVTqxBdc177giKYNd61BKqzvhD3mfZFM29Udo8xqLFRqjJGdU6N6rnzqV55CtLXVapNbm+z4q3QxfI3vteNil2bA9HUggahS5bYQsUUGSEfej8vKmZ0It+qPq+aIJvk0A6PhuHnRxjdcegWJEej3PIpxZc+LOgIfPioMDZCrTSNxsn3imfHIjEIFmB0BDZvOEZsVJycc85ZnHP2mUzv6yMImi2BjTH887/8+8GcYkqAPeZJeliEolI+T4vgku4lSbkqXsbeuIxT86Di8PxFjEW+XmIQpGxwuoTahzT4nC95NiBTX+57ATPkztvy2AIxQpxFtbwBEpez7TdU+hYxuu3Qqn/QDYbv+gLdL/gpy2w5n88CMWANiSuSTlLxLUnGZ49NQUdEG586NO2eNRsuvwq6uhPX5JTlanFc2Tjmbn61q5ChSwgxsTObvvRi5LlNyEizMerRIPrMpeWsSF5qFUw1RMYasHlP+wShYw3Uv90GrzoPVsxrcb9t53CvUl7tEyhNpVelpKNPfDbVma2rXAwmARbxmOMZ7LoyeYbHrod0hu3Qxk6CVRXFLXfHZBkAFbtdgyV5DdYFOb1Ye7SNrZK/RuHJj6xrdUHHqRjOWF2nN4xwN8rd3Yo0CNFEBHG4N3dvDWGTHUu6RL39mzWi6PAzKhPO9fORD72f89ecR6sAUFNAZXJE02CfWcdMdVpxAckPCVmQ4jY1hZRUforbNuKrhPycLzk2xQ01pVChaMSMxaoG4jqLmtUKAPl1FBRyh5rcRmkVjG0ypTHG8F1foHb+daiefo/WSRtnABOa5udUxFwYA1GD+j23WQu6yZbuaXDViyGIh4iYpWvLaHnXUyolMRiM2+X6agCN17wU2bQFGRhErduI7D+MkXjbiJnRYw3GO7F9EGj8xisJvvUg8sB62DsI/dNKEIGBbXtRewYx//Nj+N1X2Pc5M3mn4rBSFqRYN9Uw0IWvXNbo1j40cXWMex4qzr1jELSJVQZiDTmDTL4hGyMl2eaY4VxNSmySwkQFEYMPjVD1V/6F7wnUD9QZ2Xp8sylLTjWcebGhUoMt6+ChHwivf1edC67Iejm5J+VioYeiCZNQbkViA8NphMEDwv/+x5EJ4johoPJ7v/tbXHD+au7+yb186StfZ+vWbUTR8ev2dTTIgN5kgUqbUcT4apSM4lmaf2dWQB1KjoUx8bYMYPUm3qbaNUmUWkk32VV4BvQYmy3ZjnQJW9RqEjRgjXEltxF/tZ7uFxEaw3s6u+7JkGiM0Z98HemdRWX5atT02RBUYtYqdqdsYwuUsChPPUD0/LOHBqQAnLrKgpQyxqDsObRjW4omE7CG3PlTicD0PswZvRgx6EtWw7pNhN/5AVIy3hiAWtX+GB0bb+9uqkuvWkJ0wamY+TNBa9TTmwnueRK1fS+M1Dt8feJ3JgiIXnkBwUgdeXA95sozyd5ID4AsnYWZ04fsPID5wj3whosBl005hdySuPEbZLRukxEqwewcRG3ciVyyOC7XonUmDbpmnBE9bRIY5jMkYxKPHBvKHu+7pqJ0vF8yRzlVTuABJDHWYFdh2hBWCo0mLFEZuevYdtvxG3i0u9fwlvdpVpwLUcNec1iB175D098T0awKt981ARAhaEzMURV15jAuqTCsfTDEHAH7FNuOCcjVV17OQw8/ylt/8cbJbs9hkUqlwq//2o285vpXMn16H2uffJp//NA/88O7fnykm1YqEaPs08/Qr1Y0DSDGraVCSZIUFooDF9IyaH+TZIYBSfNpCDA88Azdvctj6lcl2413bAY32F5v1RpOtRFIsbogrsgIMc3rloE0vVMaMJX0GvMr+0JGBRjd/libq598MQO7GXvoO+kGFUClBvVRqi9+A06NVyZ69zaiDYcwY/KMmXDq6VmQIsX3r1A6YL78oiZPaom3V4j7S9yAUxbT+NlXob7/Y+jrgXoD9dxmGKujTzuFxprTYfYMe3gUIXv2E9x2F8GufZ01yGtX49oL0KtXZAxf9elL0WcsJfz6j1BPbCS64uwOahNE6cQeLHrFGhsrJgk4491Vl+hPa8wFy5BbHkYe34L5xJ1wxSpYOc/28eE63LMe88BzBNeuQp25AOmxN9LsHiS640noCoDFrVsm9mpT41njgZ9WV5TeKUuspjyIUhbsuPx4Ig5/lyHbJN8x7pkbOtOqRQaUSU3tMtdlDIMbhtlyy47S449lETG87Y80S1fZ34E3m0+b5iKaFz9HRZSsKRSRF4k2HSktSNHJmnbhkiNHRkwIqNRqNe45hkNb//Vf/gkvvfYlfPJTn2H9c89xw2uu598++iHe+ovv4N77HjjSzSuVPXothoh+tQIl6aOrm0GoVAnDaYXHGc+FxOSZjzaDgWUhhCJUvm/gEQ4MPcHg8Hp6+06nq2txynykxRJVkGVMXH1edUXUue8KLVngk9OYxCoTmlfzuYWqP8kaYGz3OvToUaBK0BGMWo8EvXMzam7ryaXx5CF694IAXnAZLFlKdvLMfk32FPP9LfuUtXXyfvtuzZlzmeJnKYLp7SF65TXJ846iCHbtwcyfjQt8hsEa7s6ZQeNnX060cSuVr38fKYntY7qrRGedgl4yN+4cY+izltnT+jNmYIFF45UvoPqvX0c9sh595sklTJhH53ntp7sG3ZWYKSyBfoGybszusA274FN3YaohVAMYGkO6QirvehHM6M5kSWZGN+FPryZ6YFMGUGTF3iSVqF7S7elSpGmZYffnyofSKAQVToNrQVBr8JPGSEnfbG2y8VgKjkKJhXvamDQmpEBjqMG27+zi+a9tQ491muzw2JIV58KyM4r3BdL6msM47oTtmpoqGpc1STBNXlNgWLT4yN3HCQGVJ9auZfGiRZPdlsMi55xzFq96xcv4m7/7R/7jPz8FwJe/chNf/8r/8ju/9R7e+OZfPMItbC179dPs0+volrkoQuoMolSVeeELyg/KeM7kMjWb5sk/OazpiwcYlFCP7Cq1Xt/Nnt0/pG/m+UzrXW6ZFf+cAeWjTZu4J8ZoRAs6lOS8iElSICUeMU7lVXj92cYboxnd8QTDmx8sOeDISeOhO6leeQMmrGZYFZe1WD//DAyMjx3oWC6+FBYtiX+U38/M7SyiWUyuYLI9Xi0rr1geXJadrOz8AEGAmTc7S3PnVvDmpAXUX3oZ1W/e2VRfdPJ86q95EbnoZWk9/hzr6hUYe/0VVD77PYg0+txTCs8rygIzx1xkymSMQXOiDdSbV7Ay1oAx644eXLkSZnYjOZTg3I7VuYuJRuuEXTHT6RYOBkATKhfl1b9At4K2YMUkD8mKSjxq7LWEogmDZrDlNM0Kl6un3UPO1mFsSxK7lOwQYeJ6o0ywSgHQhr1PDrD2/c9gjnNrhMuubw8AyyTIAZG0H5RzppWqYcnJdTZtqIz7fAcrE/Iz+vA/f4xrrr6C887thPY8uuRl172YRqPB/3zui8m2sbExPv+Fr3D+mvNY0CbZ4tEghoghs5UBs4lRs4fptdNK8zsYyCQJzCt9jIj1RFGSJg/0j82pkgRs9uQA+mdeSKUyM9kXNQYhD1I6sddtoeaQRCcft9VFqQ0kzSSdZ2iKxBgaw7sZ2PAD9j3yBYY3P9BBw46ARA3Gvv8l9M7NGOOtYOqj1J+4l8ajPzo0512yFJYuy7IHLW5PhjVTFnyYIq1jOtvBwABG6WKmJC9tnqfJta9JmSneBywTs3wJpqc7U0xP76H+2sstm+SYw7ifSSnbY+tjbj/1n7mC8Nb7qf7LNwi+8yDyzGYk0PHHIEHMmrgQ9yr9NOdTcQkFjT123fbyGyCgLlraBFLyd8kxSFlDWpNR1WSAFZLeArG2Ky6TcaCMB1IMNdWgEpStsk18Pdr7Xd7OMkniTcaJCQOJqEqDahDF9tZpMkMRkAB2fG/XcQ9SABYvN5QM++i2U7t9Phbbdx6S79KrjoxR8oQYlTlzZvO92+/k//3Xx/ja17/Jo48/wcDAYGHZr3z1poNq4GTLGaefxvoNzzE4mG3vQw8/Eu9fxdat245E0yYsobRIrucNvk1jv5CqXdxI5n6b5hWOANqBA0AFXcyZdw27dt5Oo76fnhlnWEM+37g2oWBKWBM345QxKpnr8BoyTrGqnmep790w/oMPt0QNGvd9115zrdtaydUP4QCx5kJYdXpLZsuXjArNV/XEq+isni5+ZBs3It//Plx7FSxaaAFRIX3XZpu3K6vK809WfkBjxUlUHnoy2RytWenlsIorkJQRkFaMjwgsnIU+/SSCRzcQ3vMU5r6nabznFdBTy7E7Jbc2ZjSVMpZ98Xe84iyYPQ1ufrQ5HUVXBeluvbIVEahVPLBg6w06ypfTos2O6SjcF9dPmkvIuiC37lrZ0PiCxBDFzzVjkxKm5dwz8jXHql7nwFMDba7q+JDe6W6cNYm6JoaHRAghxa90SCMJ2Oei1LqYni2WjIBhev+RWdxNCKj89f/9k2QyuuG113PDa69vWtGL2AnraAMqc+fOYceOZivwHTvttnlz5xYeV6lUqFarye+enmJ7kCMh2tQJpLt4Z8GIlLEZKZucfLWDO08omfrsMxZmzLyYhowRxNmIHUgR/3iNtXrLn6sD4KHj0CMty7WpR0SoDxxbABRjYOQQR9NcucqCFCgHkj7m9JiKxPQpb5eSefBx8bvvtuTLI4+jl6SeKJk6itiYkmeaYiHTzJ74BfKsSl/2vdXLF+NnKvbZk6S6Vn3LGBoXrISagiBAtu5Fff1e9BtemKh2Wtq2x0DdgZSsKZjAC5bbk3/jYfvu9XVBI4KRBkbr1oxK0zvTBsyREmCt8aoX8SSnkglFF4ISg4DJZ0xO25QfbQRfPWFHLEWevZHceQwqFM76taXc/2eHMAjiUSJRAyo1Q+DdFwWWvTOGBiq2RUlfhgoNulQjecbKeIDfZLWn8SbHZ4OBgYEJrBInQSYEVN73B3862e04bNJV62JsrHl1Ojpqt3V1FbMT73j72/i1d7/jkLZtorK//ixzutY0bbcTSzOtZ5w6pt3qOe7NAuhK8YgrogjDHlRQnm8mN2+VnKrZHTKZjJynUIv6E1VA0VxrNI2BbeiRfS1acILK6WeVzkw584RsPp/8PheKwXvYDkSYsVGC+GmqLdvgR/egL7kgPq+CIhuGdsATD7y2Ui/m6pFpWUBvwuaOVWA3Xlq5BBqW9KOXrEkP3rkfHlgPq5clgK70VROx98polJeZPJnsBcyly5ClM2DuNFS3XSyZTXswG/bAyTNbgBVBSZT57WxQysUuPmjBmLi2aS0Z+xSnTmo+TtIjC95zRdbTqCI6A34UNsJtZgKNX/g0kL8hwBAEwvQV05h+6jT2P3V8h8zf9DScdl55FGFl4zvgOBchoiaNTPlUay5EsUGy/eX+pipCUXD3HS3Y+0MoEwIqX/7K1ye7HYdNRkZHMsyIk1oce2GkJArmv37sE3zivz6d/O7pmcYd37350DRynDJYf44Z1TMIVFd2USmQzzScDCsdJuhKV9Hl5bWbcNrVUzD5CFn1gb8QBohCmlihQo2Bv/J3eY+MRkQRjexncP0PW7bvhJTePugpBpgZkCK577mvGYwQPwcT22SYAKjUiN7wOtiwkeDue1GPr0U2b0Gfvgozfw7MnVUMVrz6mtpGDITyI2tLMZBbpKjNu9C93XEo/6K68ggovWqJ7W2aWILZfdDXDfuGYEZ3+wVB4o7uTQr+fRaBJTPsWd3rvKgflMvvU0xHiRTFISkKVZ9mPDZYLZOXsDz+m7IbKf/k3jELgMrVQfkWpE/Q2sLEv6OI0ecGUQtrhNNCfJDSVIdrW6r0SzxddMMwe3XfcQ9UTKOAhXPiWG2xarQqVm3WCv9bWxWTGDD7VRkND9xTYd1ThzcZoZMjc9YjKDt27GT+/HlN2+fOmQPA9h3FPvf1ep16/Qjkt24hFdVHb/UUamoGDTOAwkYNFEAL1ki2TFqFqnciYsFO2Hr0GY8xVlMzvL8mXlwbQDvltKc4bZo8HW1pPMATv4k6GiMa3MXYnnWM7X0OzPHponhQ0souqGjSLukGRYyWs4hKphEROPkkogXzCb5yE7JvP8GP7yEAGiuXY656QY56SGaiLAj1T6qy+9qKARkcyWwKH3iKsdOXpteSQb4llcTnL8XmIlANkSeex6ycb9U17RrWdP6mSm1JE3vlKIVS2k7Umdw3rh4blbbdef3Q9MlZxIIVrW0I/kA0LkNKknQw7iRBbCis26qL0jOkOWYkNtSN6w6EXd/fzt67drLixmXMPH8GYVjOGCSqQwxVzxtJRNM9OyDoEqKRiY5MR78sP6s1MBQ0FRqEHoC00YJb2Si5F84kfQGE4SH48F/1TV7jxykHBVQWL1rI9a96OWecfho9vT0MDgzy+BNr+drXv8nzm7dMVhsnVZ544kkuufhCenp6Mga1zoPp8SeeLDv0qJLp1ZXM7Drbeoa4ETPufNrQbCRLWqSVmsRJ8nqr+ChnTJADDW6MLFLdtBNXh3bLN5yxbnlZ8n/dCt7EKi27uKMxsJ3BdbePqz0nnAwOQBRBQZ4uoIRhKC/q67oLjxGBWpXowjWEt6cMV/j0s0TVEP3CCzNgxQehTe3xP52KQPDk+swm9fxOwh8+QuOFZ3n1dTK5mbYqHXPGEtSnb8f88tXl1WideAalcL+sUntj7brXgQxi+8DUAyRQukWwtDQ+ifXiKZ7sbMgjG1E2d1n2mcT7Uw+hNvejQDIgxRj0qGbfPbuRUBjeNMzsC6e3I3PjK7LXEEhEKBEqgKUv6mXxxSvYfOd+1n52J42h42+hUmnlQ4FhGmOF98+g0EaXgBWDoAkxGEkD7u/dGTAyPL7xfTJlwmkQ3/LmN3LzTV/kPb96I9ddew2XXXoJ1117Db/+a+/k5pu+yFve/MbJbOekyc3f+jZhGPKGn3ldsq1SqfC6G17NAw8+fEx4/HSH8y1IAQ+kpKO2xHrvMmk3vmdtQ3LcultRBcRZk0no75Yu0l4VmX3Ko87dzFRUTdmk6Q71U6EoIRrZX9iWKfFEa9i9K2XX8FV98YaJAAKIp1JT8LwETlmGySXlCx57kuDLNyPPPmfBk5dPysXK8btFxsunowYZ1MNPI/vTxYmpVmhcdhbR+SvSyPQeY9L6mjs4cTWEX74St0JtkkjDcB21brt3qlYnzZbySVER+6q6ZNRlbqtAzH6UgxQrJgEp+TI+6ZXus2NB67siNmlgnPMnP1Fu/tQ6Kr0B5/356Sx93cKOQIpTH4USUVVR5u4FVcXiK/t5wR+fRN9JFWYsr1GbcZhyex0GaYyZ0gddpVFy/9wiIO8ab7cqDN3KpiWouEBwDcNTjx9Z5cuEzn7VlZfzvv/zW+zZs5f//ORn+PHdP2HHjp3MmTOHSy65kLe95ef4vd/9TTY8t5Hv394cYOlIykMPP8I3b76V3/qNX2X27JlseG4jN7zmVSxetIj/7w//7Eg3ryOZXj01x6T4vTEGK64Pt0QkudWrE0UcWC17cBKq33/XS4wwi94P7fu/5d4i981nToxrS75QQd3Jij4epVXt6PHKOqrlnh9hXnZ9ev/GZfNhpej5CIJWJdOWEqjVYGg4u3nnbtR37rTT+rRuGjdcBz3d5bHUO5jEQCCKUA88SXjXg8meaNFsGq++NK5f4j7keq5pr1IyGQ6p9PziAhomf7xK9w2ivngPcsY8OHW25+HSilEhKWdK2qCNEJTde9Kos524C7f0WCKTXcC/ewVXYFkcJVFGIx37CGEahn137+KcPzuN6pwK7t61Y2lEbMafMDYazpdVgdCzqMKVf7U0SRMwuj/iqa/t5Zmb9x+z8VZWv7BBV5dB5+60YKhQp9LWaDr/nOzz7srdR2OgGhpuvenIJCN0MiGg8ra3/hz79u3nhp/5ObZtS4MSbd6ylYcefoSvff2bfPnzn+Ftb/25ow6oAPzu+/6I3/i1d/Lq619J//Q+1j75FDe++ze45977j3TTOhChK5zjTQ7FvdEASSqR0qq8Y0vASf7cKdBoLufbqvgktjiQkjPgzY/b/oFNk2YHL53XECozloL8iCnblBYyazbmsivSZ5lnrToEK4ILzOUxM4osoM3QIQbGyu29BJChYSpfuJno/LPQZ6yEShhXE0dW9kmK0nYKsnYdldvvQ2KvPj17Oo1XXIxZMCtXNtdzO7j2di68Lm+PCxkjTkXjrnHuNHj75YiXAbidpPFGLCQwMTuSbrHn1ia2Qc/M9HF72oAUW6aT96aZ3nR+OMbbQqyu6g6yofaNgVAM2hgOrN3P4lfPo/ekmgfYYpVuC7BiDFSDVmjDUFU6Trho66lNDzjnzbM5+ao+7vjTLYwNHHtjxHWvr9tER947pmJ1D3TCREH65trC3dLIxKnx6ymxuT9sMiGgcuYZp/O1m76ZASm+bN26jW/eciuvesXLDqpxh0rGxsb42/d/kL99/wePdFMmIJ2NaMlg0cr1NDXpx7Tx2kkqbcOcuvOaMKaCHfDINSEDRIoq6ACcFB2W/FYBElQwjWIvrhNe+qbDNdcm9ilNKp8Wkl+rp7ZBYr19XN4lX9IOCdogjWZPjrzIyCjhD+/D3HU/plrBzJlJ45oXYHpTFqS8kYbgJ48Q3v1IsknP6KX+89dmjczdNRuvPmcx2g4cl06gFqQEYXqnJPnPTd7xqxfYUF3ZpHpFJy9iOOxEo3IqHDu5x4EXVXqsarJrKZZANIFqrT7KtCxzH6yXSaiiNHYkJPFVMq2PNyiB3gUB01fPL1EzlSPS57+xg3nLA+acM61wfyg6mXx9l1yAvsUVLnj3XO76m6Nf3e9L73TD0pXumUaYOMVg9zhAiomfUyVWw4UtGJgogjPOHuPRh5q9ZQ+XTMhGpVKpMDw83LLM0NAQlcrhzwlw/ItmLJoE+wvLw1qQkmxr3cPTBHKdLrUljfrp2c1k2RLJfpICZGfENoNm0+SpI0x0dHlpHVVy5lnWTsRy580gpeARG5zhc/rJxLiJ89q0UtGBwIYN42qqGIMaHSN4fhvBg4+3PocxsH0XlX/9XAakANRffSlUgmz/LWKPpA0I8jqy0cUTugqKgEVqF+KTWNa+q+i1ytq2pCHs3bG6UMVj7VU0Slk7k0r818+GkQKApjcncfNtyxjF1xOKzbJrVTANusIoDWkvFiBZ5+XyuvoWFKHb1ucXDJu/sYOu2WXr7TgAXUm1IsKC1dPoW3xszVNhJb2TCptgsIs6bQlxTwToioO/VVrcI+gsi/Whlgk1Yf2GDVx95RUEJd4CQRBw1ZWXs36cA9KUtJeKmo5IeyKsFfgw3najwFTiGBItllAWXLR/EzI1ON6W3BCUTA7NdSWB/k1z8bKRLn+txmjGdq+bUvuUSaWCWba8ReyQdArzpzIXYM0aUscgxTu+bfTguGZ158TzFQWPPwuDw9Y4Ii9aw2id6k13oBpZdUDU3wPzZqQbEial7EylSMjb7W5I7kilM2Aks0+aPyAYo3JgRSNiGRCldBNIAUMQtAIUcYQVU/SA7XpavN9O8tR/sdgyVdWgFugkF1AoEdUigOY8+lDFoC42rC0fWux9DmgQEhHSsAEEG5poJIon0uaDO4vrYrjg7bPbFTqqZP8eYSBeqxok9ngyLYBgXqxnTy3jvl4EWq2IwNDgeEDk5MuEgMqXv3oTp5xyMh//tw9z1pmnZ/adfdYZfOxfPsQpy07mS8dwYLijUSpqOvP7riRQsaGolMcvESgP6uYmdJHU+6JsZB2nCNiEblDc7xO1Tutz+eNlUzXxO+UPeqmbtMZEdUa3PpI/akpiMWsuLL7/scbDn9ucx432WYzyMa0182UM1OuoaOIAUuoNKl/6NjjvnUjbD8DQCJUvfwcZbGZ79QvOyF5zW/sWf2fugjOvigt/rxFlUKFObNxNpo+2mzTTeCj2VbRAwmHJvGrH1dfulS2+06mHXjNY6WS6E0IiKsreFyU2d2m5usjEnj4RaQwVXy3W2RSrUQRxxudANNFQg74lNVTgEGfnU7Uvs0+r0r/02GFVtBZu/3qYYHVjjMt52YEYQiKmqzFP/eczNJYdkyT0vu3E8+e3V9UeSpmQjconP/XfXHTB+Vxz9RV87rOfZGRkhF279jB79ky6uroQEb79ne/zyU/992S394SWGd1nI6gkjXti/xHvzywQM4nWsiLEE5Db3wFAaWXvkikT193MYqdxVtoNJ8mw49uq+GbopKt3rdKjDBAN7GJow4/QY8VJMk94qVZh2bJS1U6GIXG//Y7lm0/kTClchI9SEYHNB28PoPYNUP30TeilCzFL5lvgsGUnat2mJFtwXvSSXA6vjkZ1nz0BMrYgBkQjSkjdfB2b4G6Y/etem9avWXpTfXsPF/MkOavBMi2qvVeHPUCyqN97wC7abGofY4O7OZanRaVUA8f45K4i/xtNKM2xWJwZUOcwJUfEGkP/LM2L/nIpxsSu8JmOaT2K2sd2sb12zVtn8r0/b5Gt+iiTWz9fYdV5muVn2OeQmhhaF+MguQfxfQCI7ViqCTvXfGOcsXoQg5QGQCQUBHM/rDIhoKK15t3v+W1e8+pXcsNrXsXpp69i4cIFDAwO8OBDj/Dlr3ydr3ztG5Pd1hNaAumiuzI/3eBsCxxa0d7E0kKHn1GTdMBsZEBQvJIrVSdBbFjpDRja/jVKsoNJixEkaaPTLHrlrAoqBir50PrGYESjx4aoLDidyvxTUbUeTFSnsXMdY1vWYk50ADNjJqgOA7xJwb6WkqPBpGkrwX0PdlJRWxFjCDZshg2bOzyg5HsObBWcqCACrUECL3NtId73b+LEVvrueIPJMBVWDZQCjdaH20itGW7ISGwyJukUJs59uF1rrW2IK9+u5WELexc3IugmtVexOFZGYVCxbY49LlVj5Y2EIyNecsPma7HtEGafeoRn4nFKfUz48B/UuPH3h1l9cer9VfE4NBFnNmbvc4TxQIqTNFuS/UWy2FBABYgCw7PPHFnG6aCiuHzlqzcdddmRj1cJVC6ZmjFZmxHfODWjEzGJiijpn+NV+HnJZRGaotBmbBgc22MbAwL7tv6QnkUXooJaR+yNAFEBSLE/YxdII01xYkSEsG8+Pee8HOnuS7aJCqgsOI3K3BUMPXYbemjPuC7/uJISu50Mm1Lwu7w+v1w8KWsLWCVfMIpQe/eNs8EHJwaILj4d+nvKr6cQrOTYFG+7BNnJsL3R6XikiLKKAREQBD7jYQrASrYxFWVtONyQYNkTawHsSCARt4423vBRdFNsJUGBB09SwgMcqhVI8c7TyXAUSoNu5SfUi68/uQa8falESJy7ppB3TrxdgqoiqAnR6MGAysMrS1dErL7YOgyIEGdKzt4Df81YaQJshjC+//m74xQ/YiCqwx3fPbJxVI4Ce94p6US0ac74nEjea8ZJEmgqxskJ2+ItddsZ0BZtLLCNMYp0OSbZT3XGKaiwq7C+IjfJRmMoNtQsHg6zhoDNzQuqfbaMd7yIAhXSverykiNPENm9G+pjzQ+3iEUZx5htnD4b1xdI7FuSpXolRM+aOdGWT0iia1YTXXWuNaBoJa75Jr0OpHklLsoxECR/s27F/gfcS+DsVcpfN3uM9bo2KIlsKHzRBEoTBhGVMIpVPqkNQSAkH0ukpucOPfWQHR5M7JljqATWSztQMZMSe+24FjdfjxXlwupL8/CRLFgcKCplMlJRGLrDMRupFuOBiqQ2AvIgxUm6KCq6r0YbiKwLr/Lqc+f1XXKjMUM0duyAFIBrXjVGlJiOtDZIFgHEZPpqBZN57f1DkzRaAmMjwsjIkYUKB8WonHnG6dzwmldxxhmn0dfXy4EDAzz++Fq+9JWv89jjT0xWG6cEaOhB6tEAlaDXbhBa98r8m1swUIs2hWnuk/2kNiDi/w5s/drtSNyPTTquxU3TCmrTF3eUC0jrMUylApXutmX9RZ9b/SeToh9v3V9dKIV09RH0LyTad3TmojrkEkXIk2sxZ56dW3rlynXCpsTiqGIdA5RC0BOLXr4UtfvwMFp6Vh/RhatyWws6RkYEcFmRi3e3VCO4NYBJt0GaiC/t182IMEzYEpMcoxKD1ez7rGKjUv81d6Yxtmbf3sTWqRKVjX9ut4YWNAYVsxNCfgjR1FREGJjM4iLLaMTZdzvyHIpVE071kNw3jRL7DAQIMElE2TLvJtcD8wzexu/sYf0t+7nwxtnMW1VN7DbyYrRhw52D4ye/jrCsPLNBEM/g5c7fKQD0y7QKQC25MtXakb8xEwYqv/vbv85b3/ImVM7J+oLzV/OmN/4M//nJT/N37//QQTdwSlLZP/oUs6etsd3NjVJtwErSxbSnAvLmcSINgSoknH1Pj2RscqodZyPjzpX89QYxIQFIZcDDbR8Z2UbYN68tQMmLIV3BZy7Cv0b/wrQm6J194gIVgEcesgHfTj45uz0/d7YlIVL1iH3W7U8thzG2kj57mRfj3R9sW/FxZG1SCtVC3m5jQUN+bZBnWkQkA0L8Ewi+Sse1z8S2JLognH1qxNrE+JQNB7hXt9XUlLV78euqqchrRx4SYFVJcf0BjZR1bTFEOQDm7ksCpOLzpLevyE4o335/hWRZmZOv6OORT+zie3+0lav+eB5zT681t10bGqOGJ756uHKDGU45G06/yBCGsPlZ4aE7rM3JeCVqpNddDMIMVaJ0qsgAlXbgI2VbTD5a5xGQCQGVn3vT63nbL7yZdes28NF//Tj33Hc/O3fuYs6c2Vx0wRre+Y5f5m1vfTPPP7+Fz3z2c5Pd5hNWBsfWM722ijCM4xm3m9STEYeMuib5qyzY0GJ1kX7X1U6VA66Xx9Fm43oyRrPZc7oVl1HSEZMCUOmZ77EjqeFtyyOFbDLCspW8mzWU4GWfO3HFGOSHd2BqVViwgAyqa17oF96uBKS0vJ0F+qXDmPTTTJ/WBJ7jPfhMQgY8lOXIMeC8Z7Lrg7R8EWgw8XHpGWIvIS/zcN5uwB6bAob8ekQKtjWL5I4pYh2aLtAGVM/YvdhJvzx3kC2TWSMIHbMqFRVhQYpuIond1wiFMu29nBSg45YoNKomvPjP5/PI/+7jjr/cwfm/PJNll/ckj1yUMLCtwV0f3MnA1kPvfts7w/DWPzQsOdWgo/g+KcNrboRP/7Ww9t7xqVceuSdg7sLIPpume2NBiv9MfBHajYL2JgXEKrQjLLLqzPPH3Yqbvvo5uru7uf41r2dwaKhpf29vL1/78v8wNDTEK1/9M5PS0KNNenp6uO/u2zn/4isYHDx8niTVYCbz+68qGuGaJHmwnoGrURJnLI73iaQuvi3qMwBhWk9pjBavvPHBThvRAjrEW8W3tkMB0PmgY2Vls9wy0eBuhh/6ZkftOl7FdHdjzj4bVsaqEcf3Q8qied/Tbc7s0oFRLJOSn7jz3LIBGhHhf34OKQrWNoliqiGNF52NPv9Ul/CmuY80jXoxQGjFCokB5Vb9MeyQ8uBufs3OUNWxBWFQNPGaDDuTZWBS9qHTQGahipKookEHSeqI6w1ykUorqkHYMtOyFXeNYZwzphWTAkJVNagoa2/TLtBcKFGLMjErleyPgY9ASAMF3PWhXWz60RDdswIWrukiqAh7n6uz47HDk2KjWtP8zr9ZsOKi9jrGzX3/3IeEe25rD1aC0PBTbxnmxdePUq3FG2Nw6QBIgKYi+fcsVbOFSTrIMrFlw/j1+YW3zmPbtsnNoDyeOXRCFjJLFi/iW7d+uxCkAAwMDPCtW7/NksWLJlL9lLSQsWiPDaE/TpACJOYbKLHB3ryFdAejWCpuSdemSCc6X5+wTfRS8RtnaP440c5Fue3gmd8gBL2zUT35pHQnhhhAn3Ya5oYbYNWqrOdYjnKzzyT+4dsliLHgpJJTkzhAkHhs5U5eCTArc+qmSRbTVWXsLdeiLzi1mB1yIvlP3OdMUY+z3yXxZCpiacpFvP/9RuVfIfEm4qwayIo25ccWnley5VofksD/9hW3qae127Q9jwMpELM9LRtniFpmq5YExNlJOLJh4TEE8fh30TtmEVSF4d0Rz357kKduHjhsICUIDe/8e+ibaTLruzzA/elfNcw7qc24KoZ3vW+Ql77OAylxZf6RCp1RPSq09fCJ+1hURMJkz5TxBnrzmw+0LH2oZUJAZVeHxnA7d+2eSPVT0kZG6ltbJhVLwUd2W6qyIQYw3iq6g/qMs3nJc7QtJDmmTTuTuFQZRX/xMZEC0yY1SBG48dsUzjqpbduPNzGAufBCuOiiLCCM+4QDrun07FgTY1MtBMYqi738PlmQSTOTgvfbGKLLL8JUJndl5kvjxWtgZm/zLNAJmxAYJCD52HclxxBljJ8mSokXBSJrF2k2ZjNNJ2fNGrQaE5umtjkwEJsXSDLMhXHmbe3PGXsipfb5ebBn99dUlIAU6GQ4ETyLicw5IfZY8hm9uLIgBkCihLBLWHJJNsTD4ZLLXg3zl9rvZc9YxIL+K29oDRTPOr/B+ZfWS/LvSBzgzb+nqQuyMyt0fKhVuhWJSe642Gq5+upharUjl5JkQkDlpm/cwnXXvphp04offE9PD9dd+2Ju+sYtB9W4KWmWnq7ldHed3ISgfUkXY/HgFv/UIdk3JWZW/LJFfKAQq1gCSReTbZZ0JvfLlI2T7ViRppVv3FR/UsydN3HDdn6bRecoC3p2PMuSJXB6nPLCGy3dYJTc0xi8mABMJd5WomLzuw3QejIUgSDArDg0rIqZVkOfcVLxTNBmxS6hSftWLG7yQOWrlESNIx2oROzkmZ1gi5rTvp6c427hNcWTtyQQEgtcNGn4+uJjnB2KGzUCieJ8Pm5Ka6V6MQRKU5PIulR7odhDaVBTDbqCiK6ggRLd1PZ2jIrCEIqLuRIzXFhwVaRdds8nuVcR9C44EkHLDJe+Kr2vra5TBFZfAWdf1OCCy+ssOjlqKnPFdaOeS3LZGdOovM7I1u9bViMvaOP4s2zfTF5375hKBfr7jxxQmdDS5kMf/hdWLF/G5z77ST7y0Y9x730PsGvXbmbPnsWFF6zhXTf+Mo899gT/9JF/mez2ntDS33MOfdNWNbkHSu67AYySDEGtHYvi6O3MqFgwmceGsCk8T4t2LF6uQxPr9ZPhzq/TNTLHAPniX4vJe/j4xxSBEn82jSvRQ3vHcSHHh5jTTqOtFabBxkDBtPZhzN1/A7avdDCimOm9mJ5uTCVEBoeR+uQYMpq5/RNK9ZqPjdK0P8d8JNscsDEFHdgrC2kwtfR4L9/OeN6puD6NHygte/4wF15fkCQ6adFLJxgqQdaVOYy3uRIiJlY95V88B4xSRsa3r6k2eSzFkXYhcYXWcftaicuCHIhp41wmSTyYlIWyfbkxfPgn2moXzJjjt6a1VGrwrj8eSe7XuicUn/zHLrZutE97znyduCQ3i/X+qcRGtEUpDpzY/XZn3nW5qD9qDQMDRy6WyoSAyoP3/gCwF/r3f/MXTftFhFOWncyD9/4ws90Yw1nnXTKRU57wUgn66ZtmDR8TLxqT+QOA9iy1UtVKDFocC+Mm8zzKiSepPPwuNYotmPQSMJEZKGOw4q3WM+d3rLBjXfKgyOTmglYsTDvj3bgxjV0bWpc7HmX27GbQ5oE3Hwy2BClOvP6TObbNIY1zV8FFZ9oNWiNPP0flzvuR4ZHOrqNM2nknFIBb0AWMSasKQAXxil6KblwBaPGMPIE0/LtXygWDa2+w6n0XQyA6UYu4UPl59sepQ5JlQuzFZ/daJiR/TH5Kd4HoIpPtOHb94+eOSQFZ0ARSsm3SxE5WFu2VXL9la3xVUbnYsmUMy8YfF9tUHkqJGqmHfGH3y4hLT2C/Ayw9VfPevx/ir359Gju3KvbtEaIIgia0ZuiSBhVJ72Mr84C8tOt3d91VY2joGAMq99x7/2S3Y0raSE/3KRijEd96sYiByKtyCsLpFx2fiBs9RHDh6lu+XkVgxUUlzWXcNZg4WFzB+YN4s4oL598cn0UpW8C63EftDI0bY6CbadXjXpy3TR4I5rqKcRPZOFb6mXm61XECVL1hRynMqScztmQ+1c/efFBgRbbshrFGtv7kpFl6O90+DpAiZFQ9eVIyjQRNzjA2vSkqk2slzeOj3PHJhN98fjthR4n21nX1dHIrbnfalhRI+OyJNtLkTpyJNEx6rjCJbtoMQnyvm7znULPE9z6GRBEqcVHOsz011ehgkrf3oUoj+ZVciTGs/94gQzsO/zsfNYQn7zWcer4FF1kQZTMZOzZDI1aVFQNAA9Zuqgte8cYxPvmBLn74nRqrL2lmIKsSEXrPJI3w275zt4IzDjN96lPTO7vgQyQTAipveds7JrsdU9JGwqA3C1JyUrqiFcEYzfDI81R6FqDCKuOdgYrqdsyHUcmvZGWXuKzG54eUfi18dzzsYlkdadqf+euYnyJpB1KMQQ/va1nmuJVNm2DlCorTW3vYFsbVRdIKSCsoIuBcZ2p6/gLdXdRfeB7Vb/94AieOq2lEqIfXWY+f3JmT+ChiErpPRHfEAjlpdh32zp38V8wgiEQeQMgWCIM44qs26MLMK1aCnCu0SPHrkj+3kuwE7XCGz4JkrlMiajmWJQErhdfnmAwLboImt9gysSBKYYgQKhIRiEajYqZGJ8HKfLBUVI9CU/Wu016jbfCG7w9y38ePnGPH9z4vrLrA4DTpSkCMpirZtABibHwdbVLTOoAwhEuuqvOZD9e474cVnl0bcPLKyGNVDFWyqrsAZxfYDqy0VqVVgANDwvr1h84AvhOZyvVzjIjWYzaZ2HiOIY41UlHUpi9FBVW3BGst3rhVFKzNgDW0zMVfaTJy9Y15nRtsybnHtYA33mecIiInptoHYO3ats8/IeDGcW+b2Dx/o7EWCZm4K0Uigjl1GSY8OCPn4DsPwIjLixV3ksQGJY5DEhhUYN2NVUCOYShrX8qWuL9JHIy2rbKxZ7JB0+JJXaXB1JRyxqE5VZGYxEZDN0UJlVyuoez3QKIckGn9pinR1ArVLPa45m5htziQ4Gdjbv+ipu0IRRPGwKSiNKFKjWTdGNQinSZVDwg6tcfIvohbf28zP/nX3UeUQN3wuPDQ93QCLI0xTSAFDBVpUHNQNde3whBmz9NEkfD3f9DLAz+upDmVcgH6HLOSEtpl998kRrP5pyRYFkOA6T2GSy46PK7cZTIFVI4RGRrd1JJRya+CtQIqkiyBJrJABgr1nBmAUsCDi05/T3h1XtSWovr85mm8mbb8eD1wYrrNy74OmCRJs+q2AyuZ3cmomD533ahboBzbObULzkegMD2pJ6Ge2Ud05jKiM07G9HbmWqqMIbjlnrSFHkghD0jyrEjJat2pS5q30/YepZXbla0Sm3DQ5uqxyQYz7RerQqkonXz84Gmp2iU9cY7PxDEcoYpKIspmAY14322k2NbATXto1k5oOvUYiuOX+E53gbiQ7c28rB+8LkK1OG8MVnJVKAw1qWcxsoE9T49yy29vZt+GQx9xthM55bSIKg0qElFpAilQIUpAQx44u/fx3IvtccODig//RS//55em84kPTuOH33beV0kT2gAA07dJREFUTBak+FA0eztd39BUMFSxiQldOQVUgS4RKkDoFqAIP3XD4QtqWiQT5nOWLF7EW37+jZx+2irmzZ1DGDZXZQxc+/LXHFQDp8TKyNgWxup7qIT9TYAlv6LVQho5Nv/mj9NiT8QzrHOseQuuWeJTYIx1fZ4kkJI9SUxnOlrYv9SSa8sMzWMHabR5rMrMmRitcfZHpZIHgk1dKNUBpKDV2y8xmA1C73nEFXmGnIWnHmtgeroYe9klmJMXpDu0Qa19jspt9xR6CZnuKtHZy6znTyNC3f80evXy1DCxmYiwfwrAShNDkXNBFjEEQfZVak2w++Hss8kGO7OPyZ47TUZomSrHNwTxtbZmiBygSL8rSQOvtQk4bcsDgvZAhnV1FWxqr7LX0A9xnwcpIERGUWkZcM7a9FjViFWdFA9Fhj3PjFEfmgDleoiku9e6fyscMPUWdsmzc2LibNZp+yMUS07JgtodWwOiOvzSL+0F4zJvZ/uh4MIeaTRQFaeVT/tBnGQ6cVvG+ysx3XL6aXXa9fJDKRMCKpe/6FI+8qH3U6lUaDQa7Nq1myhq5tbG+xJOSSsx7Nx3J7OmX0JXdV6iBhJRGF1HwirGMScBhYDEDg/jPWt8XN5NuYUI8crH9+7B+1tQRyftElJQ4lkSZlIEtGqeQaMHdmNGBzo42/ElplpFv+TFMatQBuZyfHP+hsb3XHbvwcybVfIcTZNKMFtpCVgxBvYPgo4Ye+N1Nk+PL0rQp53E2PRpVP/3u4jHnEVnnETjFRfZWdItuQOBqIEhiK83DzYKb0Fmn8vTo5RHxYshCEymnH91xZJlLUyLKKvtAE8mQaGxzEvRtRSuRxJ8n57F2b24Sa4TscaaJB5HBmiYgKo0SkGKeI/dnt2qs/LNayeKiBpRUTL4zLlG9h1dxvL79xhmzDCJmsWX0ANngiEgy2qZGJhd8qJRHvhhwIN3V9Ha7rz+hkF6euyNLcsO7aTLAZk8OCdV8xSKQF+vobvbMDx8DAGV3/mt9xBFmt/9vfdxy63fHpcb1JRMXLQZY+e+O6iEM+iqzkdQjDX20NAjzJt3LWinGvGHxViK+MCix+bck72Rrlnz3YEUzVGugtxonOCY5Nw0vTX+dQiC0ZEXtC1FQP6h6drNTcLC2Pr7O72C40rMihVQreDfI5e3BzyQEoPLwi4iAoNDBF/5Jo0bXgZzZjWNekUMS64lBZvstvAH9xGduwIzvaeYtVMKs3guesUigqefB0AvmUPj+kvS9iVMooFK2GQ2ExMAHRGLrkp/eFOqzI02dw5vS9b9N77zxgJGP5aK31+bq3fsh6Pqbb0FLca/x34+GavWi1feiStz9g3pVKxrtDWGTWwl2uTrceAqEOvp4ryEnK9Py1gqMcpRSBLjqVxVB9HokQtOViQP/UBx8vKG93RiVaBnkyLSDFL877Wa4Tf+aJCx4QG++cUunl0bcN0rh+P8VKb8lYulNKZKB++D0dBoHDnmYUJAZdnJS/nq17/Jzd+6bbLbMyUdSL2xl3pjb/K7e9opthcqmoLBNUHodn0tUdt4Q54Lly40ZVkuq6I0q27JYlIrEJUa6/nFMmM8YIwmGtjB6Pan6D798vidLwBnpPfAjI0w9tSP0PsOX/beo0mMy7sV3yYtBtE5FsUDsxntR9EK7Gu3Eb3kRZili3OzYXP55qObO4C651GCdc8zeuXLWx+vNdGZyxKg0njB6fb8mUBvpjgHkde2jtjejFFrysq015ql5QOlW8ag0waPHZBElWNcNV5b/aBsibqmsC1xDfHk52pO7dmbJ3khzQ/TTm3kAEXePE114O0jIlSlkSTmA5IQ/y7nT+H5RWL3ZUPDCFVxOYXyqyBB0LQw5zsisv7xAGLXabdIqMTJGDtzMwfEBvqrdcPr3jxMhSg1vM2s7+L4NvFhhjgqRItnW8T0JGLgnvuq1OvHGFDZuXMXo6NH1gp4SlKp1eak0WbHY8Ratj8GN1po9tIoX/Il57XHtBnNjQU+JiZF8uoI37khfy0iirEdzxDtfo7owE5U72wKwVgc9K6+8VHqGx6kxat4/EsQuCVtmsyxhYNNIZzQGnbsRJ+0EIKA4K574Yf3oE9ajOnrIVp9OjStqss6StxfB4cJv/xtgr026Znp6WqNBJTC9Fq1kFGCWb6guXx+UC4AJy1ZEcdyBDpxAfWZg9bqGSuB8t2RM7XnLwhtUjdcEUGbNMutoKk0qZrKI46mIta9t8mYttlhVTx2IzLN4dPTdksmPop/DwPRBJ6KrFzSvDPJ+cUaQftt8+sRsdmAA9FEQw16en0mJns1Lmvy/uePDiNaJzs2S2JPJ1h1T15D2hKoeVJBZ9RF7oBGXG8Inh1Tenc0LV/5pGzmEcYY/zP/29vmyEMrEwIqX7vpZl7x8uuoVj/I2NhY+wOmZNKlUpnBtJ4VhOF0grCXePkEjk7OMylO2oEMb5Qy2dEsKdNypPbARzsxBW3MvChFzTeaaGAnjd3PATCy9g66z7kOqfXEx6QNNVGDkce+h95/YrIovsjOnZi5czBegIam++wNbIXPQCn08iXoU09KJ//ntxJ+/250fy+cf1rupHgzTu4kUURw14MEjz8LYWDtq4xBDgxjqpXy0VprZH/sgRCUhJTNbMpP8t6ekklBREAiD6Skf40hCcVS3EIbT8QChEJqgNxUgDGKKAYrYqwHjWNAytiYdsxH1j04/yL7TBFxpuGULylzBA5Ee0HdxIa+N/Z6Q9WK4UnbFVDMMDmjXDCJYa7dYkPCKwVGG4b3ahp7GvSfVEnYCP8ZG20Y2h2x9cGjy2B+707FY/cEnHmB9cTqxGi5SCrokii9LjWBi0Dc/Cw0tu+2Arn53qkEvvy1bu5/oFZ2yGGRCQGVD//zv3H6aav4+L99mA988CM8sfZJhoaGJ7ttU1IifdPPpa/vdK9bpr3Pql1a8Xg0zRnuq6k0gwbIAQpjaIzsIZg2MzP8OZBj8sulAkkYkpI3JnlZYkYkiaOgI6KhXTQGthPOPYXGrucwY0MMPXATlfkrCectRypdmJFB6tuepLFjPYwz9sxBy6y5qNPOhbCC2bkN88SDh78NBSJrn0SfcUbznOWPTHGsscSTyjeGdhOC37cEzMJ5jN1wHQwPpXUWipswBXlyHbJ7N9ElZxJdc77dXW+gHnoa9eh6oivOLb8QpQgeXRcfE8H+IejrTtiilD1JO7e402c2xK0yuZ0CIpogLB7sxb8fJWyJSiYSH6R57WlinayXR0WlhrvOTrxMNFJgPGmZkXxI/DSDeTNIqqooZXNwnEZaX5h4BDXfi8g4d+u8yqjo5jhQVG7kauKBwWIWOyH7nkGihL7FVe7885286DdmUe1TKG8M0ZEFLj/6yJ4OmJ3DL1/4twrL3h/R3+fGtex+g0Ja3B9yQe2aRWgA1RaDf4QFK0VDdFM8TgNbtyv+6aP9Lc55eGRCQKXRaPCpT3+Wf/j7v+T//dfHSstN5faZfJnWs5K+vtPtgJos63IccyfcdHxYrMrurLyTMMSI0wdLou7plEkBj61pyc7YQWt400ME0+cQzFqEqs6jOnMeGKitvISxjY9Q3/gQ9c2PU9/8+DguYpKl2kVwzfVQqaZqrJlzMCvPRK99GPPkQ0eubYAMDMDwAejrizf4O7NlTZwxON1nAUrST/zJWinoqkJvyYorMzcaaETQW0WfvSbL71dC9Pmnwe79yK79mFl9zXSCNqgNW1HrtiRVB/c+TXTlOfFS0TsPKQPSJCV9TmKXYRW0sxUoqiYFKa3UPda92DIQ7viUgUnLlBGirgHapGoOJ4HShIVxU7IgRGGoxIHlmtiigmOzmZj9axIioFqYQbr5mC6pl8R1ybahpuot7XrGhgy3vG8757x+OidfNo2gYlnkLQ+O8Mjn97P7mXr5wUdQdm5R/N1vdvEH/zREz7Tm/QYSNqno2Qe0T2Vmn4kkQd/GI/4tF2D/AfiN984+oka0TiYEVF7+smv5+7/5C5RSbNz0PDt27Cx0T56SyZfp/ecmTIrHjzKuuCUGcsuo0oihJv9dBFXrg0CS0NaYcpDiD1eZRW0b0seX6snnIoVh9YXa0nMhCKmvv6/D2g6BiBC85LVIQSwhAHXaOeixEcz6Jw9zw6yYWg19xQuRGKS0BKZuu6/QbgWCM32ppFK/n1YEc/LC5nrd71nTUfc+gdm5D71qSQpWGhHBw88Q3v5g5vTBvU8RrV4Os3pKq2wvgsQAQ5Q3abc7Nn7nXPj4INDk8+o03ywLRAzEKp6UYSny9GjVZsen2vYaTy2TK5m86iYJJles8sq2057Fz0Scrd+yKbYdeS8cP4Oxa6NIaxsMV4fy0nLkO2pUNwxtb1Af0vz4o3u45z/20tWvGBvU1AePQholJ7u3KZ55RHHuxUXzpRAREMaeP+m9sv1IlajksjI+7y171tguyYfdBn7uF+exf//BRYqeLJkQUHn3O9/OgYEB3v6OX+PhRx6b7DZNSYlUa/NQqviRTSi4WsYqrqBOr5xjTRKVTcrkF9LqVgVFs7rbHROzJQnznpsL3W8Nba+ruugM6hsfhujIrKRk1TmlIMW5oKqzzic6AkDFhAH65ddi+qent7FoDvWk7LngtuW3t17+e+UMqW6jrIygz1lJ9aNfgO93oefPAq1RW3Yho83PVyKN7NqHmd2TNrCTV8HEnkGxG1s6IYxDtCGs6MLgbSI6jpeSnDBj3GqQtA10Cqiy4hvrloEUJ6HSqRqnk0cFhERUlQvdH2UwrK1HCEUTeMkE00WJJNFo3fkiI4Qt2iliPXYC0rD/xkAUgzIdwaYfDFAfSgeVaNQwuP3YWiR/8T9rnHtxWSZnoUGAGE1FNFYhRHxPOumf7QGNHyDaDsfNHWJshKMGpMAEQ+gvWbyYb3zzW1Mg5TBLrTa/cCx1Q98E2D57ZM7M33hVGbGRbk1IcwC3GHRkDAsTVVK8U0kSQj0OfZg5V369mWdwjKLJIygvIkJ1aQu7hkMsatmqlvtFBAlC6J91mFqUilmxHPqnN9/DNpNV0W6J/2VPQOf9znfDaXVMrQK1KjI4QvDsZoL1WwtBCoCpBHDS7GTV3h6kGERpVGiQOMlgkStrqy7n7FqCIAtSUjVKypwoiQhUg2qceDCQOJKA2ER8jumYmPjqovJSKjbALPZCapZADKE0qASRvTZsuwNlPxUFoUBFGjbHTnx+v2+odFRK7lcCZ0ov11CRqOn5hWIIjGZ4V4NHP3vsp7/Y8lzAPXfE8WHz9yLe0E2dmkTURMdqOiBW+bW6fy59QZnYdaagkre5uUMIMFY/uvy7J9SarVu3ErRSIk7JIRFjGuXE3ngYFZ+9KIpgG88jJsB6iQTeBONmgiBb3tubrK6ybWszSnrFjYr1tdDeny6WYNEqwoWnekHgDqOUsCl5kVrXIW5Is5hTVzb3mDY4IcEenfQnocxJpLmcf4aWDTDQoSpZr1lubWQKVBOFzcglImzqmvGFmxYTQsJihBaMNKts0rcA4tfHez39v7owcFnr8ycTUp6FLJFQ6Y6NSwNpUA0aiWeJESl8vFZ9paxNRab9KbeS2uBoKmKoSDPzlKu1SS3lvisF2+4dYHTfkTdMnwz52N9M4+bPV6h7TrPGwNAB6IpGSyPvJjCuGeEA0EWxWs9JgE1j4Jt7++vOQAQdCffcVx3X9RxqmRDa+N/Pf5mrr7qC/v7pk92eKWkhY2O7itmF8dDGRUEMcsdrAULBKOVZb3nwo8k8PCfGMuoty5CdEBOW3AWXU9gc4x1enChF5dSL6VrzUggP80tW78xF3wwdgdD9PdMyfaZY818gJQVKgfKBNknLCiazUtm1D2l0BlSi85en1fkapSxqTgpIS2POuI/nMt850JB+TMYQ1R6bnsNOxgaR1Lg1f8XNnGKWmkoYzabm2g2hyt6f5qzKafkciVkqNTVGdxirGDz0ZlyotUxb7M1tFK4k7D7HtChvu0GVgCZDSNRChWVY8eIjG8tjsuWLn+jiV1/Xy1//djcf/tMufufnevjGZypt1j3Wsye7GLRZkHvQVKU5HH6AHUorZPuB3+MCkSS/TxDA577Uw9EkE7JRueVbt3H+mvP470/9Bx/914/zxNonGRgsHqi2bNl6UA2cklTGRrejdYSIaqsOKRQPpKRdPBWrapEsfC0CGx2oDdxhTWE0cud1Rrx5G5h8u8pO69vRiAA9M6mdfTWjD9zSupGTKPqpRwnOuah0vzEGGnUY2H/Y2pRIo46hqxknxDcuYU7y82XBzTYOCeT2Sb2B2bgFzlhRPCM2RSkumYxcKP3vj8Mwevo0EmtRF/4//pn8zeTqaVeh4AetK5pURYQgKAJSKSiwYKb1+ST+P3lX4jrcTU6i1Ho2NDYrctGq2XkC5dYhrS/WPzoJLJc9ymdIisK0S4mBbJwdWrQHotK6JAfMAjTdQSsbMyGsQbVPMXagPavSO1tYflFItVvY/bxm/b0N9CE2ZemaZrjilQ0ue1mD/lmGwQPwo1tDvve1Cgf2lj0J4dkn0ml45zaJVYfN5ZW11EGwsW8SW3fs/fejS4TYxOG2P/gLFdPEkLl6osgyV3//oek8+vjRxahMCKjcdstXk/DFf/NXf1pabso9efJlZGQT3d1Lm+eSFub0SdkcSAEyDIZJXFBbjHTjxUdeQ5vIytwEJiZWNeXP5U2q/ukTsONZh4kIQf9cgiVnEG06PO7KZt2TmFXnQLXWBCBdlNPovh8clrb4ovv7MX1xMMAmdJFT7/josoCJKNdCGNiyHRWGaDfAKm/CKx+fXdUZRBv88GGCjds7uTwrw2NxHJVmotD/OzHJ9zbbWJWzo3AgwgcmaQ6dDiQGU6lGLH1eBmtjUnUMiuTblUoUJzv0XyFDy6EhkYqK2gaR0whF+XjKiFP3ejuWJOWWXPj72HMIqKlG6QTty7Q5QUugokK46pe7OOsllaRbqUAY2qu55UPDPPfAoUErvdMNv/m3I8xdlNor9c+Cl/x0gxdc2+Af3tvFrm12sF2wOGLl6Q20hscfqrBnV7oyfOmrhhHnwRmLYGwixoLFngOm+bsWQtNY5MQR4mBByvCIsHNnwH0PVvnSV6fxzLrKQdyJQyMTAipf/upNTCUiPDIysP8xuqedDHjDYJv3W4AmZtgfzARMkBuCypnkcUkyBxkLKJK25CZDKRiks5WUNMc3Yfekuuxchjc/ySFfRtlWEN36RdSVr4De/uwAoTXRvXfCtucPQzuyol9yRfwtl6wxMR7IHdBu9S9F/UgIH3wCvWSu/a18/UtRxyx5kCKob99D+PDT5Y0oEPXwBvQLVkEguUnWUSjGVd9x3D030eTHOBFBtDWKTRgOD6TkZbwjpJtwstFWrUtx4i1kcDwLzUBKmjxDBFPItKRi46mUuSz7NRVfUVkyPBvCP+tY6Aees78DSa+1tdjz9M0P2LuunHl58Tu7OOPKShLOwLWta7rw6t+fxuf/YIitT07+mPCGd40yd6Fpiv8SBNA7HX7p90Z5+j644rpRZszW6buo4e47KvzHh3rp64s4/yKrRtZoG0kWoZtiV2Yd38+QWK2DNRcL4+OKREjvSyDCwIDwshvmH+TVH3qZEFB53//3J5PcjCnpVBqNAwwOPs20npUpS+2vSvMrevelIEmgO9Tk42C0W4W20MUUDTlCbPeiCiY6r1AyFLov/lgc/9UGMpxnWXVBhWDOSUTb15cXmkzRGv3dr1tWZeVZUK1itjwH2zYfnvPnxEzvA2dDpnOqNcgyKMU1kGdEsvmX4lg+O3djTIR67Fmii89OAWgGReckz9hoDQPDhI88M55LBCD4ydPoC1cgYYAxkgAHX32TVQE5cFF04Xlg0rxfAsjGPUlBSrFRbJvgcTTvz9q7WHsUcdcBaKNz8MCWqygLOgIXzI2UUUnL+St1TVdYn3A4d/eSFqp9cuDNByzpMW5q9Tm7cnArGEwLjDFjkeLMq4tUFjYPkTaGS15f5St/MblR1F98Q53Vl5UDvVoYcfqqBqevSsGaZUMssLn48jHOXrObWdMbqaE2FvOHBdmUU7ELkCDuY8L4Ar0ZY6gfBcHcOpEp151jUPbvuR9tRuJVscSrh/jj9dNk0i/IMpYMC3EdSRyWNv1WgIzJeEGdyaiKB4aSg8vFeHVnvTDyDeikLo3UCsI/HmoZG8U8dh/mgR8dMZACYObPTX8EpMET/PvW4h7aZ2bSCaSAkTNiMPNm0Pipa6m/9IXIvY9l9jdJnkkz2CXlWIPKV+9AJsLSDo0iUWpe6IOUrMtwCjDSk9P027ksF4kgBAU5bcod2qSUb3B32Ia79yfpfPuaJ/xAWXWKzb2jY2bCfq8qa5CaZkq25VVcLq1X0x3WxzEB5K8kBlHoTFvtVWuqqg6Z82WZIhBGdtX57nufY8Ote2mMumk3e39ceRWri3Y+WW64ftqLQnTk1xFRoWHz44imFhpOPV+49sbJU21cem2DG36pXtpnFBFVibMmx31fASEmVY0p6JsORrIGyyKdGEIb6tjQ+MbYxUMZm5KXKIIf3HVkc/h0KhNiVHw5f815nH76Knp7ehkYHOCJJ57kvvsfnIy2TUmpGIaHNtHTtzJZf2Tm9bizmzgDVd7rIymbBzDtVcRpuYhC12EjJOfTkMZPyTPHhQtaUxwj2muXI45K6/BOYOpHV2Kywyl6/rzknmcAZPK3w4ft2Ag8okuMFzUqrmPhHEzftGYgVDpPxxPQ/WsJ712LDE3wWc3sgZ6uFqxE7mLwo6MmVGS8uxX7EbsE52b2drYflgHxsbshUM1eQ/nJ3knZROUzFLZMmqyuyFbHoAglSsBAECci7MyGJwULvlQYoxZoIqMS+xWbO8hk2peyJW4KtaBpy4/2M/B8nYc+sYuHP7mLa/9iHjNPqZIbpeLuathwxxAjLdyTa71i1XsBqNgfqahfrH5phe4+4at/d3AJdYPQ8Jq3tarD0OWDFAwBxWpCgIgAIcqwIp0Ox+6ujCc4gwh8/stHl3dPmUwYqKxZfS5/9Rd/zNKlJwF2cnI63Q0bNvK+P/gTHnjw4clp5ZQ0yfDAs/T0nQqUkaWa0cGN1PpPTsqALagTFUCGj+1o1EqYkbgO3bTSTtdtxvWuQlDSvF1EaIwcQHX1IkplsE1zcvo2ojXRjo2dlz+OxFRCOPUUStV5mRV8i3vqdnleM0aZktFQoK8nRTVJY0ixQDL3xKvxW+8meGxdp5fVQrypsG30TtsYpw5KWIA2xwVKEwbFMT7anc+ucy3zUAnKo8O6UPwmgRNC0JTwL3dMvL1SEIY/K4bIqATMqMIMvNnyvti8QingqKkG1ThvUSARLj9RURucZtmC3jiMvzRY9dI+tt8zyO6nRjERfPfPd/DiP57LzGVVjNF2TtEGUcLOp8e45xN7W7Z4/zZjY+Sg207Yp70wZPaSOrs2TYDFc3Wcp+ltitChCdGx6svvL6YjtUwDRWAiy5x1qMZxp9CQOLl0csxf/F0/Tz979BnOFsmEgMrKFcv5+Mc+QndXFz+468f8+O572LFjJ3PnzOaSiy/kshe+gI//24d5/Zt+gWeemYyBaEry0qjvZWjgGbp7l5Mm9rZi0OhohAO776ceDdAz+6w0aFuOQUkG7DYcsF1Jk42/HM9JmezKrmwHdiTZ+g1ow/DDt9B12osIZyyMga9BRNkQ3mHYzNgXiUB9w0NHLKT+kRZ91unZhH55JiVBgMUPJ6PuyRcpe64+1dWMmrN/GxGVL30PtXlny+toJ2Z2L9ErzwdJ1+oOhHQiZWAhbaotoDCEQXZiz7xGLXF+Wl9YwHi4s9q7bl/GQJk4dHznk6hlVVoBT2sd4Wdm7sR4NpA0g3GqltClWZDLmC1nnFxTjTiYmaBDuPjX53Hzr20EA/Uhw7f+v+2c/MJpnHLVNKbNDhjaFfHsd4Z47q6htnbxT9xe50VvrVEtvc9ZueyNlYNiVXr7s88nICKMA645b3krKUhp3zetA7JVV8U2eW3K+0xXRGeT+rZtAd/69hFQjU9QJpzrp1Kp8CvvfA933HlXZt/HPv5fXP6iS/nnD3+Ad9/4dn7rvb8/KQ2dkmbZv/tedDTCtOmrUMoiY2MMo8Pb2L/7HrQepdq3CBO41On5UYT4GG9bwVhnAOOiBfkBKpRd8RROWh1Q4tlDhPrQbmiMMvLot1HTZhD0LwARogM7IKzQde6Li+uVdCVhGnXq6x+icZhck49GMUsWJc+pFI8o7GoVFx/DFkpASt74WuJ9rZ6rb1jkffXrwIB68rmDBylz+4h+4WqohU06+XZutmUiuSzGxhiMBjU8DP2VZKWarbso3ol/rjh/TRwGvbxdMVjx3Jpdqqz2k5tdvY9HHJnUTtUVFBglow1+yrEi99im2gw2L5CHn1UgdM8OWbC6m633WwNX3YB1tw+x7vayXDjlMnLAcM8XR7n8De2nNWMMMxaMg6EtkD070uMVERXRHmj1MlPTKfvmyltg48jJFOo2rwBcVpJ0S3pO/3e2fvj0/xxbwfMmBFQuvugCbvnWt5tAipM77ryLW771bS69pDwI1pRMhhgG9j3C4P7HqdTmIqJo1PcRNWzwvbA2k0r3bFu05QRD+ibltAIasr3EXyIBoqRZ/eMd3+qUxvtigPqW1BhTD+1FD+3NHDP29D1UV16YoTeNMaA19Y2PYQb3EO16/jC5JB/Fko+/nTyLHNCIvWDw7QjE+2SqaANS/JP5z74AxAb3jz85o+ntQp+zFDOrFxltYE6eA9WweQYwAmJaUOAuoFauzZABKUlpRZxZ2fXY4jqLg6HZ4Gyqbej4XFMknfxTOp+Sc1vWoxMw47/m2qiE4SkWq3Zq2iowtHWEvqWB39xkX2ltUjxp6sjQv6yWAJWDlc2PNuh0WhsZmLjaB+DpRxS7dwgzZhuqKq+iSz2+BNMGPGdFobN2LKbo3pnERj57bHJI8t0HLUqEb3+vxle/0d1ZY44SmRBQ6evrZdOm1nEhNm16nr6XXD2hRk3J+MSYiLGR5gjAYVecBK/TCcZX37ivecjedJRBjCQRZotfqhJx2EgMenSIxp7WNiWN558g2rOFcOGpqP55oCOiHRtobH3mhFXzFMqOXTC7JAFiDo+WAZOJS+yAWTQwGwP7B1E7946rxujCFehrzyWmzuzkWCnTVdoZ0SYazBjG4FBxIetRMNk7dYWZ1hX/zubgKWJW3L5QbLwVv67xiyEUZxuSB0pp3dVMOP1yMBVIlOx31yHFDyq2rCl+k9XwGGJqSaySTq+lsFUKdP3gAIMvQ/vyesbiNooId3+pcVDnWrpSM7RHM3dOMVB0rFXn75Z16867izvXY+VR32Vxcfy3IsKgvJMrEb56Uzd/98HpmNI4EUenTAiobN++g9XnndOyzHnnns327Tsm1KgpmRwxHUa4ckOgwSNWHPVfzGl7xzqVgVceWr6cmWFJsKhIN3I6qJJjh/ZRf+aetuVOZAkef4rojFVZMBL/NUUDZ6djVgflHEgptHkSIfjBgx2ezIo+Ywn6pavH1whvNev3NlHaO9qrR3RptSLOxVinv0tfhxgIYRKQkl9hZ7c1H1+kwhEEhY2dor22B2IS+5G0lUWAxqqfQhWhjSKQiFC0pxbMlvftUprFMGdVVwHjRhtVUsm1ibD1/vGrecpk53OG3Zsj5iwCXZqUzHBgt2H9AxNLcBgEEX/+b0PMnu+B2eRcLoOx/Zj4nSvK0J1vE0A1jptS1Me0pBN20W222nm/h7t8S0IUwdqnQj7wkWMPpMAE46h853u3c/FFF/Drv/ZOqtVsgJ1qtcqvvfsdXHLxhXz7u9+flEZOycSkPrjFghWHQlqI67tNIGIC4sa+lrjDAzUioKb1I7Vjw1XuaBfZsxeefMbGJUn431S1MyECxbmZtzu3OwHZbmcAhkYInt6UKW+6Kpj+aZiwuXIDRFef1dyROlFziFPbgAoMKki32RtgQIwt04LuUEpTCZ0ayZZzthopu5JeqVBuNJvci8L3ImYxcixNoGy+HCUQKqtiqEhERWlrPyLW4NLZvwSxJ046aVpVgoutEqooZmnsMel6xMKggMgDKS4+S4RCA5qq2KBkZbes1bUVhd+PGoYDz08uG7rh/gaBuBgvaRt8IPH9T0zciPavPjHEnAXpPciHfwjRsRtyDF+EDBPXLLZNXTRiNiX2EWsqbxMS5odzBVSxkWaLJIrg29/t4jf/zyzq9WMPpMAEGZV//ui/c9WVl/OOt7+NN/zM63jo4UfZtWsXs2fP5pyzz2TWrJls3PQ8//wv/z7Z7Z2ScYiORhgb2ka1Z2EpMWKI2RMlzW9AB7Rl2bvnexIVEdd+vW6VH8xcTGOrtV+Qnn7C+SuQWjfWKNSA0Zih/TS2PAtjkxtd8niT4M4f0xADp63IAAf/cbpn0uo5J7YrQbqlZafwGTWTHoExhF+9HYndGPTi2TRedBZmWRy+u95APbKB8M5HkaFR9Mweop+/0ubxaW5U0q5mkGFQYTpBpbYdtvy4bAVEZ9Q3GXYmqdeVtQatygHBEsbFN7y19TjGw1BVjWTlrbUtW+R2nDcnk9x2iduTF2OgKDuxvwZ3vEwYJxRM7OeVUxWV2cNk7XR05IChvbZKgWrNaMPGOw8UVTYxEXj1r4ecdaVKbJRCoxN44gCZ0YZzr1E8fuf4GZWLrhxjxuzyBoRE3vOXGCjacPeKfP+zz15h6CLK3B8bPt8WyarnhAiYhrVRUbmbmr+i275X48P/0s+u3eOJsHL0yYSAyt59+3jDG3+B9/72e3jFy1/KlVdcluwbHR3ji1/6Kn//D//Evn1HIFvslGSlidrNAgejaHIl9lUGyff4DSsao5LcPV79bmJzE2GymsxbeHnfJaiACNVTLyZcdCpGa9zyzbbX/guXn0f9qXuJNq3t7B6cgCLGQG9PPMCZhEkpE9/zx98GkGXQPQSSPWPcX0rsGtZuINi6G4Bo5UIaN7wwW6ASos87hbEVCwm//EP0W6+iZWx3I4V0upTGKSlrNyXbDUECUgwuTkgCEhyj4sq2DBaXbYef8UYwVIIGoUoNLo0hBkh2Wz7InH9FbjXeyXkVnQR5s6qhisqDvXTCbBgb46PoHhsMz960h8ZABAKrXtFHpUc12bMYbT2qnr5pkuYIgTf/3wpLz8zerNQjyQOZgTBz4cSYhdf+fOsAb0X3JebhPCBnki2VOMCbH6PG/Y1icBN4x4QI3QJBQec3xiSeYq6mj/3H9GMepMBBBHzbs3cvv/+Hf8Yf/elfsvyUZfT29jAwMMiz69bTaByckdKUTJ5Ups0DkaTzOr20IQYo+QEkBh0Z8OFTmz5/7Sau/DtvlxEdia8a0IN7qSw7j2DhSltN0whtta5GoLLqIszIEHrniRnUrZ0YgAVpGP1C2xTc9G1w9prpJBoXTtyUTfZ4v38Yko4hpGrEBBtEdcycHkZ/56ftxlhl0TSiKwV93US/cE0HOn3SpIHeZNRKLZGUyuTfSSeNTMmkHpOwE83NtQn/jBG0ca68ktyOdq7IYFUxQa5+/7iI4ozFfl3Fbs/Ovdga46pYPdSOIQWTM871T5VCI01UElRN2PnQELsesXYn2+8Z4LL3LaB7dhreXgSiuuHuf9zOgU3t1T6Vbph7SgAGdqyLKAo4fe7VipPO6Ax8GG0YniA+ysdO8cV2aZPbki1h0FRNRKgsEA3JGsZqY5qSEgTxeF0BulpcYuI+7wzOgYHB4yNLzkGH0G80Gjz51Pgynk7JYZQM6MitLYo6vWCTFPplvAkpcVO1/HLhBGjKbNj8Mv73uFx0YDvVcy4vtRlIhvdYrVQ564U0tj1LMO8kCALMwD6ijU+it27IneEEFKUgtK+3A6eJeORCqngwNjR+LMazaUkoMU+dVwhaTO5cxkAAUg3AuclnaOyCNinTEUjJNiImylvk6UnFXUgWTTmjyLTvZQFMkUcQ2Mk/Mhaw+CtfaAVWLIioBnVC1RrQWNDT+rqKQEpFojhwnBeyvQ2rBtZbqRNmSKMImhQNhvqgZvdjqXHsgefrfOs3NrLokh7mn9uNBMKeZ0Z57vYB6oOtVS9hFV745i7OekmVsGYbVR8xPHzLGHd9ZgTtrYcvuSFoaWuUEYGHvzuxEAYjw9BdGCfNZNQ+znYphe/WVmhaHFJfxaqbtGzSNEKByNheXcnUEJdpcZ023497JYXRsYkxR0ebjAuo3Pgrv0h3dzf/9JF/LWVNKpWQX33XOxgYHORj//6fk9HGKTkIaYztJ+zOuar6fdcbszOTTEn/FhV7L4jECQ0L6mtxvC8+gDImIpg5Hwlad0m3YjcAlQrhSavSkXrGXCoz5xHNX0rjoTuyI8CJJlpDFGECVfxMve8JM1EKEGIU0aZvNC8gbbbh+CztyzMOkOKATdKm8vR/TQfWGzYGi5PhOvo7a+G+DbBsNgSCCgTesqbtpG3jHqbGpzHsSQxck0KSNlgw1ILWRql+ezVSEk69yACX2PjWArLyzM7gnklFrJomMjbGTGd2PEXeT0KgTNNz1Q3Y9INBNv1gsF2liagQXv0HPSw8I0B5C6dKl7D6+iqzlii+/tdDNrePwJwlHU7IxjAyYHjk+xPz+LnjmyHX/1w9BxYMNWlkmK/EtgenViUFKWII/bVgwfdAbFZkf81ogU3r6xSR5L0QoFY1jI4e+2Cl47XLpS+4mPf86o3s3buvpWqnXm+wZ+9efvM97+KSiy+clEZOyUGICosn7LJJx7fIk9z+ZNCL15mtek8n44CX4lVUAGGHmTzdyt4tX5KmxyuXeScRnHxGZ3UdpyIAW7anP8rGKsesdDKWtarHP69LfTA6TAag+McXgpTxMCL+hGgrbu1ZEYs28MFvw0e/B//zE/ivu+BvboEfPQtjETy5HR7fhn50K4xYtUQ7NsN524DE3jSGIGFKXDstqKioRgxSypPTFV5yycawKUidp+5xbSxqd6zSUrEazq7ki20sOhdDrUcx99yDD81+6mUVFp8VZkCKE6WEZRdUOOXC0J228xiPAr19ES//xYlcpKGrK2XZUruR1J27CHQIJCkHRCyT0n4NZQv45Ry7ZtodHD/PffsUA4PHPkiBcQCV1776lezff4D/95n/aVv205/5X/bt28/rXnv9QTVuSg5OJKgS1qY3j7RuJSpkXzl/EmnxLhiDhfdF6p1OmBSKJ0Y9sLv9wVBskJuTYOnpnTXmeJZ9+9s+y05vkXtmHkxoqjZRqmzegezeDT1dlDYgX0FbEGRsHJTAoEILaqxhjW99ZRtYOpBrDQ9vQvaPwOZ98MhmeHo7RAWo2tAhu+NTiII2cfA4NCLW2iAQTS1oUAsjAqU7ZFKaz+FsX9wZK0pbBsMTO2GWgxQHlqqBNZitqNTAViTOa9wW8MVANFe586iZNvfgE92dfW0V3SLRjY4MZ74kDY3x3CO+ZUeZmJifEi64TjildSiwJrn6VWO87KfHYs+utN+FJXmPIH3Ooect1SmT5n8TXDoF6UjFpSPhi1+bhi4MG37sScdAZc2a8/jhXT+mXm9v/FSv1/nhj37M+WtWH0zbpuRgJTfSJgDBTywojD+Gih8Mrui07nwl81O+fmM00d4tmIE9RPu2W2+fAknmtqKYHpmVjCBd06Dr2AoTPZliwgBzxvLOCqeuEcV1ibGWfIl9kkk+Rgypp0+8tJ3dA/NmeOoO2jI6LbMXi0GCeOWfGexdHJT0/OYHTyGPxFGzHQBxf9fvgq89UH4eX7orqK6wowklLWKDqoXKujWHylAJbP6gvEa0s+nD9vjAheDH3nOFpqKiQhdkV3fxHkNFRaUkqv0uNOIBoRVYaTbwtQyRKKE+cPApLKbPV4VsSnL+QOifnw4Cd3/VnbNYRZaAq3hL1DBc+NLOJ/EgMFz/s6PJ75jDQ2iXw8mVm4AIibu78+Zpyag4djQSnl4X8un/OXaSDraTjm1U5s2dy8Y2YfN92bRpMy+++qoJNGlKJktEQqsfLVPV+G9PbHresVVHEZuSqzpLkHoAxjNzN7E17tjGRwEYe+Iuuta8FBNWM14/rg4dUGzQWdjGE9dGRa9YCkHQnh1zX0rupcF44TDz02wKQqwRn9iytXECxJbGojF7QvNkkDVEFOSL9xA8stG26u51sGYpzJgGAyPw4EZ4ejut8FCm7monLp22siCO8loJdGyv0nwxBj9abtr+dhKKTtyTnc2DStRM6RvmmBRLKRXVbUFKJ+c1BGjTaEoZ5dvYZIFK6iIdjWq23d+5LUqZDO8z9MwwpWH6tTYM70/b8Mx9hh0bNHOXKtKH7B/rWCbrpq1CYe5JnY8Py1ZFTJ+ZLe8vyFreU4+JlE7Kx+1160n3cfxhIK4v+as9W2hwUPjSV6fxyf/uYXjk+PD4gXEAFW00lbBz29tKGKI7DOE+JZMvEnbRv/Jl6XqvZCmXzGMGEh/VdgNZp4M9WVMVp7Kxwdsc/Iexp3+E3mdzFZnhA4zc+w3CpWcRLliBBBZsIXHywzKVk3dtRhtMfRRGT9ygcGZGv1V1+GCl4J6lOCae9VWyIV3KUXxsBgXF7IoEfv/pfCIoHbzbrVbdM39+N8EjG9OmbthlPxMUMzCKGW0gtVZjniQJAa1dStn12vvkT1YdtADBZGxQnGcScT1aQ6Baqx5cXY5J6dgxxmfDPFFEuTwzTnVkAczaL++hMXzwC4QnvjfGi97a1aJ9towv3/zniLf8Jantm8eixMuiVI2iDSPjwFO1WgGDFN8fDaiW4MM3iM7nwirqETEA9n6p+HKS1zEXMiKK4Dd/byYPPVIlio4PdY8vHSOP7dt3cOqpKzqu+NRTV7B92/YJNWpKDl66Z5+OqEpHXHMGrEwCCE/WeW7Och5CcRuinRtBR+ihvdS3PUM+MIIZHaL+1E+oP3UPhBWqq69Gps+2qqy2LEpMX294/OAv5BgWaTRynH5xueY1olPrxAfkvbqaz0SqEsqX9Sa7ljO0BUmGItakMy8UWbu5dYHxSmSo372JyotOLmBIEmjnefwIjch6vZS11Xj3w71vrSa3sCQ3jsGyONZ7pIAlkeztVrlgde0lzSrks2dVGlRVlPAnrl6Ahlas/eJunv7Knk5P0lIe+84Y576iSt8cZT2wPNGRYe8WzZN3ZM0QNq01fPYvIl73OwFdSTaOLLvi24o8fHsngMpwzoUNrn3NSNwPbaLIvNFxkuSxgMlKwBH2dYqwE29RPionVVKvsTj8DEWWP+6cf/3+fu5/sENnhGNQOp6W7r33fl5wyUUsXrSwbdnFixbygksu4if33n9QjZuSiUtt1spkgDVQMIkUiMl9SiRJTFtSBfH5MgoCN18Zg4nqjD75A+qbHm0CKU21NcYYu//bRM8/1bFpvxkbQa97tKOyx6vIsxvLQ5r65dx/7iElBoy+7UmHUli8Q/4gKp5IO02gJvsmmT0LFdVz55VOJIEYQpUNC29Q6NL2pisGg0coFt6zODdOCz2VogSkxGfwNwUFgKdc8i7PdgZWaGqBjbESKMv0OK8hhSaURhLkbTJkbAi+8AeDbH3SZnwOTIOajNElY6jROo9+c7hwOHj2fsM/vKXB1z/cIKrruO02AmxVImtIzBhqrG5zJjUxJanMX9zgL/5lP7/1ZwOcfX4jBilRS88oY2y+pCoNKkQEaLqpU4mfgctf7XL21CAJ+qbi7zVSkJLE1YzH5E3PZ7mFTc8H/P6fzuDm245ve7yOgcqn//tzhGHIhz7wt8ycMaO03Iz+fj74gb8hCAL++7Ofn4w2TskERAXZZJEdzTlCGqOkxQpci1XjlOKaGBQVnVJECPrmdNAYT6IGjbU/of7k3e3LCkRb1o2v/uNQ1J59yLqNMVORYzzc8xGDUcby1oGGwNiR0tkBpex5CzlImt8Ygpt+TPXvvmDjm2ROaCfJtkyA1vBo5/ZznUjt2hWo/i6PULKTVEVFibdN0epZGykFH6nEZpjeJt9IUoCqKgu8Zr2IqkG5KkdwcMiWHx+bQszkZA+oxFOsA24KTehyAilDNTBc8yfzmX9OubpmvDK423DrB4fQ+8aoKpssUQl0dRte+o4qP/OHVYICmkFH8MBtho//dsTAdk0oBtERPWqEblWnqiK6axE//a4Gf/yJMU4+rRnInbmmzp9/dIBFJ6UpGQI0YYkq0u6P6JGILtGEGKrY7658BUOISZgVp0ILBarxp6z+QMBo4d4HK7z+5+fwnvfO5BdunM0b3zaH2++cvHt+tErHQOWxx5/gvz71Gc4883Ru+urneM+v3sglF1/IyUtP4uSlJyXZlG/66uc468wz+M9PfobHHn/iULZ9SlqI0c2xblrZluQ1RH4uk8zUIaBDssHekonP+52v2195dhz0ICf1zjKemn07J1b/cSbht3+Yuih7YogBSkAMSkpUNx2oDTPSMmBciezYR/jIc4gBdc8zzfYt0rrfAvDwJlSRi/FERaB22UmxK2hsf0IaJ6OVuqbVteZN1QPRhEonyQyth0fUAqTYWiolTIoTp1xSMbjwGZxyMYDNzJzWK4l6y7JHdoeiOLeRCFz+e3PpmXeQuWUEFp2uOPXSgJ/5oyrd08m45TpV0NJzFFe8qdx6Yftz8MEbDZ9/f0QXY2mXkth0S6C7B2788zr9s9Mb1Nuvec+fDCb5lnyGS5vieykYujzmSnLo0LdPspmO6ahvizhQY+saHRU2bw2574EaTz9TofOX89iWcUWm/eu//QCjo2P80tvewo2/8ovc+Cu/mNkvIkSR5l8/9gn+8UP/PKkNnZLxiY5GUcq+jcabQMpNt0hYEAOJUaU/GEWCzQ/kqXXa2T40maAZQ7R707iuxYneuwOjdUEOoGz9eveUbRSANCKCm75L4+dekzwnI6bE7iTR/2RsHBK1nWS+ZL9nwu2PT4J7nky+q+88gr7gFOiqNIf+yev/3Qi/YSfqy/eO/8QtRHqqSHcFyyCYDNDuxF6m+R1LmRL/t1KxS2/MeCWTmfh3P3u/Q2kUZHTO3JgE+NgMBp0xXoJJsiX7dSXsjAcgy3IPiUU1rHnrTO78u4ktFlZeEnDl22r0z1MkuYpKyiolrHl5yJ2fbVAfLS5jDMxfGBGExZpQFUC1Bpe+LOLmT4eIGH7nLwcJgjQeTfZJNBvDKgwhOrbbKWpF+gz9haCITTwYlPQpp/ZxUWzDEO784fHPnhTJuHP9fOCDH+HzX/wKP3XD9axZfR5z5tgcHjt37uK++x/ki1/+Ghs3TmwimpLJE6lMs6+F77Xh9OJ+ufivUWBEEiDi3kw/xoppmZekWawKSZoWmo2tT43zamIZG0FvWYdadApSmD1Uo7dsgLET19vHFwNEl1+YgE6DKWE9EhSTgI6mx5wBK/7GWIJWfaPMoMkQrNuWaYX6l1vRv/pSTBgkRqoJR+AGdK1h7zB89T6CDZPPnpmGTtvjM4EdgBS3+s4CjDQQmz/dhTlbErsIdzYipsnepabqhCrjRxe/Wjq5S4EXJ0W8d1dK22/bF1AGUtyxCq2jHEhqFhFYsLqboCJE9fGpBU99QcCr3ttl1X3umgypUT7N/bJSE37pH0L+588b7Nmatn3ZmXD2CwyVGpxzcev8RSqA1S/S3PxpOPuCOicta8S2N9515c5ugNBEXoJvIYI451OxWrBokdgga/MnXrcRsRO0IDQieOqpCvc/mFPpnyAyoaSEGzdu4h8/9NHJbsuUTKb44MCbh9yI5sZMHe9PQArZY8CbZoretJyk7IykU0yyGjVEuzZiDgJINB7/CZVpfcjMeRijEVHp3327aDz24wnXfbyJWTwfc8pi+90HC60AhQaCXPnMgYW8N81p6r06C09lUI9uQAayxtTBgRHkH25Cv/ZCWLkAG8xDYO8g8r3HUY9ugqh8lT0pMtKgsWEP1WX9mc3JxF8KWKx6wDde9dkYsInpnKeQjTzTPKkZbMbkUHl2K+J7GNlSbkJMUwkZgsJ2CRpTgFFTUJoHKXZ7eu6GUdTKsirnRAVCpVcR7elcxSsKrv6lmlWxxA31gWrR4sq1ce5CzY0fUNz2X5q1PzH83Hs1p5wFUcPem1qlPcB0RrWv/fmRJMha/lzZFpiCHEyWbYkQggLbquY3wQ6oY5jENMyxKAKYhs2eHYbw9DMVfu+PZpa26HiXg86ePCVHpzSGdhD2zi1+Q1VBYLcWb2Z2bdha8uAkL2PPPdhBLS0kalD/ya2oeSehlqy0EWhHhmg8/wx623OdKONPGGmctSKdxXKsVrHYNPTxV39zVgeURB6zDE3zJNeiE7mfG7YTfuu+wlao0Qbqf36ECQMbsK0Rwd6hwzpENx7bQe2U/sJ9ZayEAFVVRzV50ZvYY8QkKhlXj7utvl0IWDYlyAGThlGEpNl3E81b/NdmSy6blGOHY5PN51N2Ty08FHcUjVHDk5/fzeq3zCw5IhUdGeoD47MZWnJWQO9sSdRK2WtIR6C0dxlqUk/sdarT4NXvBG6EUAFolzwca7hcblQcNWDT0/a+n3RyFCeabN9mnTgbZ9tq4pbmqyjGtracBh55tMIf/eEsKhV4+XXDLFvaYGRUuP3OLu57oFpWwwkhU0DlOJWRHU/Q2zeveYfHqiS/DXG02IKy/txksoflpckexf2NU5yOPf0jzNC+cV9L84kMettzFphMSamYBXPSkdkP5NZKitITZGu1Dz8wnbEoTbsMwTd+QvDIhrbDrjQi2HmgfZsPgeht44uuGoimqhoWpCRgxAaBU2IyEXELV9o5sNLMaAlKnAdKiXquI4lZnOzZ7eQaqy3SSVrjgsxt/O4+nvnmfuasqnLypeWh2Y0xPPeDoXGrfXpnSymQ8NvuwEpXDFKaygtEKMTEBr/JVUqirkESrgiAIIQf3BQye56mUs1U1bIt5e77lm3J2vII/vIwiUen4dZbu/nsf/exdWs6HX/2c70tz36iyRRQOU5FN6x6xTfLK1C6FkseyJClvZHmdXPmPHFhu1LQNLavo7F5LXqws6SDUzJJ0lUlRZqQKP1LHr5Bl4MUv08oE6eRKulELfqWfO9hwkc2tG36kZZo20BH5VQ0RhgKokhAClgwEUhuYiqUdPJt/UrqNt5AndvQ5Gt2rIngMwkx+6MNA8+P8fQXbZTfu/9pJzNPXkjfwiKDZ0NjxPDYl8a/GBnaE/MQLa/BwYu8d1L2esAQERCYRk73ZohQNugblqhTgXD7VxVPPijMW1hkf1XWmHKj4uzLYp+sDw7da7htS8Bf/Oks1q8/+CSOx7scP8kApiQjonIYNI29XFA4/pt9t4rrbbFQciAlNcA11Nc/wNhTd6FHB6zV2pQcFjGBgornvphR7ZU8xLbhX71P4c4WdRsDo3Uq907QkPowizkwRuP5/ZiSDL5GG6JdQ+z75wcQlbIdThw74LRubUxQ87U3bal0YB/ijirXfuZyAhEbrcbMQ0BsE2PsvrGBiGe/tpsf/9lGGkP2+oyGW357C9seGmo6kWjD09/cx8CW5tAI7eS5h6OS2I+GgIia1Jmm7KdL2iXGlYRBad6ukofRGDV8+v0hX/o3a7K6a4diYL99WulSq/wc5R5VuftSYB/UaMDv/OacKZDSoUwxKsep6NEDGdwBFE5EiT2JAycdzFXumGY7l1TproFoz/OYUNF12U8hcaK6aPcWGhseRacm+lNyKKRID5dBmT4ajb93nLGvxTkN2cQnbokcaSqf/g4ymfFOyiRUqLMXILOnwXCd6OGtcKDEfzUn0lOh5zWnUjvfGvJalUg2AZyJNNQ1A//xINHmAQ7csp4ZLzspW08u9P/41DTNwCdrSFtWn6CNoArTDqQGsm4Kd6Alm7tHeOoLO9h02x7qg7oQYc1eUeGks0OQOi5Du2BQFVjz073UD0Q88c3WUWrnnCxc+NoqsxYphg8Ynr0n4oGb61xyQ3birkhERXQGE3V+L4shojOC7Z4GG59KB72oIXz3G1Ve87PDHs+VP2t671oxKgGRh+tNkrdHA42G8Kd/NIu9e6cWbp3KcQFUXnDJRbz6VS/n/PNXs2D+fHbu3MmPfnwPH/ynj7JjZ7P74prV5/Le3/51zjzjdAYGB/jmzbfxgQ9+mKGh48etVdeHEm+YMuWvEegwQnmzlKy+jYCJ6oytvw+16BTC+efZ7fF+NWsB1VkLiHZtprH2bsx4MoNNScciWiObt2MWzrGTiUs46MbuxDg2Fj/o24RPKlCvw/5B6O+FMIB6A7V2E8F3HkSNtlsJH7yo8xZSed3ZSFfFAgoRwuvPJPrheho3PQElDAmATAuZ8RsXoWZ1IS51cILn7BcTacbu38rwt9ahd9jJeOCmdfRfswipZSeebHj9Vrc2rybQJbFUkpqTSbSZi1Foo3Ph91M35lAiG78Fl640e56dDxxg3ZdbJ3O84M19saqrGAic/6Y+nvneMPWC5IRdvfCa93Wz+IzA82gSlq0JGBsxDO4z9PTbK1PoJPR8ZriRzlRc7XLBGwMrztJs25TyHXPnekkXjaspKxU0KnHnLnoC1oMnxFDBqgQdcKzX4RP/0ccD9x2/eXkOhRwXQOW9v/Ue+vunc/O3bmP9ho2ctGQxb37T67nqqhfx2p96Ezt3pi/e6aev4j8//lGeeXY9f/23/8CCBfP4xV/4eZadfBJvv/E9R/AqJlfC3nlpYDRpTgtu8EDKwUxOniRa3UoVNXsRqn9e84gS/w5mL0Jd+hrqD38fvXNyw59PiZXg/sdpLL4yZjm8Ha0Uvi1V825iaXFwtQJz+0FAPb+T8HM/QEaKAYrprcH5yzCLZ4I2yFPb4OGNSH1ikYvVaXOpvHF18jsBG0Bw2TIAGl8rT1bZfc0y1OxuxHP5SFQ3xvKE+/7uR4WGtkP3b6fv0mweNDc55W9/y2uIg7VByn5UVMNbvRfBi4yFGEAywfvXUZFGYjdjMvXFsVuImH5SpaWuqmdOwPwzW0+yQVVYenEXz3w/u/BTIfzMn3Uz5+SYhRH/PgvVLtAVcF5loUSFNiuGNE1BqZs4uXxFfjtKLm7JyQ0uf0ka/dqPaeLQvcJQUTqJ2p1nJgNsfJyKGKreQ3d1hSG868b97Nmt+O73yo2SpyQrxwVQ+au//Qfuve+BBKED3HHnD/n0J/+dN7/p9ZmYL7/16+9m//4D/Pwv/AqDg3bA2fT8Fv7vn/0hl73wBfzghz867O0/FKK6Z9hXyFm4Sy4MlR8IzpcWE1WqQirf517eYPZJBaoh/7v9UTnnCkbv+irjyrk+JR1JsH4z5gf3E122OjtTlizvBQto0whUkjuINPd8qSR0DXrRLBo3XErlv29vKqVXL4Xr12SMOMz/395Zx8tR3Y37OWdmd68l90Zu3AMhCVLcrUBxp8pLBUqhQKk7bX993xo1CrRQKC2UUqDFPbi7BQgQd3e5vrsz5/fHGd2dlYTIzc158rm5e2fOnDkjO+c7Xx0/GI6aCLe8iFi+oapjjGIfN04fR8LsJYTAOngU+WfnQEeO1B4DsYY3gqvIT19FftZqag4aGhNS4tvrMdYePJS2e2cUrW95YhENBw4OJiRXCWwZfgPKyX9KQW55O+kM2P1rgpbSzWHb2owkhB/wGtpo41oVvdYWrs7mWsrHrMDaF2oGdE91zSmaxtawblZysdDaPpVFLuVAbd/idjsfYNM8qvQNJITwLEm68F/p4n8CB4mFW9LEZeN49YicWIE/BxFoq4SAWR/IYLvPnd+eKBjFo7FCAdQXYPGcc1PeuZSE2WSLj1Fvc8EFG3ju+VpcdzO9JfZweoSg8mZCleY335rM2nXrGDNmdLCsvr6egw86kJtvuTUQUgDuf+AhfvyDb3PCcZ/oMYIKXq2fwAfFo5ywEaNgngoEkbhBO75JUPtHxOv+lJoYhXYzs0dOJD/9jQoDMmwK9uRpqNo07n4Tq2ovENqBNGk+sghDkpPeuoWLiGSoVUqgRjXjDO6DtXStTio4oi/q4xNgZP+4QOF/rEuhvnAoXP04oqt6p0zRtw45tLFCK0XNJyeSntAfYUnPcRQyR47CWd6KqCvv2CiAzH6DEgWV/PIOsotaSA9r0Iciork74qGsBbICACvvmEfHtHXU7dybVP8MTluehmFphpw5FNCJ3FylIjlowy+XQFFjVdZCxU63iFsAo+tSDaWFkY51lX2MhAUda4vbTTjSLtLs+vgVmKXlOfaKuABViF/JSBJN/uens3eSfVsEWN6CfF6xYKbkmBM66NNPMXBInjE7OyUdkbWfiX9ModgZTUrnj9PyrrbjW1sTBJ/m/i4TJ2R5/wNjAqqGHiGoJFFXV0t9XR1r164Llu0ybidSKZv334+rf3O5PFOnzWDChF228ii3HLn1S6j1vzwxFWZJrW5FlCQxE1LQX5CqnapDoYUQWEN3xlkwFdVRXUioYeOQazewaS6sod+KEIBQBVqxaDsdslw8GSrcsw7E7exEDOgFQWr8EnehlFCXhj2Gwxtzqx6pHN8c7NPXLgARFT2kLIW124CYb4TfUDbXkXcEtlVaG4EAWZciNbaJ3Ox1RatX3ziNgd/bE6tOaw1c5RX1I5ycvVrWntuLQrmKjrdXMPTkgcizhtA+v43Vz62kY0E7tX36YeHiJ8WXAoQnrBT6qFQOTS52/izVvGNVaQGxbaXD8mlZmndOBcUBC3GysOC1Yo1MXW+RIKQoT6iImnlcLwIJVJmD0jFLFkI5CKFIkdeROEJ5FaCThQSltNbnYxPbUbto4dtPDpeUzC+Dg+05KAd9gFfdx99AXxM/ZaL/+HOJ+5ZHaWzaCo7lPYQeG578xc+fTTqdZtKjTwTLmpv7A7Bi5cqi9itXrmLAgOaS/aVSKerr6yM/3du+qPKdZFfNoihLW4HQUr4T/ctFCymqjJASpN+P7qNKraYQktSeR1bX2LDRyEUrKPmqWIAKhJNwEg+ElIJq2fEfkfgwFkJAYx0MatLOtd57fNJbdWwc4wZVNV69E0gdPw5fuxC9TYXwJngRTc2eYBrykqCUP00CXJf0hH6Ja/MrO1h/z2xSQjuB2ri6hk7gA6I8wUWfsq75LWSyHTQf1If6nXtRN6qefoc2s8vPdmXQqUNom9Pu+ZbEJ0nLy3JrC7+KscCp4BVfnHdEYQuHjMyTkXlSIo9UDhvmdtK6qHyV8rdu2YBS4JZwTJ78n5ZER9p1y92icG/Ly4niHxtorZ4f2u2PtRhfUAsfbjlsBC4Zka/gSKvIZDzNja0rKSdfd0WNZ4byxxc9h7JgbKUeraXEkeXLTdRPtXQ7jYoQglSqutjybDb5C7XvPntxyUUX8Mikx3n1tdCkUJPRarZsrti5r6urK1ifxIVfOZdLL7mwqnF1FzoWvYXdZxgyHReqomrfai1AqmAZkW2DQob+35tgdhX1jVDfCG3rSzeSEjFkJGL4WESmBtXRhlowC7VsUdUT8Y6IaGlHzF6MGjs09qTVb4Dxv/UGwZYEd4qgzGu7fk0tVwPHvymEiJiPSg5YeDV+qkPu0oyoScUmp1jEjcI7Tpdyd7xSfkbRpHYRdWSCJsHun2HI1yaSHljnmTdKaziUUuAqevUTpOrtmGZHeH0POnUoHYs76FzeSd2g8hVzlfLry7gFQpjnryGcWP0hUGRkPnyf8C6zLXXETaZR0rW+9Nv+qpk5nvjFGg66oJHGoeEU0tXq8s5/Wpj+eHJo8vtP5hl/aPTZrgo0KXF8bZRbJAJ4GqaI5gxgyRwY1t+lpqn8e1jgtxcZR3z/oQnHKnGvRo09vvdQufezwmNcssRi1iyTQ6Vaup2gst++e3PLP/9WVdsTTj6LOXPnxZaNGT2Kv1z9B2bOmsVPfvaL2LrOLp1LIZ0gCGUymWB9EtffcBM33Xxr8Hd9fR0vPPNoVePcZiiX3Jp5pAeOp6jasPctK+Eyqb+AIrLM/xz5hrrRN+ooScvKIfTDwx62M/npbya3SaWxDjoG0dg3tHM3NCIHDMVduRT39WfB3bRokR2B1BOvkR18ItTX6gdrtCQtESElqZQC/rJyF1X4EkGJjX1NSqCDK92f68KSdWX2Fcc+fDTFk01k78KXY8vflEIIlOtGHIajb8s6FT4pgrDkYF1KMvQbu2H3SQf9VKJz9np67Vpfcr1yFQOPH8SMq2fzsV9NKNunEII1b6/Dyjs0798YcwhOiRxWgdCU9iosF5rpAGr6WOz1lf68+ocVZce/YmqW+7+1kv47p+g10KKrVbHs/S7fNS6RBe85TH8px7iD7eB4ZIIgUOiwKuMGM3znVV/c8P1sJv1DcPyZgt77VacuVl6a/ajA40Kwr2Rn3ZCo+ORXwlaU9zf336d+/8dGNu4huWPT7QSVOXPn8cPLfl5V2xUr4zlSBg0ayD9uuIbWllYu+Oo3aGuPP1BWeu0HNBebeJqb+7NiRbFJyCeXy5FL0MR0d7IrZ5EeOKFsm6IpQ0RMOtHXLk8AUYXfxEKVy8YQFWrs0hotuedB0LuPNxQR/908CLnXIbhvFUeXGDQim8P+z2Pkzj8VlOfjICMCCiQ8NyPalIpUvgHimpRSneq7Ubw5F1IWYo8hiKGNkFeoGctRs1cV7UrUawGh7KQSMyOU3reQwss/5I9SkbKcWGr5fp8bC6cMB6VwW3PklrVj9ctUJaBobQrIlk6UUxsLoY6NVwrqRjeQXZ1nzVvr6btPY8n+laOYe/NichvyzL5tGX12q6fPxAbSTZJMvaTfTuH3SlAYuhtHWoJBe9dR298u66vis2pmjlUzq38uTrqyi9Y1in1OKadNEF4Rb+9ewL9ihXpdrSVTjuKuKwVzpwBn4HmLlI/BFygs6XrOrqFGxE/kFokLqIiNdnYOUhWV2SaXg5//b1+mTCmvJTPE6XaCyqpVq7n3vgc3erumxkZu/Ns1pFMpzj7vq4mJ3mbMnE0ul2e33SYw6bHQdyWVspkwflzMn6Wn4Ha10Ln4HWqH7RUmgINIGGrBIyBB6FCR73zRdJSo5yyxvKCJ/3TQbjQKOkoUoKutRwwcFj6oo3pX0NqBIcMRK3dCLZhVfsc7MGrk4ASNSZKAEWlQ9RPCu5CVWgXajahRsUC1//w05B6DkZ8YH3PeloeOQS3dgHPza7AhdNZU2eo0aX5F4nLHYEnHu88UKEXajptMpOfronqn9bE0pkkNrcdVenrVk17MCBVsK5TClorc8jbS/VJVmV6RMPfmhTSMriPdJxXTlviaxfn/1UIKQHZ9nuUvrWf5S+uDdiM+0ciEzw8AofO0VJp8hRD0GZuuSlDZWFwHnrspy4R9FY2DS+dDUUWfigctcUAorrxU4KdiWrtSolxHe7AmbqeXp3G0cBFpISJCStKYivFDwsP2pb4uCnjttRQ/+Wn/xGMxlKdHONPW1tbwt+uuZuDAZi646OvMX7AwsV1rayuvvPoap558IvV1od/GaaecRH19PY8+/uTWGvJWJbvsQ9pmPY/THikK6LuiR803BUZWhe9/ElkQ/Y4Vfq7i++f7xvhZcZUA13sNcZbNS9xG9B8UF1IS9i0QyI8dALWl1ek7PPU1BZlZtbZEyPBHn1ffDKQqW3z8fkpqVOJambjWQ8XbeJ/tvQcjj5sAUkeJ+D8AYlAvrK8dFviJpM/+GNbIpkoD9HpO2l84Dj2JhzeYn+7fXyQj1XiD29EfmwAXqbOSSi2Q6Oq9OjYlY+WpTTmkpEvd0Brqd2oEKUsmylWuonNpB26nS77V4f3/m8HKl9bg5kLBqXNZFzP/Oo9lTxS/lEVZ8MR6nvvWXGbft4b1c6orJaC2sBX11fscLKnTGCQJBDq7q+P5C0FgOsRF4mDjYAlFCod0OjwnLz9hY1mFSd30thKHFA415MjIPK4nljhY5D0Dk//uVo0mxR9nfD/FuC788Y+N/OSnzRghZdPodhqVTeEPv/0VH9tjN+66+z7GjhnN2EjulLb2Dp56+tng7z9ddS3/ufVGbrn5Bu648x4GDRrAuV88hxdeeoUXXnxlG4x+65Bft5D8uoVgpRFCUr/vmeW/MoVP5FLrk1YpEov0KihONCcApVD5LKq9RJKv2ogzcKnXUO8lWI7cCXfau6UHtyPT1hnRUHhCSlI775qIUv4qMfxX4tLJubRJBZQSSO9Nt+REoED0q4/kpyjsTiAaarB/cDTW8nXYXliyP+GXc+gN1fkKt8Dj2xJuMLbgZrItlBf66kfslJ7A/MSKsaFiCT8NvGfGiByX79LjkhD1L2DlE8uDP3Mb8sy5cSHzb19CpjmN2+XSubxY6GjaqYZhR/amrjlFdoPDkpdbWPFOG52r88y6ezULnpCccM0IZKmMZICbV6yenpzwbXPxzhMuA0Y57HuSxFVepeaCayc9odmitNSUwuGHf84z+UXJrVem6WwXLF0gGDzcCSY3hSAj8kGIsZ/wLcxz4+2PpOtbXjMjI3/7qfMD0Urp4oPf+lZ/Zs5MV3NaDCXoEYLK+PE6I+UnzzqdT551emzdosVLYoLKh1Once75F/Pdb1/Kj37wbdra2rnrnvu54k9/2Yoj3oY4Wc/5rIxBP0JV6ukoIpjnYhsGjrdJOjwhIJ1B9OqDallbPIbWiABTTkASAtWn/8aMdofCmrmI/NF7Q8pK1kxBEAGycX4pSenKQxuiiPg0lb3tAmfcSjtX0JCB3gNwXYWU0egPUbQPPwrHkl7mUKGKnDiTg5mEjqahULiK5ySJVpXxiwL6yGDb4h0I3w7mqkBDpByFsARrXl7F6heKfeacDof2BQk1yQTs/pUBDD+iEddRSEvgOoohB/dizfQO3vz9EvIdLtkNLgueb2XkkQ2JWXiVq5j/bAvZli2f4+PxG/JMfUmw9/EWY/cU1DT451iRR5Au+4jyE8Pp8/uxg1z6D+hg9JgcKZsgshG0COkigvsgen/5VshyUVqSwpwpfrXpcLwQTqYSnezt3XfTXH99I3PmmOiej0qPEFSOPvaUjWr/1tvv8LlzvryFRrN94Ha1IGt6l3bQK/gsyvxdEkWYobbC27lSCjl4NE6CoMLKpSUzWhb2gWuSKCWhpMCdOBKRzaFSVnkZNaJ0qeZCBz4dhXeNZ1aKdhxOFsn7lVIRFuTT/USHU6CKC/YqJVozl3AQllSJaeXLTYK+MCJ9k4M3IUnib/9K4VU6DqM+ooJSeZ8YPQjXVeTXZJEpQceiDlY9s4L1byd8D8qw02l9GXZ4b4AgEZv/u2mnGva4YCBvX7UUgPduXk1ds82A3WtjQo20BCve7+S9f61J3skWYOGHioUfer4wAvY/EU6+wLu6IlicgCAlcqE2xHLZeVxeXxuSt8shSanijLcWThDhk7Sz8NEV3oGF4cwW8fw9SxZJfvAD89K0uegRgoph4+la8B51uxxWtk00DX70cVtukgvaReaTyi6W3mRX3yt5ZS6LWroAhowoCqst2v+KJVXsbcdCSUnujENRowZtvIm8rLDiGy50Q1Fl/qpErYqnTQkjg1wvURuBw6UWEJTnuOqbTyIaDBFmhVFKIYWLLYsFI0ViguXIUSkydj4ikGgRJKgsELx968lJRsxeQTofpfCdi6txXv3w+++Wb1QGmRKMPrGppCAvLcHA/eqpbbbpWJnHySpe+s0yBu5Zy8gjelHb16J9dZ4Fz7Wy/N2O6r6wWwIFb0yCXQ+E0bvjZaWNVjgKB5YSDnYklVqKaDHHJLxrGNGQ+Tt1EaS9ZcVCjsBBlazd44+vcCJdscJMrZsTczZ3UJw1C+LCR+Rz7DmlKDLX+G8eSVZb/00oeCOqys/BI1c6I6Y75Q2sPv1RtXWJwopSLuRyqIVzqtzZjoOzz7iYkFKto2BAorDiCweg0+crr10lrZcvfCiE6/3h3SOWl75eKRXL91bYpe+7EGhVEgQfIVSsKGDB2ohfRFRT59XNscNol8K3b11fJnyzlkIVmQ902n6BTeUIm+CkfAR6j8qQqq8sJTbvUc+Cp9Z7+4TlkztYPjnBjLQNUS7c8gs4/lzY9xMCO60FBeUoaiwdVRUtNOhthV0h54mPUySoeMHMQuvndMK/+C2v0KYcK3a/eD5HQJri/c6da8w9mxMjqOygiJre8W9i4Udfv+lrVMoIJ7G/RaiyDZLD+f2VQSmFs7C42FtAthPnuYeRex0MA4cGwkpQMTuXw3n1achvf7lutiQKcPbeuaRPSumtyi33JR6F9GvjBJN0efFXP+NdnbJ+9QZd/8cL+Q2ja0pPOL4gEAg7FdoWH0t4IlwVTUCn26aseDr3wm0VAlflvSRqfk5SPcn5LjaB64lX4bcS2TXVReKUopxjbICiao3XtiafhYeuhyf/DcN3ARAsngU/vS5Pfe/kbaoTvqMPJJ+Ik7Wns4nW6vG3cwCUwhIC37Ha8rctemmCW29rqGZAhioxgsoOiqypj3zDVPzrG/nm+aWCkh4E8W28Zf5vSSz/SjX+Dtb+H9fOBi3rcefPQC2eF99LLqsz0NbWIUfvAo19wXVRK5ZoTYoRUoqpSUOv2o0w+fgSqSrYJnq1tXAiLIW0Sphyon35ERKWFgz027ALgxoQuNpkI8JtpCzXZ7hcCOH5hyS0ibi4xs0y4b1uS8erleMdlfILCCbv1x+fUhJEYZp9EfbtC1KUPw6f5Q98NHNly8Iu3LwqK7AIKaoOTe4udLbBzLf9vwQvPWpz9Fl5rE0WuFTC10CQEhENGkFt1Zgzrk1oLrSgpClIKXh4Uh2trduJVLidYASVHRSnY0MoO5R4kgq8HCcJy8HzTvDfpgm/2G609k/h20bBosCkb4GwdLZG1ac/Vt9m3CEjcd98rlg13tGO++Hkssdn8PBygVTvAQ1BAcJgw6RbxDP3FKwr8tXwJn4hFVK6BWGo4Yb+MinL1QyioL2XWr3o2BQpK5oDJTpOPQBbOkWaG4HWgqDcsj4spS1chSrKSmYwRcf8Nta/Udl5tffoGkYc24c+4+tAKVa918bCJ9fSuihLrtVlycstDDmkV2JFY9dRtC7Ksm7Wlg053tI8dbfNvkfm6dMcr2mURr+gVONwX5hfRaIKTEHBIy2+XcF9ovdX3P/Dj9Rx1dWN5Q/EsNEYQWVHpasN5eYRMvkWKHKKTVgffSwr7w0yKHRR6m04sm1USIl6wgnvdVQMGIrY/QDUh28ZbcmmkncIXvEr4WtRYk09TUGsC4Gwyps0AgEhUrU4uMQJ/QMx00l5ohOS9jEQkTtSBlqbZIFBCHCUhRRO0XLQIcaijGalgvgRGUlp3xPlKta/vZZF/5qLcsr7qAz/RB8mfGFgEJkDMPTIFMOObGLKdUtY9moLcx9ZS7+JtdT0tcPvEFpIybU5TP7z0rL72B5IpRW2cEkJrZnT90s0MqtS2vyooKL9WlLC0UknSRZQSpmKIDTxtbbBQw/Vc/8DDaxaZTQpWwIjqOzA5FbMJj1oHKLg6+mrPaNVkQu/wCLSDggdZ5Mal9rWNw4nbec9bOWInVAjRqMWzUV98HZZh1tDMe6ogVUa8JOEFJ+YWKmXVKmdsQLTUDlNSZLvQBXj9bZ1lfLkY30MtnQTc2bE91c+pX5pBZROs1+aULCLRgMF/SpFvjXHrF9+SG5N5Xu5cWwNE74wECCmLZGWDvXe7atDGH3UBpp3rQEELjqZnXIV2Q0Oi57fwLzH1tG1zmHI3hnGH99A72EpnKzL0ne7mPF4GxsWb/5U+ZubPs0uP7iig959wnPq/3awkOSJvwLF9bYSlxrygZN2aOKLipRJpqG4v4pN/B4WAq66uolnn60r3MywGTGCyg5Mbt5kUoPGBX/7X3HXIh6to4rfTaMalyD/QLWmBX9TQaToYek2YMHwMYg+/XFfeKw67UrvRqhvgGwXrC6fYrwno5qbqnOUgArXLzKhJ7ZLmpRDIaXy3r0J3ttV6ORauGUoMOn09V7frqtXWTKiDSm3Vz2hF6r9/V0o30+nAIvyZqFQkFPJQpCC1U+tqEpIAa1NiWpSogghQCqaJtQjhAs4gS+FUpBtzTHnobVkGgTHXjmA3oOjkSgWvQbZjDu2npevWcu8F7tX9E8hF/w4LqSE6Psmj02KPH5yt9h9giIj8p4mpvQ+/BcvEVviCyraTyXJN2X//bqMoLKFMYLKjoyT94p3aVOLq0DZFAsdke99VCaJ+aJEV1Yg9m5eScDxfTulRPXqjRg7HjV9Sun2ffvBPvvr3z5trfDuZFg4v/Lgehgil6/uulRcH07cWuUtIinni99ylfKTsOndy3Jaea8PANcV2DGzUqGR0RuKEAjciDOsRLkKtXQ9DK+m3lNpLY5yFc66TmT/sCKy8jLgSlEpDNYfV7FAoxxFbn2W1c+uqGJ8mn4T6xOFlGBvQnghtZ7Pjb8vAU3DU5xy/VDSwkEmWCT8MNuDv9aHDYvzdLU4WDa0rnJxu4mSJZVWXPp/7YzcuZyQEbhNawEx8bqKggiv5H7CvEB+6LGujFyqKrKvOTNsWYygsoOjOtugxkunHTXDFFLwXI+aeoJVKqIhKYOA4po/pcYX3U5IGLWzFlSEgGEjYdRYqKuD9nZYtRx23b34iVJXDwcfBq9bMHfHyrMi5yyFo/cq30ioUGANUJ6AGMkMG3GQRakgisYXQpKcavF6rajU8eVlF9xsHrve9vYZNTBG+4iboAQulq0Qw+oL2pTaaWktj7Aka26ciru+i9TgOlTOpWteC1a9zZjf7gsUp+mPjskXUgr33ja7hYX/mIPTVr0UoEoIU9EWtVa26Gvra5Us20UiSp97L9z2pF/3pSalTVpdbS5TH+vk7bvacbaxpfXL3+9kp90qRWIB+DlQNt6EKMErSaj/TiGwhRtJkV9yUwSwZKmZRrc05gzv4OSXziQ1Wk9kQd6TCg8FV3+zY0TfRUoRtSDHHG/LUTAWUVMHqRQcehSifzNKuQihtS0MGpy4DcJL3b7nvrBgPjhbuDRsN0Ksb0NOXYA7fkT5dKxA9AoJKx4m7GtIFJHcI4ogRX5ZlXpM+1J6z+ryx6GtCzW8CftrB2m52cssq1Q8Z4XyNDVKKWzL9XwPCvdR/nhFwh2rXEXXtDXkFrQA4KwPZ2pnQ471zyyl6aghJGp6UF4mXBFMesqF9jktLL5lHl1LNt68snpKG4MPaSypVbGEo3O6xI8iEJZEVVY/gZISvOJ/mXrJHqfVMnjXFA//fD3ONvJjH7GTw8cOqva76mdBSV6XhESRIR6J5mtIqhFS8M77k08Zs8+WpqJC1tCzyS2Zgdu2Vmd2hfLPdt+npNDkU9CmnDkncNK1qMp/stDVQOXzsOd+gWlHRGdKX4BK2LcQAtJp1PiJlXfaw7AfexMxx8vV4bqeasT7iQgd/gcRSb5W9BtAeW/oFWz+4RaibAkmpRSsboPWLkTfOjKnjPccUUPTUuE8HeQiFGGmWj/5W1yjEtfIRIUKy/LECccNIm8631/N2hs/LDnWFXfMI7+yveDe1YJBygoX6v4EbbNbmHfl9E0SUgAWPL42ENSKUIq0yCcKKZ7Bqmqn50KkJRiws83E42o2rYPNwL5H5HGrfqdQJbQpfuVrN/CZAn39a3CDcxe9zxWUqdcc65qW9ZIlS8z7/pbGnOEdHTdP57tPkB6zN9aQnQheExKoKFcU659jG/oPgSK/lsJlCfvT040LTieMHB3Ll6CS9p2AQsFue6A2rEcsXFB5gx6CyDuk732J/AHjcY7YPe5oFDXR+DlMJGUmOG8KjKUSr2IMZSZMIcDql4GLDsQaVI+ssSPbqKLJpLBPxxXYVjyqyI8ySr6ZVcQPRqGyDq1PL6Lz3VXkl7ZVPJb5v/uAkd8aT2ZoPcpV2mzq6grhuVWdqJwit7aLtS+uZMPktTr/+ibSMr+LOfetYswZ/cHLpGt7RRAVxeneBVHFmR+6XYkSjr8CJp5Yy5SHtk3+lbqGyoYvjY7qKcb1/EwcpNCp+C20sJIR0Tw7ST1WMFd670UPT6qtaoSGj4YRVAzg5MjOfA2rdQ3p8QeUaahKmg9KzgkFy6uf2sINlARl6cexsuuLkzpV/dbo7f2gg1HLliJyO05uFgU4+4z1/FFKn7C4RqJcb+HvSplcIa5aL/Yz0VlVxai+6PT6/rpqLqzAjQjCvmAjULi+I2zEz0UItP9BZGIWtTZk81UJKQBOS445v3qfXns00Xvffli1Nl3LOlj3wgq6lm7e6JnBB9Qz/swmlMohLR1OHShXhKu1WpH2MiEMvPyZ1J3ZoliHIISgV/O2U7qvWiarugMsXDLkte7Eu73TIo+NCu45CHVrgipMO2hTdinrtL7HBM88bwSVrYERVAwBzrLZuEPGInr1LUqeFJiGSj06fN+B0i1CASX5JTdu4vHaub6pydP0JPZd4YETKBCEN40JC0aPgRnTy2/Yg1DD+kHv8rb0MItrNdODomx+rbBX/Ld/vxBgFEu6gR8KEKn5Iwj/r2YfekwyokGw0JlrlQjX28JNjEDqfeIoWp9eXNXeAHAULZPX0jJ5bfXbVKD/x+oZfkwfeg3PkO90WTm5lZ1P6q01ThYUC4ZexI+naZGEZQN8on5hxedSt02LfMnIlY9YL/Ej8eqTNqecU96bN02OtHDDe1cpMiJerykQkIk/CypR9tAVrF0nmDPHFB/cGhhBxRCiXLreeYrULvtjDRgZCCtKKZwVC8jPf5/0fp8AkS4yvagCIaOksFLuAVGgeXGhbBK5Ii1ORFiKNypYJgXsvjtqxXLEunVlBtRzUHWZwiXhb1+L4v1osaL8hYo50SY+0aOTqrcP7w1XSieYLOIThirStlSHCt6kCwUh31cj+DvhsIQAMtYmqPs2EwL2uHgIgw7sHUsDXz8kTV4Q1KIpMHYGaMfZ0o6kyeYfhYVLSjplhBRF54bKRRW3FOtWS+6/Oc0Z52aLtUQKQBU4aSvSIrmoZOAoiyIj3KrEcUEo4EQbazc4wX/ubMB1q1bnGj4CRlAxxHFy5D58idyst5GNzQC461dCVqu0s69Nwt79UGRjv6LJLBAcVFxYiT0GPVNO8Gehr2MhJZxji/xSSpmXCgYRrMtk4NjjUI9OQmzYUGYAPQOxIWqS0HV6god5YJKJvHaWfJTrCxYTMhIneEEoeISfBapCdeSIT0pVRxZeWEGlOkGqZDZa4XnlWrUWDbv3QWQk2aUdtM/YsMWFlzGn92PgAb3CcUTHhCLn2lgym3jCbeFiVYiowtOz+B4fNg4p4ZYs6Bjd/6znt20hw8fvSrNhreDEz2VpHqzH7zowd7rk5isz7LF3jnMvbkMXDnTKBrb592L1RkVNrZA6kkupINV+Ng/3P2DMPlsLI6gYksl24K4sdjhVHa2odFp/YQu/7QXP0dgDwZsMlVXQxnuLjz4zAxO8Fx5b9FRRVFVTKNpXkbbF16fvvge89GJyJz0IsWwtYtUGVL8GRKHh3ZdPXArWRT2dlfaHiCR9i++gcI+FrtB+o3KTY2khpZzpQidhEyX8a6p0x1SKAWeNpM+RAxGWDJxksys7WXrjTDrmtFbVz0YjYPTJ/cpMnHqNiyxIZKbPZaqCsFHcl65xA3h6s2ThTimF68Dr/27fiP43D32bXYaMdMl2wZypFq8+leLVp2wGj3BJZ2DlUkl7qx7wU4stdtuti0MPr97hVxE61pZCEmrnQnORwA/sqk3DI3es5vxv9GXufDONbmnMGTZsFKJ3X2R976LlRRoMb27zzTeJtYAib+JKxG3IMRNRCf+VSsTGlDRfSQkjRqBesxH5bpKKcwshAOvxt3HOOVz/XeL8BZNW7Hy5SCvumFjtPqNI4WDbpTuQvjyUZJ4JRxhbYskwuZqvCAqcaj3tTGHulUKUUtidXTR8fJCO4IHgd6pfhuHf3pX5l0+ha9Hmn7Qbx9ZgpSs5+ygcJbEKHF6rcQiNd6PIdSpevX4dR1zaiJXytQsqdl396//kHzegtqLlp2+zy/9c0slu+zrBcbW1wKN3pnns7jRLFxSLFr0bXQ44qDMcdxUITyBWEERNRSlME+WiCNx6I78a6uHGq9dwxhf6s269yfSxJTFn17BxpEuoOz3NSPRbH+RckcSlkMLtvN8qul1UJZK0TRUTZsnnVnSFlNoMtAMg8k7lqJ6oz7QAhEJG8oME26tKQkvSRRJlt6mcilwF7SzpkgoSvXn6BRXeNLJASAnGHuvLM2OhqO0tA+EkNmIpEFLQ78RhFca2aTQMSW/6xlWnbveOVcAHd69n7itZbvnSSl67uYXl03O0rY0KKopl03Lc/+P1LHhj60XFNfZ1+eEV7Uzcy4lds/pecNZ5WT755WQT1FHHdWDbGyOwKdLoN3Q/HZT/t/9jFX1HSunyoLYWzjhp62uddjSMRsWwcXSV+FIWaEsCYSMigPjmBaKCSGQboqahqE9JYuhCZUtz4mO8cBPXha4doyKzaqxkU9cnR2f79TQL/hukp6Uo9AUq/OTtCSEUlpeXJUiyJRTKS3+cFGFUKFgkj09hW/Er62/jKj3JoCgZkRSNmAmkrcXrUSPqECWyvwpL0GuvvsiMxO3avCqGrvUOhVqiEBWkdo9rU3T0UlJIcSksr/7QwJ1tpgJOFt5/qIP3H9K+S3YGahsl2XZFV+vW9yg+4dNZejUqL7qpmGPPzPHcI2lWLo1f2HHjsxulVcrEHLY1eeJVkQuvcNKQgism4LQTOrjptobqB2HYaIxGxbBRqJa1uC3rkjNl+m0g5gRbKJeU0obENCkRQUcowqeH/yK8ySk3I59dFxYuRFRTjbknUNU5i9pH4tlhiQgtsa6imW7xzEzSj/Dx+wiFEqVE7A0++qm8lqZc8jIdLeOi91nZRBWOVbgV1UMIKZB1m/+9bu20dlTel969faFIyxw1MkfGylNj5SO3raJG5kgJpwrrp87ImpE5bKmTn406MEP/scXHke+ClhXuNhFSpFQcfEyupJACuurFwccUf09dt7yWTqPvvzQuqRIPH30FPOHcW5ZCUItEiuIYuOjfDQ1b/5ztaBhBxbDRONPeAFSxsCIiQkqJ724giyStFwW/C/H311k5qZYq/KPQNKWUfvq9917FvnoKYkW10U2C6HtlEA1EgilF6PVCaHOLbTnYthtrJyVYVjQCSHjLVWDGsWSpEg7aNCO9XBlBu8QxF2qAKh2lrmfUtaitYh0kN+fitG5+PyanS7Fyciv++fYFCxl78/eOHxeLUqHdhegbPSMc751BBaaiQy8u9jFLQm4lfXtNHdRUEUDTp3/xtX/v7XTF8yCAOlwyZUo+RGsmW0rQgEUGieWJL4KSQYYsW1FN0TLDR8GYfgwbjVqznPybT2PvfhDU1HsLvZVF6pOE7UutiGwXcTfQ7S1X241cF5YvhtrRfhas0AG3uJuS/bN+Hbz8MmLD+vKD7UGIdW2Qd8Au92AVCOl6idcSnCwTN9GNSr8RF15xbQISJJtoQrlSYVluRIYopymJa2ZKOeVG20qBTp//1kr6HD6gVGOU47Lh1ZWo3JbxLF303HoG7teAJfJByLBvGrNwYzKUUoK8kti4ZY9P4pIW8XBdgU6g2DTcpr6vpG1N/HgydVDfV7DPiTa7HWmRrhV0tCjeeTzPG/fn6WjZjAcdoasDcjlda7QkClrWhwfT2ORwxNGdDBySJ5sF2y51/wkyFcKWg10ohRSCGu+mLMyArdCFMQvvgv/cve3qIe0oGEHFsEmoNcvIv/sC9kHH6wUba4mJuCdEo3MCjYyML9dhQd7tOmYnUArluvhpRqPzV2wui2pplNLCzltvImbO3MgBb/+IvIN8ey7uvmNLaBA8c0jkzdNPBJc8KXrmnYpv974YGZcyQt+VsJ2fnEzgFvmixNsWjyVtOV6OFD9ni8ZVfr7WuHkFFKIrx6jv7IrrRMOdwnbKUThtDqseXlTuAD8Saz7sQHU52LXgm7AgDCOO4h9/KWElRQ7L0xTFtoMgykXgcORFtSx6J8fS6XmGjJXsc6pN3yGeiSOSdK62l+CA020mHmZxyw+7aFu7OY9c4ziC15+1OfDjeawSM5Jlw2vPpBi7c44LLt3AmJ21dsv3gbKswvtJ/2FTnA25FEII7EAzV7yRF8wdJHzTy8B1jEZlS2MEFcMmo9atRnW2QU2d/uJGtCDlBBdf7ojhCyYli2sUbCC8B0esEl3p/SqloK0V8eQTiPYd10tfPv8B7shmaO5FfDbztAxWNMW8wr+shVO4QkffJKWjL8Yz3wTOtSJwrJUJycp0yLEvNFXTv0uNrasISxGGUQfZSL2+orUBU9IzJTXpMt7S0v14XjS4Sqf8b/twPcv/M5f82i3ncD143zpqanzhwDOLUe749dXw86CESxV2GfOGQFEjs9hCMXZvi532kUAN4GKp0OclPknrc9Orn+ATX0lz3++2zHmYdEeGvQ/JkxbFmhHhuqxd4vLtH6xl+Mji9PjBSBWsWS3p1eBSX6tIeUJKZbf70KxjIRKFlGAsBc7fEqr8Dhg+CuYUGz4CCmfaZJ210bPRRLT0JbaIv1v7+VWCUGYINSve52RE0adSzpgKQAhUxkY1969wTD0b0ZXH/tezyJemQZfvnKj9Fyzb9SZsf5m3TUQODPpJeGsvs1eEjE68yt8Drhu/ZjqsuDi0uJjwLkp5jqJFmiDivhyWVw+nzs6RkjpqxpbKE3DiSiZLKkRHjkXXTiO3astlZ60bYLP3xQOKHLcKa/Yk4RRI+36JgmQUtTIbJI0L/YX0b0fY3iX3NE24WORJCUf/WA67HAgNfTfu+KplxRLJ779fx7KFftkOPeYalaXR7mLE0CzDR5YPr5dS0a+vQ8daRQaFLfQEp0XR5EeSjSAj9M/GKIVthOe/Ipgy1bzvb2mMoGL4SKil88i/9zLkfVWs57SX8K0PhBMLsNCp9EXkGV2oEan45PDef5WrBSWh4iajyJ6VpaC2BnXkYTjHHIlqaqr+IHsYIpvHen4q1rWPIsgjLJ0rRUQ0KUKGk3yi37OoJrJG9+U7NEKo6QjSmStdqyb0G9kYTQpIkdcRLSK07iXhO/2mLcczVbmBkFLoIOyP06pP0XzCkOoGsomMPqa3t9+YuF2FoFbadJeEXzG6tIYGHAQSpQU67/q4hCnnU1Jx3Pkf3czR1M9l4l55dt41H0sAuGiuxf9eUsdvvl3Hrdekef8lRUZqDUq5iCCNwkLRYLsMHeIWmXsKHy0W0CAktVKS8oSOtNB6LLeCkCjR10sIwYzZNvMXGUFlS2POsOEjoxbNIb9kPmLwSOTAYdB/AGQygWI64oKghRN/QlSRXxvzOhMhUH53dUJ9DV6ajohGRoU6Wn8wQ4fgDhuKeO99xNvvbuqut3tkexb1xhw4cGxkqT5fgUBRYtuNiQ63pFuk6fD3lfKcZWXBBF2+Zk+4vS3DyrnVjMlVQk+/Zd7Mo8JK348PZOUjS8rJAB+J/hNrkVbo7xAI895/5TQkItbaF9qTt7FFvuI5dZCIaG4WETExeQLkhIMlo/dwmfvexp+Qpn4un7uok48d4ATauLYWeOzuNI/dlWbYSIfzvt7K2F289UqRFwILVUUWXuGFGFe+DyRQ4wkl+n6LSqq+qFi6MKf0TM6OI/jGT3tVd/CGj4QRVAybB9dBLZ6Ds3gONDRiHXkSSoASIl7fJ0r0yfxRUApRUxv6vRT4WMT2B8ETT+2xG2xoQcyasxkGsf2hBIi+4XmLazsqbFutIGElRcr4U0SZAoVB/pbyOymRo600ETNWuWPw16Ua09i9U+TXb5lcO4Uh/tIT0lwEdtnMswIpHM9MphPAWWUm6Wo0NMl7i39JHUex70mSue9Vn2wOoLGPw8/+0k59r/g46nvBmV/KMnZcjgMO7orff0KPKY/AYuP2VwotpIQ3gRvR9hUXWS0WVqTnw7KhRfDlb/VmxUozhW4NzFk2bH5a1+O+/SJyn8NAepNSNRNKRP0SU+OX2zawtZdwmSu3rVKoPXZFzZqzY2pVdh2KmDAkPhUVnIhSl6CUI2xlvGgM6Qa5UQrXx1Ppl3aFjAtVIpj0y03IVoIfS6m+gxG4W0idAqx8v4PGUZlAq+LvO57Rt3Ar7WuTFi5+PaNkIUVFPunzUz6kudRxhjWBLEswaEyVBwcMGemw58E5TvpsDrvkbKM48BDtpFs8Pn13OgisCuO3I0JHEjaQkbLoloqfJb3CxVPCetdh6XLJmtUWy5bbPPNSmudeTpN3dsinxjbBCCqGLYJasgC1Tx5EqqKgEZh+Ss1JJZaHk6tAJb19JvQZMzMJAb176+pirW3lDqdnst9onXdeegY0X9NUoGkodfnKTaRQKjmbICVD1X/hvqI+Ksn+JjHjSGx7Rbm8bZ6AFKk2XFmj4jnrNqZwWrZM0cp5T25g7IlNKKGCauFaAAtNGRC9Bjq3Slr6NXF899fC66Q1LLEKyRU8Eiul5Pf34VRxKuoaFOd9v5Nd98lr62vkXSXp61xJ2+OgTUDJ10yfo3QZ9awA0kle4WXIOXD8mf1o75Be6QfDtsI40xq2DFKC7WVwquaF1BcqvCiQwk2i05NCmy2UHf7ESuh6P347ItsEqf39zxKcow4lf9gBuKOGozbG+WJ7p7lXQsniUFjxUUWf9G+3SA4JW9pBmLN2x7SEgy31TyiEiILtVExLEn6OXthwuUSHEcfGquK/o/1nLCcm1FRGIJSicc+mqlpvCh2rHd64cnmQyxCiYxcoJBYOKZEnI3PUylzsOLwtgsDq6Nil8CObXG3mUIXnJdxeEs3bohPGpciTJkeaHFbgVqtoW1vB2VQqvvZ/HYzfM+85niZo5ArGWhjBVDxCgZNwb/p/1FDsQBslVcX3uvCWWbzEoq3dMkJKN8BoVAxbhkwNFbwBgfjDIRAqfEHCJypsKMKyp1EnwkhTpdAalugDUkR+YvtU0NwPmvvijN8JNrRgT3oasaG12iPdfunKQ32m5DWKakwKz7QfChxuqvOkxB1VFbaXJj8xbDxmFSxttin1Bi3QzrF+/36YvFJx06EtXVIyeSJLekMvrEMkU1vufa5+gM3ow+uwpQNSR9ngHYtPRhYKJoXEjHfBspyySIuw6KGDROIiVfy62bikRT64ZinyWCI8NwqFjQNKkAMGjSgvqOxxYJ7Ru4SiU7EIVThaLWhZ8acBFi4p3MAPXotKno5IaTEuJXS+lEq+Sn6kzsYwd56ZHrsLRqNi2DIMG1WkBSmk0C+iSKjwI4REqWURu35kexHpLNh3gpCiF0c6VkBDPfkTj0FVjofc/pm5rIQaPNn+JoQWRixLBSn2pXSwpBMkaYubcSKTlSj0KSl7R5TBMysFPhmCvBtNfKavZ1QLlCyklMtU4h0r2tm3Y1Hl2lKbQv1Am6N/PYhhB9RhWzr8Ny1cUsJFuA5LX2thw4xWlJNQV6tgvAASh7TIUiu6qBVdZLyU/FHfE18vkiZLjchSJ7JkZB6/cGQKJyyEHnwvvX9CkRaK+l6KUbuWHssXv9UFSpX6ynl9+r+1FidWqgGXGvLUCDdwtpZCX3MbfeUELvXCJRMRUgSQIvnte2NEFH/cUz5Mb8RWhi2JEVQMWwSR0fUvYhp+EoSWgqeZSFgWbOcLKmUI+i9VQSxprNEmUkKvetSYkeU36gm8Ma/MyshZUToMWMqoI6ouJOjLi8VaCRWo/Uv3rycopyt0qo06kZbaLhRS9NWWaN+TlHCwhYOF4xX1C7PUJvfpTXmK4AelJz7p2Q6dthzrJ68tN6BNZu/z+pKpl4nmLttS7HRQmsHjLKRdPlsqCGpElozIB+fGN435BQlttACU8nKkCHT+Gu2M6yK9gofxYogA/nonzJYL7POJ5JEcc0aOmjri2syS6IglCwcn5/etyODEhI/gKAWeGVELTNGIMUEooEghSAtBilBwsTxNVSX0I0YLZg89Zmr4dBeMoGLYIqgOzznVf9L42We9n+jn2HZJfXnbl31FI7o7Ff6xcdpeb4cKd9SwTdhw+0KsbIH1lbQFIqiGLAPhoHKKe1FmXRxFy29fov2RWdqB1CmzndKCie/kKtBmnYyV9zQ83pu3BFvqdPK20H4slU0nItCgBA5Orsu8v81B5avzaNkYMo2SQXvoibBwbP7fOWUhKhaqUdjkSQnthxJzgo6pKCNbuN559HxTRNBPXEgRuNg4ujCi57di4yBwGX9A8jk57tNZqvUAio5u0XyL9WsFtqfRKdmDN0AXT7D0CDIgRLWsQgt4Ugh9XSucSp2HUjdas0ayocVMj90FcyUMW4aF8/TvQptM4U+ERP/K6LIqJj5V6hFXYdu4lkegGup3DMfau94IBIBilDYdFLy5VpM9tvqpSpDedzCdT8xl3eUv0/niInIrtfCkXKV/HM/JM+eQkTnSVp6MlSNj50lZbsVoFoXEKXJULfRFiQspLR+uY+avptL64Yaqj2Rj6DNWmxXKaZw8j58yvWhtRI3IVdBgxL8VaZEnXRCRpU070b/dIEIqarbTmhqoq1f0HVS8p/peemu918r3ga/BGbmTy69/0BCEj1f+5gn84CPfZa1SjR67TK8WAokkLWxSWLzyujH7dCeMt5Bhy5DtQr/3VOfr4QspqkAoKaV5iW9boEGJ++QFyxTJE4Pye4mua+5D/rRjsB9+BpHbMqGp3QExfw3qoXfg5D2LzrEQqijEuNjX5CPuH0XtcWPpemYe7vI22u+dTvu9IPvXUnvgEGT/WlRHnq7Jy6nbpTd1x49ITBIXOvsm4WlVPL+J0Ek43Mp35Jz+/6bQtbwrXsFwC5BpkFWcQ7+WdIHrqdJapTQ50la5/Cc+ug9doNElI/PB0lL7tXBLhm/7y5qaFWuWlepF4KLKfPu9mk4Rk9f43TcuqZsi4rJWyWnfc7C2hfB0gr5WThS9rQtgr121WcxE/HQPjEbFsMVQbnUPHt//REnwvOWC5dVoUYLskb55yH86FggsonAZvpCjgvpDWGE/akA/8kceiJISt7kPzsB+uOlUVce0PSHemA83vwTZHIFpR+ZJ2wWVlFGIoC5P5cmxdDhs2J+UAmEJ0rsPiK11V3XQ9tBsWv75Pq3/nUZuxlo2PLGoIBlc2FO5SRd8s5XWUoiI+Uh6+UZwFS3vr6NrSecWF1IAWhZXK/yGLunS8yURSiGVrlW0MQgBtqpuvxWFUQVDEhK/rYoJLoGrbMGP52tCXAiuqwszx5Q/snAfOrV++YdECi9aSAhPMBHYSGxkUFhQIAKzjxCCMSNdDtir576gbG8YjYphy9HR4eVSSdblRn1P/BDY4AkVf96VJaZRSW4QalUiQ1H+lr5wEtmfFpIEauxwsmM/Ga+Klssj35iC/c50RHWV+bo9Ys4quHwSnLEn7DG00Ksh5phajSYAdG4MW5RK0hX3J7IGN8A7y8uP0SrtaxIKK1GxxRdSQudbgVcZObqto8i35Fj073mVDmyzsXZuFifrIlPlNSuW5yMSKygoPa0KBOn2kwQ4HwX4yiOn00XVqqLJ3fMj1t1Xo0RQYCXMHvf/K835P+iKNPMEX8D/EhY57HoZZ885t1X7h4nSelh9GMpznq3ke0Sw13K1kf2QZyvyEMjl4ciDs7z6ds97MdkeMRoVw5Zj4bySb0bK13wUmnWiL15Val0FouhOjnVphT9+FLLyzUwpEr8FsYebsLQVyz8Y28I9aE+ypxzZs/xY8i489qGuoCv9iSvUPgD4Kds1JV2fg99awFEFy3UfWlvjmSWq8T8qqenw1fmgXLxQXv1jCRUTJoVn0vCXOJ0Oq55ZzsxffkBudbbyIDYTyoUZk1rKTLRag5KWjq7yXGSWg7znduqHUvt1f6KaC39il8KBjixuTiVfNoTXX7nEcJHWElYvLV7+1vM2Lz0eFTP8b5I+nsQKzkKQFo5+F/A0X2HqufC3fyw1KOqEzqFSHboyctB9ARaSNFaR8JbJ9IyXkJ6A0agYthyzZ8D43VBSxt5oAk0KxJ4c8ffg4vVJlNOmBNb9qDCkIn9XaVaK7SPq6DJsIM6uO2G/P7NyR9sLGzph6lLE7oOJCimx/CjeG72rgjMcQwp9xtKWr+b3KQ59xW+RT0q3H0d1OnTOayEzoiEhGsbTp0jBsms/hLxD06EDSPWvIb8hy4ZXV5Hf0EXdiAaUo2iduo78uhxu1q1sxdpCTLl9PUP2qaX30FSx+UIpaqyuMtoNAZ7Dq9/E13iFhQ21QFgjcrpdvd6u1NupQpBHYBNqNxN9uhS0t8DU15PH9e+rapjymsNJZ3cxZBQ4StLVBk2NbqAFCsPLBSkcUoFo4gmugEP8KypR1BGGX1erzEx5vUghvfo9gVeaNgWJ+PMJdOTYjNlmeuwumCth2HJku+ClZ+CQj6Msz8ZTbX6TKrQqgQBR6lUpaT+B/4vaOCGoxHicvSf2LEEFEB1ZlOsivRfjpMywUoJQXiIyr0BhVBZUkU/FNWghLh0oVGeezM6N1B04CKtPDe6GLO2vL6dz6ppY0/WPL2LgBRNKjFz7mtSNrGX140tpn7q+qEXHzO6VcfiJHy1jv6/2Y8RBdcG9JYSgc3WO3gPLfQEUNSKv5e2osB/5bKGoEVksWbglJYQQ71oJJ8gUWypz7z1/Fjj5UmMTvPuqzbuvxqeXdNrh9M93se8hWQYMdLCFIiW0lsXXsoVfNUUKAvGlBoUtio/V/46WMu1IwBLxPDT+Z3+bwjvUdSGXg4eeMJE/3QUjqBi2LCuWwaP3w4TdYMzOFD1ZEwgmvArCikAEqfKTKGl2qkJICfov5/8iBDTUVu5oe8O2gsMtSv4Ve5PVIb1SqqLLpCcdFWhftLnFzyCrnSbDyUNQv1d/6j49BuW4CEuiHJe6fQfQOX0tq69/H5XVU1bb5NWsvnce/c4YhXKVp1kJ9562HJpPGUbj/v2Z88spwXbdFTcHr/15Ne/9ex2D96rBSgvWLchx2Hk1CFHKP8JzrC1zD/unNmwTfhu0MKIKluuGqaCijsAhEorutXLycPP/wcx3Nt7kmc1a3PGPOt58LsXv/rImHCtJlY91sjrL27lN8mMjqHJcQhyurSbqMHLz5vParPWT3zbQ0mY8I7oL5koYtjwd7fD26/D0o/pVZWNV7Qntlfev3B0s8PxSCrarNnlcUdhzYqOeZ8dWyzYUnFflhSr7cqbwwjY9zwhVLKQAQcFAP/V+2sqTshzStiJludpvAl2Nr2ZML72t9/rv/87s3ETTZ3aOjW/944tY9Ou3saWL8JKR2cIhIx0dRSQE6YE1DD579BY4O1uGjrUOc55uY+ajrWTX5ek3qrwTp6/tKI/wJnI3yExr4+rQY3SOFj/pm+2ZX7SWwtc0CFwV8Rdx4en/Cma+89GmDScSDOgLKclj1+MoJaT4JImiFlCPhSWEZ/IJ//lRPj6Bv5IDL7yW4txv9OKJ5402pTthBBXD1mPNanjofugsnw01UKREXSBUdL2eGZVn8gncB4X3E+mjSMBI8I0pS9IMHAxEIdYUmxe2Z8SgXohe8Yd0mJU2nDDCBHCqaJlerokWBixMFS+FziCbsvLIElXlhBTU7TcQ2RgfU58D+pOyFBnLJW25RQ6nQgga9++HsDf+zX9b0zS0tBZAoMiIPClZnabIIh5l4ydTsz0xQAB2LB29L6RElJ9elO47z8Mzd27SIcVYON9m7RqJUmH+mlJXyfUHUwEXSAM1SGqRZISFLSUyIRugL6To4oaKyVNsDjixD/uf0Idv/b9evDfVRPp0N4zpx7B1yXbBww/ASadCTU3iq5KAIArE97lTnhee6z93vGd54JgbFWpkZJtI9xXDmCNU29Z6eXLlzrYH0hb2Z/fCmjhQZ4JVeBlfC1XyUVRJAcYXUFwlSAknts4nMCNJiVJuybdmIQU145pof2NFsKx+1yYv8Zm+8NGkZ37si7Aldp80uZVdxZ12Y5RbamZWZESuhFkuub1F8XkVwsVSipTIeWdK6mKFLrSuU2Q7FK4DytG/Vy+BN54QzHkPqpfwSzNseJ5VyyX9+uarelN2ANtzEo5rQlRMm2IhI/4ncYGkkGCdEPznvlq6stufQLsjYQQVw9Ynn4MnJsGRR0PvRu2QGX2bizrNiVD+cCPJ2AJKfA4e9b4RO6l9JQr3FXTuvQVOnoq1sHzuj+0F+7N7IcfrpGva7BIJ6S1xzsql0g9yrkSiNJLw3+Ir+E1DgcbFF6L8+jy+06eOetFmEQfKhDR3X5yuZOesQAtS1T2sSIt8JGGfRuCSJo+QoVOtn+fEkZKmvooamSXfBa8/JXnyDpv1qzffJH7YkR188/vrUW71X0UL7RCrCm2M6K9oGC9UWiBJQqFobxc8acw83R5j+jFsG9rbYdJD8OJzsHihTt7gmW6iBQx9841rlfBHKeE/EjMbbSQl6wUFDRTWYy+RfuXdTdtBN0MM7o01cWDgmKp9SsptUVwDKLHfyLUp70+hAn+WUmTnt8T+bp/VGhabgyKNDoBUivyarZcbZXPhOMmTuCWqMffok2jhUkOuaF2aMNuqL/T458zCRQoXBxsrIzjoeJfvXJml78DNI+wNHpLnG9/TptKkZHGlSAsi2rMQXwjxE1oXvqiUE1L8RmvWlXobMXQnjKBi2HYoBYsWIl58Hu6/G6Uc7WNigZLgSv+3SvYtqSCMJE6iVTxzBaL8s2veEuzZCyt3tJ0gdxsUFP4rDP9MylchqDJ7acTfQVHZ7zipjXIVXbPWk1/aHlu+8qFFyDIOpULofC+1o+urGWi3YvXcPCqn0+VHPa6i/kDF6DYSlxrRRa3oQgmBo8Jz6qesL1e/x8dFV26u6w2fvLhQ4Nk0jj9ZX8NQCK6cLt8vJFgqTb7vGFuLVfSVVZVuOEGyJ66h22FMP4ZugejqQs2aCePGaemk8KlT6gFdhZ0eT/CIhRuXHYz/n0oMj5YbWoq32Z7J2FE7QMGkpc9DNEmXLJMWP44KauwEeqqEN2O/RThzhGoYlXNZ86+pxe1l5f0rR9F79yY65rZVGmi3IlMvsC2tsdJRN3pm9wW5wuOWuNQJ7YcTrgsbuYBUuiBhOcJtlbedIGW57LFPnj/emiVTq1ixRPLcIyleejxFvmQelWR2+1g2VomivIiiv3hpUVpIAd9VTUcm2dH7DJ3wTqryQs6wwVqDaIoPdm+MRsXQbRCTJyNWrdJP42rDfss08+MJVOTfxpmDkl49wZo6Z2M66faola2BD0hiJlL/twonyurq/RCpvhw6HcXfo6PCUbRasF7eNXU1zppiZ1hhy8j2ye/lSqntMupn10+ksaS+V4XQJh9LKBTJdYHSwquGnHiocdtodbVx9LmUKGrJkcKlV5MinYEhI13OvriLb/6qg1R6I01CKvxgoUiJ0m5gAqgRRa5JASlEEN2TERIpRKQisv7J+yURE54lvjhjW4KUeV3v9hhBxdBtEI6DePJJxBtvEmZ7K0NFD8yC9ZGnYmLPno9MLKxZRH5QsGQFcs2GCjvdvnDfWVIhhX01FW2jhEJKoXZGBZ9CZGSpQNfnkUKbOgp9U3zSfVNaO4MfDVI8SmlLOuZvX9oUgGG720H4djyaSSXU4dE5UMoLIH6N8PJ+QFHRXqCopVBLo802QsDOEx1+dlUrX7ykjfG756jm7njnrQyOo2KJpIPjFOF+0yjqgFQZIUUXJCxuEL2TJIJ8IL7EsZAIoLPLKxpu6NYYQcXQrRCui5w+HXHfA7qEqf+YKTFTqsjq2HJvg6KMlVGHu6gQIpK69zv3fq9aS/rh5zf2kLo/XXnyd0+pMIn5okClyU63lcLVWoHEtaE/RbySrl/d2NWOo0rR9kpxVFXd6HrGfmuXorws/ii1qUqR25Bj/TvrAKgfWcvQE5sZenIzvcd3b7+V3v0hagyzvHPlG8YKfXmqrYvpVMzSKry8JsLTa5Wu7CwkDByuOPy4Ln782xZ+dHkLtXXlTUuPPlIbCpaJfjK6GrISIEs4QQkgJWRZc1DhXScRpLCxkdhYpLH1b5Fi6TIb40zb/TFKL0O3RLa2ov57J+4RhyGGD0OFubKLniuFtfEUCiUJxPAk1W+hH64qWql0vvCVqxFtnVjT5iIXLO2xjzT3ncXk2rqQX9gb0naJCUqF/5fwNQm1KUnnXAsgOqxZ6ECvWB8CWzqB7JiSLrLOxm3NRZsw+mu6FEOpvCy+W8yGyatJNViM//pIeo2t1/lJFAhL0L64k+l/mU/H0u6XYyVTn5SkLCq6gKsUM57Psm5+nk98UVQlrOiigxYpnAJfF309LBwy5LzvhiCLxC6T3wYhAp+Tcbvmuej7bVzx815FzYYNz3HaGW0cclgnKZnsZxM9UscTNJPuLxtRcl38WFWgVclIP4FbsaA2bhTsv2ee198xU2F3xlwdQ7dFuC7WM8+hhg7GPfQgVG1CXR3vqRcIJ1Boli+WSoJtQZVq19JG6u7HkB3dbyLbUqiZq+i69hUyFx+EsmWQxt5bC4SVkUXsZIlYG9sqNEUoUtLBltEoHeU5i4a9SM/s4wsaSMHQb+zK/J+8GTRqGN+bVGPpzKHR/C39D+3HsMN6I9PeFB95S68dlGG3H43lnZ/MILchX6q7rU6mAdK1Uafv0hPy1Ke6+NT3rOCalPM4941uDtIzFanIWkWaHDZuEJGjlMISkEeQqsKsY1mw5/45ho7Is3iBrr98yGGdfOG8FgYNKkz4V8lmW3pd0hrpCSSgHWhdFJZnGkphlxVsHAf+5/SsEVS6OebqGLo9YvFS5B33kv/UKVDfQPC4ivqO+C9LG6Hy8BUnwTZtHYjV67Den4Gct7jHak/KoZa20HXVi9hH74T1sSGe02okPNZv6PmQqIjAIgApHS/SJzyxKel4FXKL36QtoXBcbQ5Iy7iAIwSk+mao260P7VPWAtB7194Vj8HvI5V2kSLJfKEQlsBusBh0VD8W3rfxSfsyjZLew9I4WZe1c7Ior36NlRZBRleAmkZJ01ALJwer5uSCdqVw8wQ5gMJzW6hCVKxb4nDO/1rYRVE0ouhvmzwZ6SCUwhKhhkR5kV41Cc64QRskrnISwtGLRSPHgb0PzLFkocVvr1jF+AlJAqAKzFl+L26sJz/iiISA49C3RHqisoy0EQgsIVFK4Xq9yhK+LEF/Fuw50cQod3eMoGLYPrAt6N1Lq+69RQp0cjjYOKfahMVi3mJSk57bDAPd/lGr2sn99z1yd78PNTaiLoW9+0Bk3zrkngOxM+HkLwretpUSOK4uYOgt0VqSktdHt02JUiYGxaAv7YSzYAMtU9Yha6qohos2M6VK+Mj4qjNbuux8Rh96DZQsmLSG1gWVtWc1TRYf+2Ifhu5XF9Qn6lyfZ+30DvqNStFrkH6krpzRhYVD886poF2+y2XVrBxTH25nwVtZbc4sINcJiz/MM3gXK8weW6jRENBYl8OyPFnd0yL5XlnRm90iT63M6yR+It6PEOVzq+D15yC9nC7xQRQ68SoFqbTiuz9YmyikCBSBLswbphY20OYeb9z6b7AihyKBNNKLAhJBF4pkbYkUMqKVKk/OONN2e4ygYthOCB9GsUex4iO6hCtoacd+7IWP0knPJO9CaxbVmiX3lA7Jzj4xi7rP7kpml74lnGq1s60d1PcpJ6To9lDOGCCQdSlqJjZRv2sjqrMaM40WQir7QkhSwmXgQY0MPLCRKVcvYtXk1pK9pntJjrl8MJnevjOn9rfp1QS9DqgF4VdsUAzexQKs2P7tjGTQrhkG7ZqmY5XDE5evZ8384uN5854sp/2kzj+U4JcAlKtoWekydHBcQ+UXG/S1JD41srwKR1aYzP3cQ46D548S+rNYBcKLbUN7i8uhR3QlXM9QSCm8JgqtEM37/k14goln0rUQWCWqieocMVpYCQxgkc8OLkKV1qooBU+/bKbB7o6J+jFsH+RysGYdRe+Y1cfMFuCpZhYuJXXnJIRr1L/VoNZ10va3t8nNWO051FLwowIzD0Qnz03eY+R/gUxbnlNs6U51iHM111MLVdISCAm7XTIUu670I/Hj/zeImkZLR6egdNVh7z6KzoNBPo8yQlJtH8kJ/6+J2qbi/c17O88zN3TgusorUKjA1ed6/TKXaU92UurGL7we0aiqTb0MSgnee91i/WoQSpEhR43IkRIONg4WDrgOoivPBV9tCYQm31xoobC9/Qfh/7GzobGBegT1QlDnRfbYQmIJ6Z3j5BNa6kpLpDYNCUjSrvh5gW6739T66e4YQcWwXSAAa8rU8NUx+BEIP16z2iexchFLVmD/92HSDz+L6Nr+6sFsU1xFyw3v0Pns/AINh64BZHu+Jn4St6qSw5XMmxPWwAUB0ntbjqnUwm0ljvZ1SdiHJRxSIo8t8p4pw/WCfrWjrUwJBh/amDjCvc7rQ69BqWBflnARnkNwYD7EN2VUEcJtCdJ1gvGfSHAQB96blOOfF7Xyxj1ZZr+eZ8ZLeR7+fTv//mYbnRvCJHrV50YMNVdR3IQzFUMpWtbB9b+u5d4bU9CVD4VAXyBCUW851NdETUFK1+ARwdc0OFFJV9o3AaWFTomPiFbNEFSq2+NH9oVuxd51jWhXookf/XP2y79kmLfITIPdHaPzMmw3iJlzEP37onbdBVw3LBriKP237VUzjPkTRvT/2Sxyygys96YZ4eSjknfpeHAmHY/Oxh7TRJ+LPoYQAilUzPHSlyGhlIZBv3Xbli9rRo0G8elMCi2E6GrJntOlCgUES7rBvhUi8MmQuNgi9MXQFZbdoj0oBb3G1AJrY/tN95KMOcYPuxVeThOV4GAa4ib6dYTHDFqTM+aQDJPvTE5K17JS8ertxX4zc98LRx1E7SeauRQuAqlcb8IOi0j4TXUUUBkzmRC0rlH87m/rGDTU9c67CjQjEqX9iwg1HsoTUvyssuWyHSfvMhRLKhYWLINAIAqeB35/CrjqphS3G23KdoERVAzbDQKwXnkTtWAR7oRxqH59IJdHzl2AnDYT1bsX+SMPgD6RyJBsFjF/CfbbHyDW9ayMst2CnEt++hrY0IVoqgkEk9DZViBiZpiYFAnofCnRtXFjj9bIWDJPJpLp1tfWWF6IjCwSHHS+DSniQkr0tz/O6HhVvlg9MeH0xoKIGLdUPE64b0oJD/GjTNdWnoiHjhc0NgvWLVcsmaFYvwKmvqyYcJDWBLmeYOAfkz8CS2jBQkiC0fr/Bz4vCHJIUrjEkyN6LVyXUUOzZDK+JkmbcvxWkcpMej1eQjdV7vhjewiQhMKOFj7DsONqEQX3l39MgYCiYNocwQ9/U8OMuWb6214wV8qwXSEAsXgZcvGy4nUdnaT/+xCqqTeqdwN0diFWrP4I72SGaul4eRG9Thzj6RrCvB3F+pFwipQoUkXCBwhcr5lum5EOdqJTbrjARSB8nxnPzCMp7ju2tQhHBVrDsfrdYmfaofsXm2eqTbBWGBUVxXUUaxclOwc39IXP/CTFgNFxR9DOVsUj1+Z56C8ODX0sRkzU5h/Xk320CcUlLfJI4We1dXCxCK+AbqyAFI5XR8g3tfjj1dlwM5ZDpl7FtnU9gQT0/nTRRJdMwfmp5nsXimyQFrJo3cYSFbaEEuQc7Tdl24rnXrH55v/Vks0aU8/2hhFUDD0OsW6D0Z5sZWp2bkIKHWHh4mcPBX8qChK5Af5UKUWho6cnwAiFbem6NhLXM+mUq9gcTrz+BK2RCMpkVg1GAyhFrtVh5dvFtYXq+tnFG1QlqJRGoJCWYOpjnUXr+g8XnH9lGNYcJVMPZ3zP5r4/5LnlJw5j93bZ/UhJ737QshrWLHI49nNuoDsRnsOvTQ7XK1oA3nmNnRuBIMwarKN08qSFi1IuMmLWcbBQuNgRzUrgT7KR0oWf9C+SSNrrr7JfSjhy31wUjlG4glXrBM+9kmbdBsHDT6WYNa+60HZD98MIKgaD4SNhN9dSs0sfwIs08RKE6bdqN8FXQU+ELiJI/BWKGiG+j4mrtPtReRQZmd9k7ZkQgoyb5eg/DmfVh53MfXw96+dlA4fRqJjlILHKpZbXPZYQaHy/GcWcl7qY/0axD8rZ/5dClDheIbQQeOIlNlNfzjLrTcWsN+MhyP36KfY9Lro/bYuxcLET/WY8MxROcHygyAgnImCqSGvthOyisD13XLsgR0uSD4ogzMsYdaj1nW791PkZUb1A4fsn6T7DtPkKwT9ur+HWe2uq7svQfTGCisFg+EikRjRE/vKTvekJsrRDpRZNHCXCrLUUzu2+QCNRFQSD0KGzYC8iacosHInWOtT11blPavvZjDiigSk3r2bu4xuI60a0cUV55qlSjqLa2db34Ih6hHg+HgKmPdFepHYZvqugoU95cUsIQaYORu0hmPdusd5m2BiHtB6h57sTFTIKz4RelyEfnKsUDlI4gRCZhPTEgljVikjH4RHr3ynA8oSswr1rf1fhGZB8Z+eN06boMenRuA4sWCK599FMmS0N2xM90lj3i//9CdM/eIvrrrkycf1RHz+ce+68lffefplnnnyYSy+5EMsyakGDYVMIHVBVJFW+CCa+cvlE/Gkz9E+hIIOqbuOq8hOXXTKzrd9H4siDTzUyTE8qLe0XsseX+tNnbIaONU6Br4ki7xWWKgwN9oWUtMiRlq6XZyTUZkR9bY68uLiK86g9qn8kN49IPq5BwxVCKL1/TyMS/kR1GVpAy6BzoqRwSQuXlHBJ4fv7FOP340IQhh6N7vJb+bqbFFHTkAh+EGEK/Kh2JUeFOgPRvQjh5UvRexBK8NKbKc79Vi/aO4x3Wk+hxwkqu+06gTNOO4XOzmLbL8Dhhx7MNVf/kZaWFn7x69/z5FPPctGFX+anP/7+Vh6pwdAzyM5ah8qHkTXR9PrV5FAJdB4CL5NtUqYNyiQMUSVFEX99/Hf42Y+OkV7toqhRws0rRh/Xm3duWatNWkK7kvq6kZwSgQbARzuy5rCl7iclHGpEllqR1f4zegtA0ThQ0tAvPnK3+jma1tXJ58PJeQKjIiakRMmQpZYcNUL7oRQ6KwtU6dMdHGvYxhda4ugFlog7BIdrdfyR9Mfotak29WLh5PXEs2lO+kIvvnZZL9as63FT2w5NjzP9XPaj73H/Aw9x4IH7J67//ve+yfQZMznvK5fgOPqp0NbWyoVfOY9//ft25sydtxVHazBs/7htedpeXkqvwwcXhCZvGmEZxIihQvgdFxovypk1QCmt2Ymm/fLxzRW+2Sg+ZoW0oXn3Wt7+60rIuYhUKIWJyN5UEDWjk91ZKIRySYt41WCpdDRSXgnynifFPmdkeO7v4UvVtJddjji78jlyXcXEffIcdYYW0uZ8IHjpIcH6VZKWtdCrd3nHYxfpJW5TiWn0k8xoSf04QNrvIzgl4YZW4Fid3Fk0nV+050oUOtBKBPc/nmHR0h43pRnoYRqV0049iXE7j+VPV1+buH7s2NHsvNNY7rjz3kBIAbjt9juRUnLcsUdvraEaDD2KdffMJr+iPbZMltKMFBAVDkSBIOGLLPEHVcTU5DnRlrIMRTODSBH/8QWYdCSTbvQnsnsWvNqO9MoD6NiZaLFF3xgimPZYBwpVJKREP9tCO6FKFAPGxk3OqxcpVs53i3w5CqmROfY/xmXUeMWo8YqjznL5yY0OX/5/OQaPqBzp5HhntJQfSrEpJ6mFd34VgSknug6gXHh2lEBQFNVF+0R9WPxPi5f1qOnMEKHHXNn6ujq+++2vc90NN7Fq1erENhPHjwdgygcfxpavWLmKpUuXMWHCLiX7T6VS1NfXR37qNt/gDYbtnbxiw6QFsRTqUOntWBW8uQukcIs0I7ZwAqHCJ0WeGpknbTlFmXDjCJyoFBNJ1S9R1FvZkpO6AITr0n98hvf+syEo+BcTZCJCU75TseKDzqAQYKl+lSJImpbrKh7xLZfl6CiOkg42tslRJ7NIWaCJELDHvvmyGXP9I6skPihERWEn0IVENGgZ7yeFIlUkYJbGRmAhYtJRqerHhan1lSuYMs1i7gLjZ9hT6TF6sksu+gpdnZ388+ZbS7Zpbu4PwMqVq4rWrVy1igHNzSW3vfAr53LpJRd+9IEaDD2UdP8MulChCnweQs2Ab7Yh8ndU6yKwhYMlwglKR4K4QW0Zv8CeUqAizrp+fwoV7CWqpbH8bSHQvEjy1Fn5gsnYyzkinMCCkekPR/28mYWvdDD7yVZ2OqYBPxFdbM9KMf2RVrpavOMpM8mHmXEVC98tdkrpbIWrvpTlkE9J9j3RpqaX1gBZwqFG5LBkcvp+P3pJH2O5ZC+VfHpCfYnfc9JaX+Pl65P8ZPRChGHIfvNy5h8bgRQ6jsj2ficJKYG/TWB+0168+Tz8+s/mxbEn0+0EFSEEqVSqckMgm9X1WkaNHMHnP/85vvO9H5PL5Uq2r6nJxLaL0tWVpaGh2Avf5/obbuKmiBBUX1/HC888WtU4DYYdAVljedqP0G7iZ4pVys9y4S8LnW8lCltGNCPK/6W8pHC+P4SI5QFxlSiKEPInWMszrcgiJ1FNcaZb7fhqFSaWU1pIGHlQLTOeaGXha+2MOLBOOw97zjTCEsx5tp337mxBSHBzYFXxCMt3wQdPxZ9FfQbBIacJ9jgSMrUuHRuy1KFrGPkaFFmiNk/Uebm8KKIFwLw39VuiWCjQJh2FKvIL8rVRvmCkx+FPJEmyiCuiWW/jDQSQIhQ8tHNt+LeIbOEXK4ymxF+9RvK1nzQwdWa3m8oMm5Fud3X323dvbvnn36pqe8LJZzFn7jwu+9F3mTz5XR5/4umy7Ts7dXKldLq4EFUmkw7WJ5HL5coKQQbDjo6bdZDSr7Gjl8Uzz+rcIpanu7eI1+DxCervQEEeFAVK52lRSteiDE1HKsi0qhTkEdSUCFkWuKRlXJPhCzaF44l+HveJBu78wiKmPtjKmMPrqGmyaF/jMOe5dtbO1c8G5cLs13KMO7S0pKKUFoYe/E0bnRtCIWHITnDuLwV2CixbH1tDE36ikSDeSEIJ805U4HAjooU/3WtBwcLxfGhUMJ74sYY+NzaqKAon9B3yhSYVJPgrhV88MWbSQ5AiHhGU5J9SpInx1GKPP5fix79pIO9U9mkxbN90O0Flztx5/PCyn1fVdsXKVRx4wH4cftghXPL17zJ0yOBgnW1Z1NRkGDpkMOvWb6CtrS0w+TQ392fZsuWxvpr79+e99z/YbMdhMOxo9N6vP1oY0X8XTfjKSz6mPEfWKswjRe/fBdoRF8jgBIUH/RVlxAQyVuELh4pvX2Y8J/1hAAtfauPDB1poW5W8zYs3tjPu4N6eGiF54n3munYWvx8KS0LCZ38gSKVBWgVeJAUnKo+FUE6RsCI9zVWYy8Y7OqUNKQ6CGnLUSCfwt7G8FPuhBklfI+n15w2AaBlD/5h04jq9LElrFdPTKK1ZSQNpP+dJmRugSPei/FMhWLla8J3/7cWUqd1u+jJsIbrdlV61ajX33vdg1e0HDx4EwDVX/6Fo3aBBA3n6iYf49eV/4OZbbmfqtOkA7L7rRKZMCYWSAc39GTx4EHfcde9HHL3BsGOSGVFPZlAdJGgl/AnRkmHeDVFsBUikVDNfc2LjxKovV8LCobCETnX5XvT+6gfY7HFmPbufUc/L129g1jPF+Zr2OjmNVVAIMDbJuy5zXo8LSzvtBU0DfJNHkqbJEygKvEcKj6NQSAk+K8gIJ0iTL7yU+tFdSMJrFAiKSpvJlCLwHBFFTtD+9qH5Rv+tP7v+4ftnoIqTHZGbQMDchZLHn67hpTfTvD/Noqqbx9Bj6HaCysby6mtvcPGl3yla/oufX8aSpUv56/U3MmPmLABmzZ7D7Nlz+fSnzuA/d9yN6+oHwuc++ylc1+XRx5/cqmM3GHoKvfbpj3LcxEJ6ssQEunEURxEJochshJACqsCnxeunYgyMvz/dWkjtv3HIRb3pXO+y6G3tZ9I0WDLuUJu9TkkjLLCUSzxFnA5tRigmHpnirQdCYWXIGHDyCssWkX3543NJ4wTCmX/sMmKucgPPjuRzLAReUUIRCDwyto8ETQzEtGPJl84rMBlNeVPQ0k+N70fruMoXdMpoVBR0ZeFvN9fz4mtp5i7Y7qcqw0dgu7/6S5cuY+nSZUXLf/zD77Bq1RqeevrZ2PLf/fEq/vqXK7jxhmt4eNLjjNtpLP9z9qe58+77mDNn3tYZtMHQw5C1YZxHkjYlidLVkMNtQ38I4Tnqhmv9mjnV4WkjEjeoshPl1+/RzjFCKY74Rm/u+OoqjvhyDbselcZ1FNLyxCqhBYBUgSDkONB3WDxwN5UBywqz4wpCd1hJGLKtnY/dmH+PDnd28YJgyhyNIo+FTT7IKhskoyv4O9yi3BnSJqK0b/5JMnMFjrFK+6MgyOOSLlN4UEeQC352eW+eedHU6zH0AEFlY3n2uRf42je+x9cu/go//fH3WLNmLdffcBPX/PWGbT00g2G7Jbu8I5iYdTZYvTzJRBCuSZ4cNVpjkJb5mKATZkERVWtCAsdPL4NsqJXQv11EBaFJCw51VlY79yovnb5Q1DXAuX/rTU2tt48CjZLWqLixfCJCQLYjHPuhZ8ERnyIYo8D34wn7cBCgdKh2oT9NdNwOElGygGNolim8LsnXSVdI1sFNUUEkjPxJobCp5G+i+7CE9Bx3BY5yg6Rt0f8BPpxm89ebGnhjcnHQg2HHpMcKKkcfe0rJdU89/WyRpsVgMGw6La+tZMBZIzfKtBMLelVR7YhA4FJr5WLJ1ZQXKux6/hLVG32iDqOCvJIFfi3esiKH2nBSrxFZT4gITUAgcJUiUwuiZJY1hZ/AP9CQWIIZL+cBOOwsOPYL3igiSe3i51ELdQ4SW+U9P4+kffn6p1JCXOX8KUl9uihS4Ol19F5S6Pw01VzvaK5Z3/nWRafX98slACgHXn87zaU/btroURp6Nj1WUDEYDFsPpzVP67tr6L13X6IGA1+DUfoNn1g4M4Al8tRaeiKPZoC1hDa9CBFNSFbOOKHXZ2Q+JhTFC+7pD3kEQgkvY6wKGmdEHls4oZCSsJccNmlVmDwuPMbofoLfLvQbAp/4vH+MESElqZtAWNG5akqjYv4ghX2kyGHjFIxV1+yxlO/7QiBaeQYm0l4ul+Q9AmUSuhUek0CQRmCJglHasN+e+TLHZthRMYKKwWDYLKx/YTlN+/T1IkTA15m4aGfLUiYe8CNv9Gdf2xEVUrSwEBF6vN8OwqtXU+id4felt9NupP7kq3O9LHunnRWTO6jtZ9O1waFjVZYDz+tFXV8bBWRkzktoVi4VvB5MXlmkKggQ/vgkin4jBMNG+tl1fUfbSgivRs9GlFiO7N/GIeOVIygUh7QY5pKKaLZ8wcmtoInR2pHyWEUOtslHa1l6NCaqxxDFCCoGg2Gz0DZ1vc7YagmkEEGujjCDbLIwYRUkZiuMzLGi1Y1F9LevZRAFxfX0xG9Lh4zIxwQcP8+Ig2DQHnW89eeV5DvDbTtOraGpf5gR1Y92KT91amGsnL+NwBOWvPW5Thi+i7/15p2WZZHg5mLjUidyJMeFe8IWMghfJtJD1DyTNM5Ag1NCq2IRLyLo9yOJp8t3HLwMs0ZIMcTpMUUJDQbDNkbBsjvnB3/4/iVCCBwCQ0+4HlUgpAgyBc6zcf+SJPQ0LwJdifcjFBmRLyHgoPO62NA0MsWACWmahtvU95MMHOsndPd0Qio0MJV33S3to+KH/vr7dnKKeZP9zLAKoYqFLAsH2/uRuEFbWXYUel0ahzR50mSpJUuDyFErHZSQQYhyUpA2vtNuUY9aGCknPuS9NoU9BsUGI0h0bZ+wnV5vWXDbPTVl9mLYUTEaFYPBsNlY+8xy6sb1pnGfvpGlWqNgSdfTsnhxHgV+EgIKcnmEafHLo3CJ1qwRQX8lt1UutdLhxF/0CxblO12wFdH089HIGz96JbnL0hqjNHlCDxfFhhUOp1wII8bH9+Efb8zEBUilBZQ8eEJLqf1Dhhy20KKIr5mKj0p4Ogw/n0q8j1JikPKcmEvnU8GL+tKTShCWnKhhiY5Gm+EQcM/DGR592oQjG4oxgorBYNisLL5+JvlPDqffsYNJBYKHNylGJuXQqKKp8aJ8/PWWcCtoEMohcJTOqlq4XOBSYxU7bdo1IkiIVljs0CepZg2eBkSHP4vIMpcUnukJHbUjcRk6LMeIYV7PUZNXZOviekM68ib5fOh9ZcgHkUtR0xgUmqX08mIjUFImFP9KCRz81PrFwoqvH3FQpJAlHWtDU5KKCTN/v7WGa26qS+jZYDCmH4PBsAVYftdC5vz6A5QT927QeoWo+QdSwqHOyiVmSq0u3FkUTeC2cEjJUI8R/qigIGFS3hCAvLJQifJRkjeJblgj86SFQ4YsGbLUkCMl8lhS78fCpU50US9zWEKA8PLDBqalSmYd38SVvCZdJKTEt4vmjwlHXnwsVtkxxE1gvtBiifg+S+l89Di1EBP1TQF4450URkgxlMIIKgaDYYvQOa+Naf9vCk57HqW0U602x+hwZCkgLfOkZLF5xxJuCWGhEC+La2R7W+SplTo9fegn4/+4FQQgPbG7pSZNVWweqRFdpEQe2+vbllojYwlfOHBJCyfxYauQkfNSxlTltY4LF9pMFfVdKbd9fNzFDdOVzGyRlZZIGK/SE0oaQQZJComFwEaQRpIRVqKmxXVh2iyj3DeUxggqBoNhi5Fd3sXUH7/HsgcWk+9wcWP5Syq4p1bhmwIE6eR9MiU1JtW+sxcKBNE1XhZbXGpEF72sTjIydEEtdNqN7rOUBsdFRM5LdeMTXhRPnchTQz6xhlHSvkr1l8LBLnE9tOXJD6NODkX2l8+eZeM6AiEElhCkhMQWsijqJ8rLb1psaDFTkaE0Row1GAxbFKfNYfmDS1n+4FLS/dPs+qOdsJvSIASuEtgyeYKslNYewI5pAVSsAGIy1YkqpdLz+06q9bIrtk/fj6RoWxHWO44uj3ijoE06eSBefiBp79KLv1HxxeUdhxPx6wm5ZLwU/6Xz3Ogd6JT5urZP1HDjH4HjwvMv1bLH2OKK0ok9K4XjCH7wy14bM3DDDogRYw0Gw1YjuyrL+7+YQdfSTk+gUDolfoFc4KrKj6Z42LKeeGtltuw2ChL3l9R38cY6OqVW6H2ImIDgT/zx7LICRZo8loybZoKaPn60jnQqRDiFieGi/fuffU1PaVTR5xocMkJ5B1EYhB1qUKRQCAXCBT94WyB0mDHCi9zRp+fhSXWs3yC801VmQEr38aPf1NHWbqYhQ3nMHWIwGLYquXV5pl85m/XvrdMRIF5iOI0nvPh/Jc51eqGMTKxaSMkF1YT9dn7mkGj2kHwgBCVPpH4IcHzfekKvIZtgZokLEHFdia5Y7If2QqFZSPuZBCHF4ZHHzgcQSX2visxdgfGppHAQr/9T40UjCRQZFDUCMt6PX8enRkCthDSQEvDb3zXy6usZXFf7lfjkvRxxf7iykdVrLB54rBbHSQ5N9nFd+N8r6nniOZM3xVAZY/oxGAxblbohGfa8bDR2vUSKeJiwG7yxa02LjMy/ugaPXmB7Sd1SOFjS1TV6IiG5QrlFJiAhdJ+O5xMSFjoO20nPlBMU9vO0KDYOaeEEfbpelWLpRwCLUkYl3cBREplQ9VgpsDwxQwh0n+jMuX74sBa04ualVFEafUEegR1oavzR+MHFrnbw9c6w9I41kyCsCcCOClPeMdbXwWU/78MZp7Zz5mltDB3i4Lrw9ttpbrujgcnv6hwot91dyynHdtLUW2EXzDBKwfIVgjPP60Nnl3lPNlSHEVQMBsNWZcLFw7HrZCCERF+8LZTnm+KbUgiiSaTQk60vr9hCCw/FL+6qIHFciABs/DwprqdS9lPmFwg7KBo8X5TikF/PO0W5YY6YkugaPamEwF3hCUHRvwVJocq+wKHIkFwA0R+FjGikfC1NcK4JRZhUiUihMMkdgbCoFBx0QCcPP1zPXffWc9e9dWQykM+D48Q7WL1Wct43mvjVj1vYfUI+8DVyXJj0ZIZfX91AZ9dGOdQYdnCMoGIwGLYavcfW0jC8hqAub1JkTiAsAJGqxWmZ16KFt75UpIsdyycS+o4IFFKqSO0aL0OrKE7+LnGol1ktHpTxG/ENSps67VpeocBqSJPzktGVaqFrHlmiUj4UrVmxqhh0kDNFQjod7VPQ1ZW0hWbRUosvXtrELjvlmTguTy4Hr72dYuXqSuULDYZijKBiMBi2Gg2ja1HKRVbU+quIDiEMGLZwvQrCOkKmWEJQEe2BG9YS8n03hFe3xtPS+E6kvg5FeDlPUkLXxtE9ev1SqDkJ7T6uV3W4VFBzWL3Z304LKXWivPOvv71PaSGlUsRT6MujhZkqdutvqbRPyYyZ6eo38pg+y2a6yZFi+IgYI6HBYNhqqLwqcHgtRRg5opRi6QvrWTWlHd+sE63om7y1TsEfLhCeasTrNxbPq0OCpVBkpE7clqRGSS7mRxD6oxLWC1xqRY56mSMlXNLC1U6xuLHoodKEuWKUV9wxXBonQ77sefVNaTala/qU3FbAw4/UbeRWBsPmwYi6BoNhq7FmSmvVbZ2sy+LH1rD42XV0rszRvFstA/ao09oVr66Oi0CqeKZZpcAqk/Qt9DEJ680opZDCTagNFN3GK34YCRUOfUlEpEdfe+FSL7PEZCKvZRqnjHYkThpPePL27yAK3jC1305HTlKbdnFdXYnYPxe+pieN0kndhBdL5CuZqhjHVX9uZOlSM10Ytg3mzjMYDFuNrtU5VE4hUpVnx5m3LmfpM+uCv1e+38HsR9Yz9sRGT7Dwo4QUMjLpOkC6YvfRonxe+K+Xtr98en2F9CKAotWJQ+OU8MQJdLg02jE41osAqdxKGe0AqCGHLRydsj6yNx295NcLEtx0ZQ0vP5lh8NA8x5/Wwf6HdpFKwfy5NpNfTXHhBRtCJ1mvDEAeHXZc5hTR2QU/+1lf3n3XhBEbth1GUDEYDFuVlW+uZ+BBTSXXK6VQLix9dl3Ruvf/vZq1czoZd1oTvYdnCAwawiHlJUwrdo6tjCWSDDfFpHCokXETS+ipEvqg2OS9kOkweklF2lolzEuFSJHs9KpNOC45JbjpyjpeflKHBi9dbHPTtb246dp4ttfDD+1gwsRcWPPI68WviFzYt//huusajZBi2OYYHxWDwbBVmf/AKsp5mAghmPb3xSWbLH65jWd+sJgHvzCHB784hxWvbSAjHNKWS0rq/CnVFTQM9kiZMoQRFBkRFjuM9+D/Dp1WhYiHGYvI7zylKjSH+xK4Qf2dwrEF+3dUIKSU44+/a2LDej97bZhjRQdp45fy0WPzUtvcc089kyYZvxTDtsdoVAwGw1alfUmW6TctZZdzBwNhBlOlFEII5j24kuUvbajYj+tNqFPu2MCI/TI6hWqESpaVqClFCnCUCFLdJ5EiX3Kd35+/tSXCKJ/kIWhfk1KFAEGQIVfWj0UISKdgxJg8C+aUf5QvX2ZzyYXNfObsVj5xXDuZjHYg7uiEJx6rYfmSFPvt10V9vcv8+TaPPFLP9OkbH+VjMGwJxLiJe2+sA7gBqK+v5+3Xn2fv/Q+nra1tWw/HYNju6DWmhmHH9qPfx+oRUrB+ejsLHl7NuuntG93XiANrOPLbfSCmwUhKaBYGPfvBxynyQbi0xNXp6YskHK1NSZGUYK4Ql3rRRaqM0OP3qftLzg7bS3RW5XA7f6bgZ99s0uHaVaOwrOJkbQbD1mJj5lCjUTEYDNuEljmdTL1u8Wbpa8Grnayek6Xv6FQss6rO+RZXreicJlr/YUeEFNDOuSnh+vWEI9sobNwqMrvpzLHVZYDTolSx/T1akbkyY3bOs9ueOaZM3hgNiMCpLs+cwbDNMT4qBoOhR/D8Vetw8zrrqgzcWyUpkSdNjhRaI2LjYos8aZHHSngCWp6mI0WOjMhRK7LUyhyUTOjm4wkYMkzOn9TGxiEl8qTIB462hT92US2f0vt0HTj8E51VtjcYtj+MoGIwGHoELUsdnvjFGpQKNSA2eWyhsKUiJRUp6WJ7DrdJNYJ8046OHtIhy1KEFYyj/xduC376fi/5m1KxtraXiTYt8loQEg5p6XrlBFTkh6LqyKWQ6MJ/ffttfKSTwbC9YAQVg8HQY1gxLcdzV60nSBlfTT0bL/uZRJGK1N1RkXU2LinhRrQ1ccECIBXJrRJ3rVVYOKS9StFBiLAIW0ajhSQuld1N9L4toXDysGa1eZQbei7GR8VgMPQo5r7cRbZ9PZ/4fiNWSusrZJkIIB1K7CZUJdZ/pKQbhBJrLYuDq6K1l5N9SiRh+jdfACpVhDGMQPI1KqGYU7xJXOti2fD8EybXiaHnYsRwg8HQ41j8TpZ/fWElb97ewrql5SsgIyBdItInJZyYL25QTdgzC9kiyYzkJfcX+kdXKq7eOdZW2vzkqkKdTYiFqzPzuvDOGynefydVXecGw3aIEVQMBkOPxM3Du/d08N9L1/LmnR3esui0rz/7fikKLRzoVSpwaC3UsriqclHFDDlsoR+w1WbK7eqEW67KcMlZvXjt+RS5rMBVWiPU0ipYu1pXXLY9ISWfg6cn1XDFLxo3MjTZYNi+MKYfg8HQ43n9P50sfC/PbsdnGDzepqGPQAhdYVkKPzGbdoLNK1UQnBzHxcLCKZlQTuKSllo48T1VKiWfUwrWLhe8/LgOMb7u8ga9vVRICU5ebzxgsMOYnXO4jmDqlBQtG8y7pqHnYwQVg8GwQ7D0wzxLP9QOrUMmWJx6WR1WLTiKWGI1AbHU94UoBA6yRGSOopZcbImgvJ+MUtqXJZPgZqJcgRPZzYqlFiuWWqUP0mDogRhx3GAw7HAsmepw41daWPZhFturySNxSZH3ond08rdSWhCFJI8kHzO5KC+rbXF73/G2yGTkCSlCQet6Y74xGJIwGhWDwbBDIiSM2kVhS514zcLFQeIoicKveuxGUrJF0cYiGWhV9PY2Lg4WQuUL0t8L8ggstIdsIACJsHrxK08Zh1iDIQkjqBgMhh2SYeMldlpLDH4eEz+SB0DieOHFhYYgvY2Fq7UvReHJCgcZEWL8rQRp8jjeZ/9/Jw+rVwheNoKKwZCIEVQMBsMOiYgYvkViHhS9TKLzqLj4Qo3fXutdrKKoHpHg4aKjiIQAy/OudRQIKZgzXXLDb2vp6jCmH4MhCSOoGAyGHZJls1xcVyE9G008MqdAhyJ0DaA4oqhdMp62xtOwCC//yZK5ghuvrGPRHOMcazCUwzjTGgyGHZLWtTD9FRfX0eadpKy05QURlag7CZeHafFryMX6lxKGjnJZt8poUQyGShhBxWAw7LA8el2e1UsUrquKErm5SeE7MURCiLKvPXGwcciQo4ZCx1qNZUGf/tVoZAyGHRsjqBgMhh2Wjha4+Xs5nrzJZcVigpwlylXk3VJaFb/WjlMgyujl0nOyTVWROr+91WhUDIZKGB8Vg8GwQ5PthDcedHjjQf33wFEwdi9BnwFw6EkOLhZ+7lofHbasIoncdB4WXdfHz5tSOnW+68K8mRarV5h3RYOhEkZQMRgMhgjL58HyedrHZP8jFDUNDk4geGgHWj/+R3rRP36EkO9eKxA4CCxVrFHxzUv33ZLZWodkMGzXGHHeYDAYEhE8+m8LSyhSOFiek6xEhy7buAjhpYPzhBEHi7wLXa1urM6Pivi/tLfC9ZfX8eFkkzfFYKgGo1ExGAyGErzyiOCYT0PvvmAJVRCiHEb1BH4sjksuK7jyR3Xku2D/I3L0aXapr1csXyxYNNdi8isp8nnjm2IwVIsRVAwGg6Ekgut/YvGdPztIi8BNxRczJA5WxOf2vdclD/8rxbKFWln90O3GvGMwfFSMoGIwGAxlWLFIcs0P4byfODQ06YggbTQXuFhkO1zefNri0dssWtcba7rBsLkxgorBYDBUYMF0yS/OFex2oGLkeEW6RrF+NSyYIZnzfopclzHlGAxbCiOoGAwGQxU4ecG7LwrefXFbj8Rg2LEwekqDwWAwGAzdFiOoGAwGg8Fg6LYYQcVgMBgMBkO3xQgqBoPBYDAYui1GUDEYDAaDwdBtMYKKwWAwGAyGbosRVAwGg8FgMHRbjKBiMBgMBoOh22IEFYPBYDAYDN0WI6gYDAaDwWDothhBxWAwGAwGQ7fFCCoGg8FgMBi6LUZQMRgMBoPB0G0xgorBYDAYDIZui72tB7C9U19ft62HYDAYDAbDdsXGzJ1GUNlE/JP8wjOPbuORGAwGg8GwfVJfX0dbW1vZNmLcxL3VVhpPj2PAgGba2tq36Rjq6+t44ZlHOezjx2/zsWxtduRjB3P8O/Lx78jHDjv28fekY6+vr2PFipUV2xmNykegmhO8tWhra68olfZUduRjB3P8O/Lx78jHDjv28feEY692/MaZ1mAwGAwGQ7fFCCoGg8FgMBi6LUZQ2c7JZrP8+ZrryWaz23ooW50d+djBHP+OfPw78rHDjn38O+KxG2dag8FgMBgM3RajUTEYDAaDwdBtMYKKwWAwGAyGbosRVAwGg8FgMHRbjKBiMBgMBoOh22IEle2cgw7cn5tvvI43X32Ot19/nrvv+DcnHP+JonZHffxw7rnzVt57+2WeefJhLr3kQizL2gYj3jL84n9/wvQP3uK6a65MXN+Tjv/AA/bj17/4GY8+fA/vvPkSTz56P7/835/S3L9/Yvu99tyD2275B++8+RIvPvcYl/3oe9TV1W7lUW8eUqkU3/32pbzwzKO8+9ZL3HH7zRx80AHbelibnd13m8hPL/s+D91/B5PfeJFnnnyYK/94OaNGjihqO2bMKP5+/Z95+40XeO3lp/ndb/6PPn2atv6gtyBfveA8pn/wFg/e99+idT3p/o4yccJ4/vqXK3jt5ad5582XePC+//L5//lsrE1PPfZCTGba7ZgzTz+FX/3iZ7z0ymtccdVfcB2X0aNHMnjQoFi7ww89mGuu/iOvv/EWv/j17xm3805cdOGX6de3Lz//xW+20eg3H7vtOoEzTjuFzs7OxPU97fi/9+2v09jYm0cff5J58xcyfNhQzjn70xx55KGcftbZrFq1Omg7fvw4/vmPvzJ7zjwu/90VDBo0gPO+9HlGjRzOV7769W14FJvG5b/+Ocd94hj+dcttzFuwgDNOO4W//fVqvnjehbz19jvbenibjfO//EX23mtPHn3sSabPmElz/378z9mf5p67buUzn/sSM2fNBmDgwAHcevPfaWlt5U9XXkNdXS3nnft5xo3biU999gvkcvltfCQfnYEDB3DhV86jrb04XXxPu799Djn4QK675k98OHU61173d9rbOxgxfBiDBg0I2vTUY0/CCCrbKUOHDOZnP/kh/771v/zq8j+Ubfv9732T6TNmct5XLsFxHADa2lq58Cvn8a9/386cufO2woi3HJf96Hvc/8BDHHjg/onre9rx/+Z3V/DW2++gVJhZ4IUXX+bWf/2dc87+NFde/ddg+be/cQkbNrTw+S9dEKSrXrR4Kb/6v59yyMEH8tLLr2718W8qu+++KyefeDy//f2V3PjPWwC47/6Heej+O/jut7/O5845bxuPcPPxz5tv5bvfvywmaDwy6XEevO+/XHD+l/jeD38KaE1DbW0tZ376HJYuXQbAe1M+4J//+CtnnH4Kd9x57zYZ/+bkB9/9Ju++NwUpZZGmqCfd3z719fX89jf/y7PPvcjXv/X92Pc8Sk889lIY0892ymc/80ksS3LVX64DKKnuGzt2NDvvNJY77rw3mKQBbrv9TqSUHHfs0VtlvFuK0049iXE7j+VPV1+buL4nHv+bb00ueni9+dZk1q5bx5gxo4Nl9fX1HHzQgTzw0COxmhr3P/AQbW1tnHBcsYmwO3P8sUeTz+f57533BMuy2Sx33X0/e+/1MQYNGrgNR7d5mfzOe0XakPkLFjJz1pzYNT72mKN49rkXAiEF4JVXX2fu3Hnb3fVNYt999uK4Y4/m15f/sWhdT7u/fU456Xia+/fnT1dfg1KK2toahBCxNj312EthBJXtlIMP3J85c+dxxOGH8NxTjzD5jRd57eWn+calF8Vu6onjxwMw5YMPY9uvWLmKpUuXMWHCLlt13JuT+ro6vvvtr3PdDTfFzB1RevLxR6mrq6W+ro61a9cFy3YZtxOplM3770+Ntc3l8kydNmO7O/YJ43dh3vwFRYXM3pvyvrd+3LYY1lalf7++rF23DtDV2/v378f7Bfc2aK3K9nZ9C5FS8tPLvs9dd9/HjJmzitb3tPvb56CD9qelpZWBAwbw6EN3886bL/HW68/z85/+iHQ6DfTcYy+FEVS2U0aOHMGgQQP5zS//H3ff+wCXfvN7vPDCy1z81fP51jcuCdo1N2sHy5UrVxX1sXLVKgY0N2+1MW9uLrnoK3R1dvLPm28t2aYnH3+UL37+bNLpNJMefSJY5h/7ipXFVb5XrlzFgAHb17E3N/cveR2BHnMtS3HqyScwaNBAJk16HIABFe7tPk1NpFKprTrGzclnP3MWQwYP5so//zVxfU+7v31GjRyBZVlc++creOGlV/naN77L3fc8wOc++0l+86v/B/TcYy+F8VHpBgghqn6g+PUd6upqsSyLP1xxNTf842YAHn/iaRobe/OFcz7H9X+7kbb2dmpqMrHtonR1ZWloqN9MR7HpbMrxjxo5gs9//nN853s/JpfLlWzf3Y9/U469kH332YtLLrqARyY9zquvvREsr8l4x55wfrq6uoL12ws1mZqS1xHCa90TGTN6FD/7yQ95e/K73Hv/QwBk/OubTbq+4Tkp9/3orjQ1NvL1r32Va6/7e0xLGKWn3d8+dbV11NXVcvt/7uJXv/k9AE88+QzplM1nP/NJrv7zdT322EthBJVuwH777s0t//xbVW1POPks5sydR2dXF/V1dTz0yGOx9Q898hiHH3YIEybswptvTaazswsgUBlGyWTSwfptyaYc/2U/+i6TJ7/L4088XbZ9dz/+TTn2KGNGj+IvV/+BmbNm8ZOf/SK2rrPLO/YEQSiTyQTrtxc6uzpLXkdgm1/LLUX//v24/tqraGlt5Rvf+j6u6wJ6QgJIp5Ou7/Z9Tr759YtZv34D/77tPyXb9LT726ezS0cvPvTIo7HlDz78KJ/9zCfZc889ggjHnnbspTCCSjdgztx5/PCyn1fVdoWn5l2xYiWjR40s8s1Ys2YNAI29ewOhWri5uT/Lli2PtW3u35/33v/gowx9s7Cxx3/gAftx+GGHcMnXv8vQIYODdbZlUVOTYeiQwaxbv4G2trZuf/ybcu19Bg0ayD9uuIbWllYu+Oo3isI3/WNPMok0N/dnxYpitXF3ZuXKVQwcOKBouZ8/JkkNvr3T0NDADdddTa/eDfzPF86P3QMrIvd2Ic39+7N23brtUpsycsRwPv2pM/j15X+M3buZTIaUbTN0yGBaI9/tnnJ/+6xYsYpxO+/E6tVrYsvXrFkL6Gf7woWLgJ537KUwgko3YNWq1dx734Mbtc0HH05l9KiRDBw4gEWLFgfL/Rt3zVp9U0+dNh2A3XedyJQpH0Ta9Wfw4EHccde2D1/c2OMfPFjnibnm6uKw7EGDBvL0Ew/x68v/wM233N7tj39Trj1o1fiNf7uGdCrF2ed9NfDTiDJj5mxyuTy77TaBSY+FviuplM2E8eNi/izbA9OmzeCA/felvr4+5lD7sT12A2DqtBnbamhbhHQ6zXXX/IlRI0dy7vkXMXv23Nj6FStWsnr1GnbbdWLRtnvsvivTttPzMXDgACzL4qeXfZ+fXvb9ovVPP/EQN99yG1f/5foedX/7fPDhVA495EAGDhzA3Hnzg+W+38matWt73He7EsaZdjvlkUn6RvzkmacFy4QQnHnGqaxdt473P9De4LNmz2H27Ll8+lNnIGV4uT/32U/hui6PPv7k1h34ZuDV197g4ku/U/SzevUaprz/ARdf+h2efvYFoGcef21tDX+77moGDmzmgou+zvwFCxPbtba28sqrr3HqySdSX1cXLD/tlJOor6/f7o790cefwrZtPvOpM4NlqVSKM884lXfenVKkMduekVJy5R9/w54f24NvfPsHvPPulMR2jz/xNEcecVgsNPvAA/Zj9OhRPPrY9nV9fWbOnJ34/Z4xcxaLlyzl4ku/w11339/j7m8fX8iIPtsBPnnW6eRyeV5//c0ee+ylEOMm7p2cTcbQ7bnp79dy4AH7ccdd9zJ9+kyOPupIDj3kQH7681/GEj0decRhOhXz62/y8KTHGbfTWP7n7E9z1z3387Of/2obHsHm5anHH2TmzNl89ZJvxpb3tOO/5uo/cszRR3LX3ffx2utvxta1tXfw1NPPBn9PnDCe/9x6I7Nmz+WOO+9h0KABnPvFc3jjrcmcf8HXtvLIPzpX/vFyjjn649x8y63MX7CQM047md13240vffmrvPnW5G09vM3Gj3/4Hb74+bN5+pnnEt+OH3hoEqA1iPfddRsbWlr41y23U1dXx5fP+zzLl63grM98frs0/ZTiXzddT58+TZxy+meCZT3t/vb51f/9lE+edTqPTHqcN958m/3324cTjv8E1/3tRv501TVAzz32JIygsh1TV1fLN79+MSccfyxNjb2ZO3c+N/zjZh58eFJR26OPOpKvXfwVxo4ZzZo1a7n3/oe45q83kM9v/ym2fUoJKtCzjv+pxx9k2NAhiesWLV7C0ceeElu2z9578t1vX8rECeNpa2tn0mNPcMWf/pKYkry7k06n+ealF3HKKSfS2LsX02fM5Ko/X8eLL72yrYe2WfnXTddzwP77lly/y677BJ93GjuGH/7g2+yz157kcjmee/5FLv/9n4p8HLZ3kgQV6Fn3t49t21z4lXM584xTGTCgmSVLlnLb7Xdw8y23x9r1xGNPwggqBoPBYDAYui3GR8VgMBgMBkO3xQgqBoPBYDAYui1GUDEYDAaDwdBtMYKKwWAwGAyGbosRVAwGg8FgMHRbjKBiMBgMBoOh22IEFYPBYDAYDN0WI6gYDAaDwWDothhBxWAwGAwGQ7fFVE82GHYAdp04nrM/+yn23XdvBjQ3I6VgxYpVTH7nXe574GFefuW1bT3EHZanHtfVswtLH1Ri33324qiPH8Fuu05g4oTx9OrVwD33PciPLvv5FhilwbDtMIKKwdCDEULwg+99k3O/eA65XJ5XX3+Dp595nnw+z/BhQzniiEM57dSTuOrPf+Xa6/6+rYdr2AjOOvM0zjz9FNrbO1i6dBm9ejVs6yEZDFsEI6gYDD2Yb379Ys794jl8OHUaX//WD1i4cFFsfSaT4ZyzP01TU9O2GaBhk7n1tv/yjxv/xZy589h9t4nccfvN23pIBsMWwQgqBkMPZcSIYZx/3hdYu3Yd5194aWI13a6uLv5x0y2kUqnY8j5NTVz01S9z9MePYMCAZlpaWnn9jbe45q83MHPW7Fjb3/zq55x5+ikcfdypHHvMUXz6U2cweNBAFi1ewjV/vYFHJj1OKmVzyUUXcMrJJ9Dcvx/z5i3gD1dczfMvvhzry68avPteB3HpJRdy8knH069vHxYtWsJt/7mTf9/236JjsCyLz5/zWc447WRGjRxBLpfnw6nTuOnmf/PMsy/E2p5x+ilc/quf88PLfs6KFSv52sUXMGH8LnR2dfLscy9y+W+vYN369UX72GXcTlz4lfPYb799aGpqZOXKVTz9zHP85Zq/xdoPHTKYp594iHvue5C/Xv93vv+db7L/fvuQSqV45933uPz3f2L69Jmxtj7TP3gr+Pzna67nL9f+rWgcUd7/YGrZ9QZDT8EIKgZDD+XM00/Btm3+c8fdiUJKlFwuF3zu06eJ/972T0aOGM5rr7/Jw5MeZ9jQIRx37NEccfihnH/h13jr7XeK+vjR97/NHnvsxjPPPo/ruJx4wrH88Xe/YsOGFs45+zPsNHY0zz3/Ipl0mpNPOp5r/nIFJ57yySItD8BVV1zOhPG78PiTTwNw7DFH8dPLvs/QoUP47e//FGt79Z9+xzFHH8ncufO49fY7qaut5YQTPsF111zJr3/7R27+121F/R915OEcecShPP3s80x+5z3223cvzjjtZEYMH8bZn/9yvO3HD+fKP16O6yqeeuZZli1bztgxY/j8/3yWQw85iE9/7ots2NAS22bokMHccdvNzJw1m7vvfYARw4dxzNFH8q+brufEUz7J6tVr2NDSwp+vuZ4vfv5sAG6+JRzn62+8hcFg0BhBxWDooey9154AvPraGxu13fe+/XVGjhjOdX+7kT9ddU2w/PAHDuGG667m17/8fxx/0pkopWLbjR0zilPP+Axr164D4O77HuCu//yLK37/a2bOms0pZ3yGjo5OAF586VWuvOJyvnDO5/jVb35fNIZRI0dy8umfobW1FYCr/3I9d95+M1/6wtk8/MijgTbhtFNP4pijj+S119/kyxdcQi6XB+D6v9/EPXf8m+99+xs89fRzLFq0ONb/x488nC+cewFvT34XACkl//zHXzlg/3352B678e577wPQ1NjI737zC9auXcfnzjmPJUuXBX2ceMKx/OkPv+HrX/sqv/x1/BgO2H9f/nDF1dzwj9Ac841LL+Lir57PmWecyg1//yctLa385dq/ccbp2om2kgbFYNhRMeHJBkMPpX+/fgAsX76i6m1SKZuTTjyOtWvX8dfr/xFb9/wLL/HiS68yauQI9t7rY0Xb/vVvNwZCCsCUKR+wYMEiGht786errgmEFIDHnniKbC7H+F12ThzHtdf9PRBSAFpbW/nr9X9HSsnpp50cLD/D+/z7K64OhBSApUuX8c9/3UoqZXPqyScU9f/QI48GQgqA67rce782w+y+267B8tNOO4levRq44sq/xIQUgEcmPc77H0zlpBOOK+p/4cJF/P3Gf8WW3XX3fV7/ExOP2WAwJGM0KgaDIWDM6FHU1NTw2utv0tnZWbT+tdff5NBDDmTC+F2KzD/Tpk0var9y1SpGjBjG1GkzYstd12XN6jUMGNCcOI43355cvOwtvWzihPHBsgkTdqG9vYMpUz5IHCvA+PHjitZ9kODfsWzZcgB69+4VLNtzj90B2GOP3Rg+fFjRNplMmr59+9CnqYm169YFy6dOm1GkcVrmCYy9e/XCYDBUjxFUDIYeyqrVqxk7djQDBw5g7rz5VW3T0NDgbZvs07Jy1SqvXX3RutbWtqJl+bzWcrS1JaxzHGw7+RG0atXq4mXemPwxAjTU1wcCRtFYV64K2hSNNWE8juMA2gzk09jYG4Bzzv5M4j58amtriCiTqu7fYDBUxggqBkMP5e3J73DA/vty4AH7Ve2n4ptb+vfrm7i+f/9+XrviiXhz0r9/P5YWmFr8McVMQm1t9O1bYawJQkO1+NuefNqni6KdDAbD1sGI9gZDD+We+x4kn8/zmU+dSZ8+TWXb+uHJc+bOo7Ozk91325Wampqidgfstw8AUxPMPJuTfffeq3jZPnrZh1OnBcumTp1OXV0tu+++a1H7/fffF4BpBWanjeE9z6l2zz332OQ+KuE6LpbRshgMJTHfDoOhh7JggXbo7Nu3D3+/7s8MGzqkqE06neZLX/wfLr3kQgByuTwPP/IYffv24cKvnBtre9ihB3HYoQczb/6CmCPqluDir54fN/E0NHDRhefjui733R/mHvEdYL/zza/FzEiDBg3k3C/8D7lcngcemrTJ47j73gdobW3lW1+/mJ3GjilaX1NTw8f22G2T+wdYv2E9ffo0kU6nP1I/BkNPxZh+DIYezJVXX0smk+bcL57DpIfv4bXX3mDGzNnk83mGDR3CwQcdQJ8+TbEw5N9fcTX77bsPF3/1fPbacw/efe99hg4dwvHHHkN7ewc//sn/FjmKbm7mzZ/PQ/f9N5ZHZfDgQdz4z3/HEp3d/8DDHHvMURxz9JE8cM9/ePa5F6itq+WE4z9Bn6YmfvO7K4pCkzeGtWvX8e3vXcZVV/yW+++5nRdefIU5c+eRTqcYOnQI+++7N5PfeY/zL7x0k/fx6mtvsPtuu/L36//Mm29NJpfL8cabbwfOw6XYZ+89+eRZpwPQt0+fYNlvfvXzYOy/+8OVmzwug6G7YAQVg6EHo5Ti8t/9iYcefpTPfeaT7Lvv3uy7z95IKVi5chUvvvQKd9/7AK+8+nqwzdq16/j0577IxV89n6OOOoJ99tmL1pZWnnr6Wf5y7d+2iq/GN779Q77+tQs56cTj6d+vL4sWLeEXv/pdYmbar3/r+3zhnM9xxmknc87/fIZcLscHH07jn/+6laefef4jj+W551/kjE+ezZfP/QIHHbQ/hxx8AO0dHSxftoJ77n2QBx565CP1f+11f6d37958/IjD2GfvPbFtmz9fc31FQWXEiOGceXq8kOHIEcMZOWI4AIsWLzGCiqFHIMZN3HvLvhoZDAZDlfgp9HfZdZ9tPRSDwdBNMD4qBoPBYDAYui1GUDEYDAaDwdBtMYKKwWAwGAyGbovxUTEYDAaDwdBtMRoVg8FgMBgM3RYjqBgMBoPBYOi2GEHFYDAYDAZDt8UIKgaDwWAwGLotRlAxGAwGg8HQbTGCisFgMBgMhm6LEVQMBoPBYDB0W4ygYjAYDAaDodtiBBWDwWAwGAzdlv8Pqlb0CaUZijcAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Compute t-SNE\n", + "logger.debug(\"Computing t-SNE\")\n", + "tsne = TSNE(n_components=2, random_state=0, n_iter=1000, perplexity=75)\n", + "x_tsne = tsne.fit_transform(test_y1)\n", + "\n", + "# Plot t-SNE in matplotlib\n", + "fig, ax = plt.subplots(1, 1, figsize=(6, 6))\n", + "ax.scatter(x_tsne[:, 0], x_tsne[:, 1], c=x_tsne[:, 0] - x_tsne[:, 1], cmap=\"viridis\")\n", + "fig.suptitle(\"HK Foundation: t-SNE\")\n", + "ax.set_xlabel(\"Component 1\")\n", + "ax.set_ylabel(\"Component 2\")\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/guides/index.md b/docs/guides/index.md index 0b7f4c3a..5cd5e2c2 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -2,9 +2,18 @@ This section contains guides to help with various aspects of HeartKit. The guides are designed to provide detailed information on how to use HeartKit for different tasks and workflows. +## Core Concepts Guides + - **[Quickstart](../quickstart.md)**: A quick start guide to get you up and running with HeartKit. + +## Notebook Training Examples + - **[Train Arrhythmia Model](train-arrhythmia-model.ipynb)**: Training a 4-stage arrhythmia model from scratch. - **[Train ECG Denoiser](train-ecg-denoiser.ipynb)**: Training an ECG denoiser from scratch. - **[Train ECG Segmentation](train-ecg-segmentation.ipynb)**: Training an ECG segmentation model from scratch. +- **[ECG Foundation Model](ecg-foundation-model.ipynb)**: Create an ECG foundation model. + +## Hardware Guides + - **[Run simple demo on EVB]()**: Running a demo using Ambiq SoC as backend inference engine. - **[Full HeartKit EVB App](heartkit-demo.md)**: A guide to running a multi-headed model demo on Ambiq EVB. diff --git a/docs/guides/train-ecg-denoiser.ipynb b/docs/guides/train-ecg-denoiser.ipynb index d3b4b4eb..0e5f59e5 100644 --- a/docs/guides/train-ecg-denoiser.ipynb +++ b/docs/guides/train-ecg-denoiser.ipynb @@ -6,15 +6,17 @@ "source": [ "# Train ECG Denosier\n", "\n", - "__Date created:__ 2024/07/17 \n", + "__Date created:__ 2024/08/13 \n", "\n", "__Last Modified:__ 2024/07/17 \n", "\n", "__Description:__ Train, evaluate, and export ECG denoiser model from scratch\n", "\n", + "\n", "## Overview \n", "\n", - "In this guide, we will train an ECG denoiser to remove noise and artifacts from raw ECG signals. Once trained, we demonstrate how to evaluate the model and export it for inference for both TF Lite and TF Lite for Micro.\n", + "In this guide, we will train an ECG denoiser to remove noise and artifacts from raw ECG signals. \n", + "Once trained, we demonstrate how to evaluate the model and export it for inference for both TF Lite and TF Lite for Micro.\n", "\n", "__Input__\n", "\n", @@ -26,7 +28,32 @@ "__Datasets__\n", "\n", "- **[Synthetic](https://ambiqai.github.io/heartkit/datasets/synthetic/)**: Synthetic ECG signals from PhysioKit\n", - "- **[PTB-XL](https://ambiqai.github.io/heartkit/datasets/ptbxl/)**: The PTB-XL is a large publicly available electrocardiography dataset. It contains 21837 clinical 12-lead ECGs from 18885 patients of 10 second length. The ECGs are sampled at 500 Hz and are annotated by up to two cardiologists.\n" + "- **[PTB-XL](https://ambiqai.github.io/heartkit/datasets/ptbxl/)**: The PTB-XL is a large publicly available electrocardiography dataset. \n", + "It contains 21837 clinical 12-lead ECGs from 18885 patients of 10 second length. The ECGs are sampled at 500 Hz and are annotated by up to two cardiologists.\n", + "\n", + "\n", + "
\n", + "\n", + "- \n", + "\n", + " View in Colab\n", + "\n", + "\n", + "- \n", + "\n", + " GitHub source\n", + "\n", + "\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -q --disable-pip-version-check heartkit" ] }, { @@ -43,25 +70,18 @@ "outputs": [], "source": [ "import os\n", - "os.environ[\"KMP_AFFINITY\"] = \"noverbose\"\n", "os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'\n", - "os.environ['AUTOGRAPH_VERBOSITY'] = '5'\n", - "\n", "import contextlib\n", "from pathlib import Path\n", "import tempfile\n", "import keras\n", - "import pandas as pd\n", "import heartkit as hk\n", - "import physiokit as pk\n", + "import tensorflow as tf\n", "import numpy as np\n", "import neuralspot_edge as nse\n", - "import matplotlib as mpl\n", "import matplotlib.pyplot as plt\n", - "import plotly.io as pio\n", "\n", - "hk.silence_tensorflow()\n", - "logger = hk.setup_logger(__name__)\n" + "os.environ['DATASET_PATH'] = '../datasets'" ] }, { @@ -79,45 +99,62 @@ "metadata": {}, "outputs": [], "source": [ - "# Seed for reproducibility\n", - "seed = 42\n", - "\n", "# File paths\n", - "datasets_dir = Path(\"../../datasets\")\n", + "datasets_dir = Path(os.getenv(\"DATASET_PATH\", \"./datasets\"))\n", "job_dir = Path(tempfile.gettempdir()) / \"hk-ecg-denoiser\"\n", "model_file = job_dir / \"model.keras\"\n", - "val_file = job_dir / \"val.pkl\"\n", "\n", "# Data settings\n", "sampling_rate = 100 # 100 Hz\n", "frame_size = 256 # 2.56 seconds\n", - "num_synthetic_patients = 1000 # Number of synthetic patients\n", + "num_synthetic_patients = 10000 # Number of synthetic patients\n", "\n", "# Training settings\n", - "batch_size = 256 # Batch size for training\n", - "buffer_size = 25000 # How many samples are shuffled each epoch\n", - "epochs = 50 # Increase this to 100+ for better results\n", - "steps_per_epoch = 50 # # Steps per epoch (must set since ds has unknown size)\n", - "samples_per_patient = 25 # Number of samples per patient\n", - "val_size = 10000 # Number of samples used for validation\n", - "val_percentage = 0.2 # Percentage of samples used for validation\n", - "test_size = 5000 # Number of samples used for testing\n", - "verbose = 1 # Verbosity level\n", - "learning_rate = 1e-3 # Learning rate for Adam optimizer\n", + "batch_size = 256 # Batch size for training\n", + "buffer_size = 25000 # How many samples are shuffled each epoch\n", + "epochs = 100 # Increase this to 100+ for better results\n", + "steps_per_epoch = 50 # Steps per epoch (must set since ds has unknown size)\n", + "samples_per_patient = 5 # Number of samples per patient\n", + "val_samples_per_patient = 10 # Number of samples per patient for validation\n", + "val_metric = \"loss\"\n", + "val_mode = \"min\"\n", + "val_size = 10000 # Number of samples used for validation\n", + "val_percentage = 0.2 # Percentage of samples used for validation\n", + "learning_rate = 1e-3 # Learning rate for Adam optimizer\n", + "epsilon = 0.01\n", "\n", - "# Plotting settings\n", - "bg_rgba_color = \"rgba(38,42,50,1.0)\"\n", - "bg_color = \"#262a32\"\n", - "primary_color = \"#11acd5\"\n", - "secondary_color = \"#ce6cff\"\n", - "tertiary_color = \"#ea3424\"\n", - "quaternary_color = \"#5cc99a\"\n", - "colors = [primary_color, secondary_color, tertiary_color, quaternary_color]\n", - "plotly_template = \"plotly_dark\"\n", - "pio.renderers.default = \"notebook\"\n", - "plt.style.use('dark_background')\n", - "mpl.rcParams['axes.facecolor'] = bg_color\n", - "mpl.rcParams['figure.facecolor'] = bg_color\n" + "# Other settings\n", + "seed = 42 # Seed for reproducibility\n", + "verbose = 1 # Verbosity level\n", + "plot_theme = hk.utils.dark_theme\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
INFO     Job directory: /tmp/hk-ecg-denoiser                                                        1872153631.py:6\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34mINFO \u001b[0m Job directory: \u001b[35m/tmp/\u001b[0m\u001b[95mhk-ecg-denoiser\u001b[0m \u001b]8;id=653937;file:///tmp/ipykernel_1619872/1872153631.py\u001b\\\u001b[2m1872153631.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=196438;file:///tmp/ipykernel_1619872/1872153631.py#6\u001b\\\u001b[2m6\u001b[0m\u001b]8;;\u001b\\\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "nse.utils.silence_tensorflow()\n", + "hk.utils.setup_plotting(plot_theme)\n", + "logger = nse.utils.setup_logger(__name__, level=verbose)\n", + "\n", + "os.makedirs(job_dir, exist_ok=True)\n", + "logger.info(f\"Job directory: {job_dir}\")\n" ] }, { @@ -131,19 +168,18 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "datasets = [\n", - " dict(\n", - " name=\"synthetic\",\n", - " path=datasets_dir / \"synthetic\",\n", + " hk.NamedParams(\n", + " name=\"ecg-synthetic\",\n", " params=dict(\n", " num_pts=num_synthetic_patients,\n", " params=dict(\n", " presets=[\"SR\", \"AFIB\", \"ant_STEMI\", \"LAHB\", \"LPHB\", \"high_take_off\", \"LBBB\", \"random_morphology\"],\n", - " preset_weights=[8, 4, 1, 1, 1, 1, 1, 0],\n", + " preset_weights=[24, 8, 1, 1, 1, 1, 1, 0],\n", " duration=10,\n", " sample_rate=sampling_rate,\n", " heart_rate=[40, 160],\n", @@ -155,10 +191,11 @@ " )\n", " )\n", " ),\n", - " dict(\n", + " hk.NamedParams(\n", " name=\"ptbxl\",\n", - " path=datasets_dir / \"ptbxl\",\n", - " params=dict()\n", + " params=dict(\n", + " path=datasets_dir / \"ptbxl\",\n", + " )\n", " )\n", "]\n" ] @@ -169,12 +206,12 @@ "source": [ "### Download the datasets\n", "\n", - "We will download the synthetic and PTB-XL datasets using the `heartkit` library. If already downloaded, this step will be skipped." + "We will download the synthetic and PTB-XL datasets using `heartkit`. If already downloaded, this step will be skipped." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -185,6 +222,92 @@ "))" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create data pipeline\n", + "\n", + "Next, we will create a `tf.data` pipeline by performing the following steps on each dataset: \n", + "* Loading dataset class handler \n", + "* Leverage task specific data loader for given dataset\n", + "* Splittiing the dataset into training and validation sets\n", + "* Creating `tf.data.Dataset` objects for training and validation\n", + "\n", + "After creating all the `tf.data.Dataset` objects, we will merge them into a single dataset for training and validation. \n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# Load datasets\n", + "dsets = [hk.DatasetFactory.get(ds.name)(**ds.params) for ds in datasets]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 10000/10000 [01:34<00:00, 105.77it/s]\n", + "WARNING: All log messages before absl::InitializeLog() is called are written to STDERR\n", + "I0000 00:00:1723573260.884713 1619872 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723573260.904235 1619872 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723573260.904314 1619872 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723573260.905752 1619872 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723573260.905821 1619872 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723573260.905867 1619872 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723573260.950750 1619872 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723573260.950842 1619872 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n", + "I0000 00:00:1723573260.950898 1619872 cuda_executor.cc:1015] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero. See more at https://github.com/torvalds/linux/blob/v6.0/Documentation/ABI/testing/sysfs-bus-pci#L344-L355\n" + ] + } + ], + "source": [ + "dset_weights = np.array([0.9, 0.1])\n", + "\n", + "train_datasets = []\n", + "val_datasets = []\n", + "for ds in dsets:\n", + " # Create dataloader\n", + " dataloader = hk.tasks.denoise.DenoiseDataloader(\n", + " ds=ds,\n", + " frame_size=frame_size,\n", + " sampling_rate=sampling_rate,\n", + " )\n", + "\n", + " # Split patients into train and validation sets\n", + " train_patients, val_patients = dataloader.split_train_val_patients()\n", + "\n", + " # Create train dataset\n", + " train_ds = dataloader.create_dataloader(\n", + " patient_ids=train_patients,\n", + " samples_per_patient=samples_per_patient,\n", + " shuffle=True\n", + " )\n", + "\n", + " # Create validation dataset\n", + " val_ds = dataloader.create_dataloader(\n", + " patient_ids=val_patients,\n", + " samples_per_patient=samples_per_patient,\n", + " shuffle=False\n", + " )\n", + " train_datasets.append(train_ds)\n", + " val_datasets.append(val_ds)\n", + "# END FOR\n", + "\n", + "# Combine datasets\n", + "train_ds = tf.data.Dataset.sample_from_datasets(train_datasets, weights=dset_weights)\n", + "val_ds = tf.data.Dataset.sample_from_datasets(val_datasets, weights=dset_weights)\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -196,14 +319,14 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 8, "metadata": {}, "outputs": [ { "data": { - "image/png": "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Ir31zv8s77lu3W9hjzcHS3l5uXHXq6X36Nevr1wul6695ve69prf275HYffuCQdZ/fr20YqVqxQIBBJ63Wa1dXWaNXtO1Oerq0PBVXZEYBhNTW3o+qyszLDyed/O10WX/E6SdMnFY7TXnru3proAgAgEWQCApPH5/Fr4/Q9a+P0PWrlqtf52z1806ugj9djjT0mSvvhyljYWF+ukE4/TN3O/1UknHKsNG4s1c9ZXlvcKBIK2n2EYRtz1aek9XJv+/+lnntOML2fZXrt69S9xf157qamp0fr1G7TTTgPjun758pWSpEE77qglS35qKi8rK28K5k468bik1xMAnIrdBQEAbeL773+QJPXsUdRUFgwG9d93p+mYo49QXl6ujjziUL37v2kKBu2DoZaYprlVdfxlza+SJJ/fr1mz59j+qamtTfizVv+yRjv0314eT9uNZU7/bIa279dXe+w+rMVrP/9ipvx+v048YVSb1QcAsAVBFgBgq4zYdx/b8kMOPkCStHzlqrDyt97+nwry83XnHbcqOzt7q3a825zHlZub08KV9kpLy/TVnG909lmnqUdRkeX5bt0Kmn1W3abPanm54gcffqzu3bvpvNFntape8fjPM8+pprZWd995uwoLu1ue79u3j8acf66k0LLJ16e+rUMOPjBqnRKZIQQAxMZyQQDAVrntT39UZkaGPvz4Uy1fsVJer0d77bG7jh11lNas+VVvTH077PofFy/RkqU/69hRR+nnZcv1w4+LW/3Zixb9GKrDLX/UF1/OViAY0P/e+yCh9xh/99/04vNP6503X9Yrr03VL2t+VVFhd+2x+27adtueOvm0czfVe6n8fr8uu+RC5ebmqLGxUbO/+jrsLLDN3nz7XZ1y8gn6003Xa7dhQzV37nxlZmZo5MgRmjLlVX08/bOYdcrNydFJJxxr+9zmnRR/+WWNbrjxVj3097/qf++8prfefldLf1qmNK9Xe+6xm0Ydc6TeePOdptdN+NsD6rNdb/351pt0/LHHaPqnn6ukpFTduhVorz330GGHHqQVEQExAKB1CLIAAFvlvr//Q6OOPlKHHHSAzj7zVHm9Xq1d95tefOk1Pf7Ef2wPKX7r7f/qxhuu1VtvWze8SMQHH32i5ya/pOOPPVonnXicXC5XwkHWsmUrdPpZF+iq31+uU085UQUF+SotKdUPi5c05ZJJ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", + "image/png": "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", "text/plain": [ - "
" + "
" ] }, "metadata": {}, @@ -211,23 +334,171 @@ } ], "source": [ - "ecg, segs, fids = pk.ecg.synthesize(\n", - " signal_length=frame_size,\n", - " sample_rate=sampling_rate,\n", - " heart_rate=60,\n", - " leads=1,\n", - " preset=pk.ecg.EcgPreset.SR,\n", - " noise_multiplier=0.0\n", - ")\n", - "ecg = ecg.squeeze()\n", + "ecg = next(iter(train_ds)).numpy()\n", "\n", "ts = np.arange(0, len(ecg)) / sampling_rate\n", - "fig, ax = plt.subplots(1, 1, figsize=(10, 5))\n", - "plt.plot(ts, ecg, color=primary_color, lw=3)\n", - "plt.title(\"Synthetic ECG\")\n", + "fig, ax = plt.subplots(1, 1, figsize=(9, 4))\n", + "ax.plot(ts, ecg, color=plot_theme.primary_color, lw=3)\n", + "fig.suptitle(\"Raw ECG Signal\")\n", + "ax.set_xlabel(\"Time (s)\")\n", + "ax.set_ylabel(\"Amplitude\")\n", + "fig.tight_layout()\n", + "fig.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create augmentation pipeline\n", + "\n", + "Since our goal is to denoise ECG signals, we need to create an augmentation pipeline to generate noisy samples. \n", + "\n", + "We will leverage `neuralspot-edge` preprocessing layers to create the following augmentations:\n", + "\n", + "* Baseline wander: Simulate baseline wander by adding a low frequency sine signal\n", + "* Powerline noise: Simulate powerline noise by adding a 50 Hz sinusoidal signal \n", + "* Amplitude warp: Simulate amplitude warp by randomly scaling along a low frequency sine wave\n", + "* Gaussian noise: Simulate lead noise by adding random noise following a Gaussian distribution\n", + "* Background noise: Add real noise captured from NSTDB dataset\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "nstdb = hk.datasets.nstdb.NstdbNoise(target_rate=sampling_rate)\n", + "noises = np.hstack((nstdb.get_noise(noise_type=\"bw\"), nstdb.get_noise(noise_type=\"ma\"), nstdb.get_noise(noise_type=\"em\")))\n", + "noises = noises.astype(np.float32)\n", + "\n", + "preprocessor = nse.layers.preprocessing.LayerNormalization1D(\n", + " epsilon=epsilon,\n", + " name=\"LayerNormalization\"\n", + ")\n", + "\n", + "augmenter = nse.layers.preprocessing.AugmentationPipeline(\n", + " layers=[\n", + " nse.layers.preprocessing.RandomNoiseDistortion1D(\n", + " sample_rate=sampling_rate,\n", + " amplitude=(0.05, 1.0),\n", + " frequency=(0.5, 1.5),\n", + " name=\"BaselineWander\"\n", + " ),\n", + " nse.layers.preprocessing.RandomSineWave(\n", + " sample_rate=sampling_rate,\n", + " amplitude=(0, 0.05),\n", + " frequency=(45, 50),\n", + " name=\"PowerlineNoise\"\n", + " ),\n", + " nse.layers.preprocessing.AmplitudeWarp(\n", + " sample_rate=sampling_rate,\n", + " amplitude=(0.9, 1.1),\n", + " frequency=(0.5, 1.5),\n", + " name=\"AmplitudeWarp\"\n", + " ),\n", + " nse.layers.preprocessing.RandomGaussianNoise1D(\n", + " factor=(0.05, 0.25),\n", + " name=\"GaussianNoise\"\n", + " ),\n", + " nse.layers.preprocessing.RandomBackgroundNoises1D(\n", + " noises=noises,\n", + " amplitude=(0.05, 0.25),\n", + " num_noises=1,\n", + " name=\"RandomBackgroundNoises\"\n", + " ),\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Visualize augmented data" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "aug_ecg = augmenter(preprocessor(keras.ops.convert_to_tensor(np.reshape(ecg, (1, -1, 1)))), training=True)\n", + "aug_ecg = aug_ecg.numpy().squeeze()\n", + "\n", + "ts = np.arange(0, len(aug_ecg)) / sampling_rate\n", + "fig, ax = plt.subplots(1, 1, figsize=(9, 4))\n", + "plt.plot(ts, aug_ecg, color=plot_theme.primary_color, lw=3)\n", + "fig.suptitle(\"Augmented ECG Signal\")\n", "ax.set_xlabel(\"Time (s)\")\n", "ax.set_ylabel(\"Amplitude\")\n", - "plt.show()\n" + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create full data pipeline w/ augmentation\n", + "\n", + "We will now create a full data pipeline by extended the original with shuffling, batching, augmentations, and prefetching.\n", + "\n", + "For validation, we will cache a subset of the validation data to speed up the evaluation process." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "train_ds = train_ds.shuffle(\n", + " buffer_size=buffer_size,\n", + " reshuffle_each_iteration=True,\n", + ").batch(\n", + " batch_size=batch_size,\n", + " drop_remainder=True,\n", + " num_parallel_calls=tf.data.AUTOTUNE,\n", + ").map(\n", + " preprocessor,\n", + " num_parallel_calls=tf.data.AUTOTUNE\n", + ").map(\n", + " lambda x: (augmenter(x, training=True), x),\n", + " num_parallel_calls=tf.data.AUTOTUNE\n", + ").prefetch(\n", + " tf.data.AUTOTUNE\n", + ")\n", + "\n", + "val_ds = val_ds.batch(\n", + " batch_size=batch_size,\n", + " drop_remainder=True,\n", + " num_parallel_calls=tf.data.AUTOTUNE,\n", + ").map(\n", + " preprocessor,\n", + " num_parallel_calls=tf.data.AUTOTUNE\n", + ").map(\n", + " lambda x: (augmenter(x, training=True), x),\n", + " num_parallel_calls=tf.data.AUTOTUNE\n", + ").prefetch(\n", + " tf.data.AUTOTUNE\n", + ")\n", + "\n", + "# Cache the validation dataset\n", + "val_ds = val_ds.take(val_size//batch_size).cache()" ] }, { @@ -236,20 +507,21 @@ "source": [ "## Define TCN model architecture\n", "\n", - "For this task, we are going to leverage a customized __TCN__ model architecture that is smaller and can handle 1D signals. The model consists of 4 TCN blocks with a depth of 1. Each block leverages dilated depthwise-separable convolutions along with inverted expansion and squeeze and excitation layers. The model is followed by a 1D convolutional layer and a final dense layer for regression. " + "For this task, we are going to leverage a customized __TCN__ model architecture that is smaller and can handle 1D signals. The model consists of 5 TCN blocks with a depth of 1. Each block leverages dilated depthwise-separable convolutions along with inverted expansion and squeeze and excitation layers. The model is followed by a 1D convolutional layer. " ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "mbconv_blocks = [\n", - " dict(depth=1, branch=1, filters=8, kernel=(1, 7), dilation=(1, 1), dropout=0, ex_ratio=1, se_ratio=0, norm=\"batch\"),\n", - " dict(depth=1, branch=1, filters=16, kernel=(1, 7), dilation=(1, 1), dropout=0, ex_ratio=1, se_ratio=2, norm=\"batch\"),\n", - " dict(depth=1, branch=1, filters=24, kernel=(1, 7), dilation=(1, 2), dropout=0, ex_ratio=1, se_ratio=2, norm=\"batch\"),\n", - " dict(depth=1, branch=1, filters=32, kernel=(1, 7), dilation=(1, 4), dropout=0, ex_ratio=1, se_ratio=2, norm=\"batch\")\n", + " dict(depth=1, branch=1, filters=16, kernel=(1, 7), dilation=(1, 1), dropout=0, ex_ratio=1, se_ratio=0, norm=\"batch\"),\n", + " dict(depth=1, branch=1, filters=24, kernel=(1, 7), dilation=(1, 1), dropout=0, ex_ratio=1, se_ratio=2, norm=\"batch\"),\n", + " dict(depth=1, branch=1, filters=32, kernel=(1, 7), dilation=(1, 2), dropout=0, ex_ratio=1, se_ratio=2, norm=\"batch\"),\n", + " dict(depth=1, branch=1, filters=40, kernel=(1, 7), dilation=(1, 4), dropout=0, ex_ratio=1, se_ratio=2, norm=\"batch\"),\n", + " dict(depth=1, branch=1, filters=48, kernel=(1, 7), dilation=(1, 8), dropout=0, ex_ratio=1, se_ratio=2, norm=\"batch\")\n", "]\n", "\n", "architecture = dict(\n", @@ -277,7 +549,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -318,53 +590,53 @@ "│ B1.D1.DW.ACT │ (None, 1, 256, 1) │ 0 │ B1.D1.DW.B1.BN[0… │\n", "│ (Activation) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B1.D1.PW.B1.CN │ (None, 1, 256, 8) │ 8 │ B1.D1.DW.ACT[0][ │\n", - "│ (Conv2D) │ │ │ │\n", + "│ B1.D1.PW.B1.CN │ (None, 1, 256, │ 16 │ B1.D1.DW.ACT[0][ │\n", + "│ (Conv2D) │ 16) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B1.D1.PW.B1.BN │ (None, 1, 256, 8) │ 32 │ B1.D1.PW.B1.CN[0… │\n", - "│ (BatchNormalizatio… │ │ │ │\n", + "│ B1.D1.PW.B1.BN │ (None, 1, 256, │ 64 │ B1.D1.PW.B1.CN[0… │\n", + "│ (BatchNormalizatio…16) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B1.D1.PW.ACT │ (None, 1, 256, 8) │ 0 │ B1.D1.PW.B1.BN[0… │\n", - "│ (Activation) │ │ │ │\n", + "│ B1.D1.PW.ACT │ (None, 1, 256, │ 0 │ B1.D1.PW.B1.BN[0… │\n", + "│ (Activation) │ 16) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.DW.B1.CN │ (None, 1, 256, 8) │ 56 │ B1.D1.PW.ACT[0][ │\n", - "│ (DepthwiseConv2D) │ │ │ │\n", + "│ B2.D1.DW.B1.CN │ (None, 1, 256, │ 112 │ B1.D1.PW.ACT[0][ │\n", + "│ (DepthwiseConv2D) │ 16) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.DW.B1.BN │ (None, 1, 256, 8) │ 32 │ B2.D1.DW.B1.CN[0… │\n", - "│ (BatchNormalizatio… │ │ │ │\n", + "│ B2.D1.DW.B1.BN │ (None, 1, 256, │ 64 │ B2.D1.DW.B1.CN[0… │\n", + "│ (BatchNormalizatio…16) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.DW.ACT │ (None, 1, 256, 8) │ 0 │ B2.D1.DW.B1.BN[0… │\n", - "│ (Activation) │ │ │ │\n", + "│ B2.D1.DW.ACT │ (None, 1, 256, │ 0 │ B2.D1.DW.B1.BN[0… │\n", + "│ (Activation) │ 16) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.SE.pool │ (None, 1, 1, 8) │ 0 │ B2.D1.DW.ACT[0][ │\n", + "│ B2.D1.SE.pool │ (None, 1, 1, 16) │ 0 │ B2.D1.DW.ACT[0][ │\n", "│ (GlobalAveragePool… │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.SE.sq.conv │ (None, 1, 1, 4) │ 36 │ B2.D1.SE.pool[0]… │\n", + "│ B2.D1.SE.sq.conv │ (None, 1, 1, 8) │ 136 │ B2.D1.SE.pool[0]… │\n", "│ (Conv2D) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.SE.sq.act │ (None, 1, 1, 4) │ 0 │ B2.D1.SE.sq.conv… │\n", + "│ B2.D1.SE.sq.act │ (None, 1, 1, 8) │ 0 │ B2.D1.SE.sq.conv… │\n", "│ (Activation) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.SE.ex.conv │ (None, 1, 1, 8) │ 40 │ B2.D1.SE.sq.act[ │\n", + "│ B2.D1.SE.ex.conv │ (None, 1, 1, 16) │ 144 │ B2.D1.SE.sq.act[ │\n", "│ (Conv2D) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.SE.ex.act │ (None, 1, 1, 8) │ 0 │ B2.D1.SE.ex.conv… │\n", + "│ B2.D1.SE.ex.act │ (None, 1, 1, 16) │ 0 │ B2.D1.SE.ex.conv… │\n", "│ (Activation) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ multiply (Multiply) │ (None, 1, 256, 8) │ 0 │ B2.D1.DW.ACT[0][ │\n", - "│ │ │ │ B2.D1.SE.ex.act[ │\n", + "│ multiply (Multiply) │ (None, 1, 256, │ 0 │ B2.D1.DW.ACT[0][ │\n", + "│ │ 16) │ │ B2.D1.SE.ex.act[ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.PW.B1.CN │ (None, 1, 256, │ 128 │ multiply[0][0] │\n", - "│ (Conv2D) │ 16) │ │ │\n", + "│ B2.D1.PW.B1.CN │ (None, 1, 256, │ 384 │ multiply[0][0] │\n", + "│ (Conv2D) │ 24) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.PW.B1.BN │ (None, 1, 256, │ 64 │ B2.D1.PW.B1.CN[0… │\n", - "│ (BatchNormalizatio…16) │ │ │\n", + "│ B2.D1.PW.B1.BN │ (None, 1, 256, │ 96 │ B2.D1.PW.B1.CN[0… │\n", + "│ (BatchNormalizatio…24) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ B2.D1.PW.ACT │ (None, 1, 256, │ 0 │ B2.D1.PW.B1.BN[0… │\n", - "│ (Activation) │ 16) │ │ │\n", + "│ (Activation) │ 24) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B3.D1.DW.B1.CN │ (None, 1, 256, │ 112 │ B2.D1.PW.ACT[0][ │\n", - "│ (DepthwiseConv2D) │ 16) │ │ │\n", + "│ B3.D1.DW.B1.CN │ (None, 1, 256, │ 168 │ B2.D1.PW.ACT[0][ │\n", + "│ (DepthwiseConv2D) │ 24) │ │ │\n", "└─────────────────────┴───────────────────┴────────────┴───────────────────┘\n", "\n" ], @@ -391,53 +663,53 @@ "│ B1.D1.DW.ACT │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ B1.D1.DW.B1.BN[\u001b[38;5;34m0\u001b[0m… │\n", "│ (\u001b[38;5;33mActivation\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B1.D1.PW.B1.CN │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m8\u001b[0m │ B1.D1.DW.ACT[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m…\u001b[0m │\n", - "│ (\u001b[38;5;33mConv2D\u001b[0m) │ │ │ │\n", + "│ B1.D1.PW.B1.CN │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, │ \u001b[38;5;34m16\u001b[0m │ B1.D1.DW.ACT[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m…\u001b[0m │\n", + "│ (\u001b[38;5;33mConv2D\u001b[0m) │ \u001b[38;5;34m16\u001b[0m) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B1.D1.PW.B1.BN │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m32\u001b[0m │ B1.D1.PW.B1.CN[\u001b[38;5;34m0\u001b[0m… │\n", - "│ (\u001b[38;5;33mBatchNormalizatio…\u001b[0m │ │ │ │\n", + "│ B1.D1.PW.B1.BN │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, │ \u001b[38;5;34m64\u001b[0m │ B1.D1.PW.B1.CN[\u001b[38;5;34m0\u001b[0m… │\n", + "│ (\u001b[38;5;33mBatchNormalizatio…\u001b[0m │ \u001b[38;5;34m16\u001b[0m) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B1.D1.PW.ACT │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ B1.D1.PW.B1.BN[\u001b[38;5;34m0\u001b[0m… │\n", - "│ (\u001b[38;5;33mActivation\u001b[0m) │ │ │ │\n", + "│ B1.D1.PW.ACT │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, │ \u001b[38;5;34m0\u001b[0m │ B1.D1.PW.B1.BN[\u001b[38;5;34m0\u001b[0m… │\n", + "│ (\u001b[38;5;33mActivation\u001b[0m) │ \u001b[38;5;34m16\u001b[0m) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.DW.B1.CN │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m56\u001b[0m │ B1.D1.PW.ACT[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m…\u001b[0m │\n", - "│ (\u001b[38;5;33mDepthwiseConv2D\u001b[0m) │ │ │ │\n", + "│ B2.D1.DW.B1.CN │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, │ \u001b[38;5;34m112\u001b[0m │ B1.D1.PW.ACT[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m…\u001b[0m │\n", + "│ (\u001b[38;5;33mDepthwiseConv2D\u001b[0m) │ \u001b[38;5;34m16\u001b[0m) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.DW.B1.BN │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m32\u001b[0m │ B2.D1.DW.B1.CN[\u001b[38;5;34m0\u001b[0m… │\n", - "│ (\u001b[38;5;33mBatchNormalizatio…\u001b[0m │ │ │ │\n", + "│ B2.D1.DW.B1.BN │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, │ \u001b[38;5;34m64\u001b[0m │ B2.D1.DW.B1.CN[\u001b[38;5;34m0\u001b[0m… │\n", + "│ (\u001b[38;5;33mBatchNormalizatio…\u001b[0m │ \u001b[38;5;34m16\u001b[0m) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.DW.ACT │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ B2.D1.DW.B1.BN[\u001b[38;5;34m0\u001b[0m… │\n", - "│ (\u001b[38;5;33mActivation\u001b[0m) │ │ │ │\n", + "│ B2.D1.DW.ACT │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, │ \u001b[38;5;34m0\u001b[0m │ B2.D1.DW.B1.BN[\u001b[38;5;34m0\u001b[0m… │\n", + "│ (\u001b[38;5;33mActivation\u001b[0m) │ \u001b[38;5;34m16\u001b[0m) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.SE.pool │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ B2.D1.DW.ACT[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m…\u001b[0m │\n", + "│ B2.D1.SE.pool │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m16\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ B2.D1.DW.ACT[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m…\u001b[0m │\n", "│ (\u001b[38;5;33mGlobalAveragePool…\u001b[0m │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.SE.sq.conv │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m4\u001b[0m) │ \u001b[38;5;34m36\u001b[0m │ B2.D1.SE.pool[\u001b[38;5;34m0\u001b[0m]… │\n", + "│ B2.D1.SE.sq.conv │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m136\u001b[0m │ B2.D1.SE.pool[\u001b[38;5;34m0\u001b[0m]… │\n", "│ (\u001b[38;5;33mConv2D\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.SE.sq.act │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m4\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ B2.D1.SE.sq.conv… │\n", + "│ B2.D1.SE.sq.act │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ B2.D1.SE.sq.conv… │\n", "│ (\u001b[38;5;33mActivation\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.SE.ex.conv │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m40\u001b[0m │ B2.D1.SE.sq.act[\u001b[38;5;34m…\u001b[0m │\n", + "│ B2.D1.SE.ex.conv │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m16\u001b[0m) │ \u001b[38;5;34m144\u001b[0m │ B2.D1.SE.sq.act[\u001b[38;5;34m…\u001b[0m │\n", "│ (\u001b[38;5;33mConv2D\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.SE.ex.act │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ B2.D1.SE.ex.conv… │\n", + "│ B2.D1.SE.ex.act │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m16\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ B2.D1.SE.ex.conv… │\n", "│ (\u001b[38;5;33mActivation\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ multiply (\u001b[38;5;33mMultiply\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ B2.D1.DW.ACT[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m…\u001b[0m │\n", - "│ │ │ │ B2.D1.SE.ex.act[\u001b[38;5;34m…\u001b[0m │\n", + "│ multiply (\u001b[38;5;33mMultiply\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, │ \u001b[38;5;34m0\u001b[0m │ B2.D1.DW.ACT[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m…\u001b[0m │\n", + "│ │ \u001b[38;5;34m16\u001b[0m) │ │ B2.D1.SE.ex.act[\u001b[38;5;34m…\u001b[0m │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.PW.B1.CN │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, │ \u001b[38;5;34m128\u001b[0m │ multiply[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", - "│ (\u001b[38;5;33mConv2D\u001b[0m) │ \u001b[38;5;34m16\u001b[0m) │ │ │\n", + "│ B2.D1.PW.B1.CN │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, │ \u001b[38;5;34m384\u001b[0m │ multiply[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", + "│ (\u001b[38;5;33mConv2D\u001b[0m) │ \u001b[38;5;34m24\u001b[0m) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B2.D1.PW.B1.BN │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, │ \u001b[38;5;34m64\u001b[0m │ B2.D1.PW.B1.CN[\u001b[38;5;34m0\u001b[0m… │\n", - "│ (\u001b[38;5;33mBatchNormalizatio…\u001b[0m │ \u001b[38;5;34m16\u001b[0m) │ │ │\n", + "│ B2.D1.PW.B1.BN │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, │ \u001b[38;5;34m96\u001b[0m │ B2.D1.PW.B1.CN[\u001b[38;5;34m0\u001b[0m… │\n", + "│ (\u001b[38;5;33mBatchNormalizatio…\u001b[0m │ \u001b[38;5;34m24\u001b[0m) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ B2.D1.PW.ACT │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, │ \u001b[38;5;34m0\u001b[0m │ B2.D1.PW.B1.BN[\u001b[38;5;34m0\u001b[0m… │\n", - "│ (\u001b[38;5;33mActivation\u001b[0m) │ \u001b[38;5;34m16\u001b[0m) │ │ │\n", + "│ (\u001b[38;5;33mActivation\u001b[0m) │ \u001b[38;5;34m24\u001b[0m) │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", - "│ B3.D1.DW.B1.CN │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, │ \u001b[38;5;34m112\u001b[0m │ B2.D1.PW.ACT[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m…\u001b[0m │\n", - "│ (\u001b[38;5;33mDepthwiseConv2D\u001b[0m) │ \u001b[38;5;34m16\u001b[0m) │ │ │\n", + "│ B3.D1.DW.B1.CN │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m256\u001b[0m, │ \u001b[38;5;34m168\u001b[0m │ B2.D1.PW.ACT[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m…\u001b[0m │\n", + "│ (\u001b[38;5;33mDepthwiseConv2D\u001b[0m) │ \u001b[38;5;34m24\u001b[0m) │ │ │\n", "└─────────────────────┴───────────────────┴────────────┴───────────────────┘\n" ] }, @@ -447,11 +719,11 @@ { "data": { "text/html": [ - "
 Total params: 3,351 (13.09 KB)\n",
+       "
 Total params: 10,223 (39.93 KB)\n",
        "
\n" ], "text/plain": [ - "\u001b[1m Total params: \u001b[0m\u001b[38;5;34m3,351\u001b[0m (13.09 KB)\n" + "\u001b[1m Total params: \u001b[0m\u001b[38;5;34m10,223\u001b[0m (39.93 KB)\n" ] }, "metadata": {}, @@ -460,11 +732,11 @@ { "data": { "text/html": [ - "
 Trainable params: 3,091 (12.07 KB)\n",
+       "
 Trainable params: 9,675 (37.79 KB)\n",
        "
\n" ], "text/plain": [ - "\u001b[1m Trainable params: \u001b[0m\u001b[38;5;34m3,091\u001b[0m (12.07 KB)\n" + "\u001b[1m Trainable params: \u001b[0m\u001b[38;5;34m9,675\u001b[0m (37.79 KB)\n" ] }, "metadata": {}, @@ -473,11 +745,11 @@ { "data": { "text/html": [ - "
 Non-trainable params: 260 (1.02 KB)\n",
+       "
 Non-trainable params: 548 (2.14 KB)\n",
        "
\n" ], "text/plain": [ - "\u001b[1m Non-trainable params: \u001b[0m\u001b[38;5;34m260\u001b[0m (1.02 KB)\n" + "\u001b[1m Non-trainable params: \u001b[0m\u001b[38;5;34m548\u001b[0m (2.14 KB)\n" ] }, "metadata": {}, @@ -490,85 +762,76 @@ " params=architecture[\"params\"],\n", " num_classes=1\n", ")\n", - "model.summary(layer_range=('inputs', 'B3.D1.DW.B1.CN'))\n", - "#keras.utils.plot_model(model, show_shapes=True)" + "model.summary(layer_range=('inputs', 'B3.D1.DW.B1.CN'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Preprocess pipeline\n", - "\n", - "We will preprocess the ECG signals by applying the following steps:\n", - "* Apply Z-score normalization w/ epsilon to avoid division by zero\n", - "\n", - "The task accepts a list of preprocessing functions that will be applied to the input data. \n", + "## Compile the model\n", "\n", - "__NOTE:__ We dont apply any filtering as the model is expected to learn the filtering mechanism." + "We will compile the model using Adam optimizer with cosine learning rate scheduler and mean squared error loss function. We will also attach metrics and callbacks to monitor the training process.\n" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ - "preprocesses = [\n", - " dict(name=\"znorm\", params=dict(eps=0.01, axis=None))\n", - "]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Augmentation pipeline\n", + "t_mul = 1\n", + "lr_cycles = 1\n", + "first_steps = (steps_per_epoch * epochs) / (np.power(lr_cycles, t_mul) - t_mul + 1)\n", + "scheduler = keras.optimizers.schedules.CosineDecayRestarts(\n", + " initial_learning_rate=learning_rate,\n", + " first_decay_steps=np.ceil(first_steps),\n", + " t_mul=t_mul,\n", + " m_mul=0.5,\n", + ")\n", + "optimizer = keras.optimizers.Adam(scheduler)\n", + "loss = keras.losses.MeanSquaredError()\n", "\n", - "We will apply the following augmentations to the ECG signals:\n", - "* Baseline wander: Simulate baseline wander by adding a random frequency sinusoidal signal to the ECG signal\n", - "* Powerline noise: Simulate powerline noise by adding a 50 Hz sinusoidal signal to the ECG signal\n", - "* Burst noise: Simulate burst noise by randomly injecting burst of high frequency noise to the ECG signal\n", - "* Noise sources: Apply several noises at given frequencies to the ECG signal\n", - "* Lead noise: Simulate lead noise by adding a random frequency sinusoidal signal to the ECG signal\n", - "* NSTDB: Add real noise captured from NSTDB dataset to the ECG signal. \n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "augmentations = [\n", - " hk.AugmentationParams(name=\"baseline_wander\", params=dict(amplitude=[0.0, 0.5], frequency=[0.5, 1.5])),\n", - " hk.AugmentationParams(name=\"powerline_noise\", params=dict(amplitude=[0.05, 0.15], frequency=[45, 50])),\n", - " hk.AugmentationParams(name=\"burst_noise\", params=dict(burst_number=[0, 4], amplitude=[0.05, 0.1], frequency=[20, 49])),\n", - " hk.AugmentationParams(name=\"noise_sources\", params=dict(num_sources=[1, 2], amplitude=[0.05, 0.1], frequency=[10, 40])),\n", - " hk.AugmentationParams(name=\"lead_noise\", params=dict(scale=[0.05, 0.1])),\n", - " hk.AugmentationParams(name=\"nstdb\", params=dict(noise_level=[0.1, 0.3]))\n", - "]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Visualize the augmentations\n", + "metrics = [\n", + " keras.metrics.MeanAbsoluteError(name=\"mae\"),\n", + " keras.metrics.MeanSquaredError(name=\"mse\"),\n", + " keras.metrics.CosineSimilarity(name=\"cos\"),\n", + " nse.metrics.Snr(name=\"snr\"),\n", + "]\n", "\n", - "Taking the existing synthetic ECG signal, let's look at the effects of the augmentations on the signal." + "model_callbacks = [\n", + " keras.callbacks.EarlyStopping(\n", + " monitor=f\"val_{val_metric}\",\n", + " patience=max(int(0.25 * epochs), 1),\n", + " mode=val_mode,\n", + " restore_best_weights=True,\n", + " verbose=min(verbose - 1, 1),\n", + " ),\n", + " keras.callbacks.ModelCheckpoint(\n", + " filepath=str(model_file),\n", + " monitor=f\"val_{val_metric}\",\n", + " save_best_only=True,\n", + " save_weights_only=False,\n", + " mode=val_mode,\n", + " verbose=min(verbose - 1, 1),\n", + " ),\n", + " keras.callbacks.CSVLogger(job_dir / \"history.csv\"),\n", + "]" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 15, "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "text/html": [ + "
INFO     Model requires 3.02 MFLOPS                                                                 1319647386.py:3\n",
+       "
\n" + ], "text/plain": [ - "
" + "\u001b[34mINFO \u001b[0m Model requires \u001b[1;36m3.02\u001b[0m MFLOPS \u001b]8;id=119005;file:///tmp/ipykernel_1619872/1319647386.py\u001b\\\u001b[2m1319647386.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=942841;file:///tmp/ipykernel_1619872/1319647386.py#3\u001b\\\u001b[2m3\u001b[0m\u001b]8;;\u001b\\\n" ] }, "metadata": {}, @@ -576,215 +839,268 @@ } ], "source": [ - "ecg_noise = hk.datasets.augment_pipeline(ecg, augmentations=augmentations, sample_rate=sampling_rate)\n", - "ts = np.arange(0, len(ecg)) / sampling_rate\n", - "fig, ax = plt.subplots(1, 1, figsize=(10, 5))\n", - "plt.plot(ts, ecg_noise, color=primary_color, lw=3)\n", - "plt.title(\"Synthetic ECG w/ Noise\")\n", - "ax.set_xlabel(\"Time (s)\")\n", - "ax.set_ylabel(\"Amplitude\")\n", - "plt.show()\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Load denoise task \n", - "\n", - "HeartKit provides a __TaskFactory__ that includes a number ready-to-use tasks. Each task provides methods for training, evaluating, exporting, and demoing. We will grab the __denoise__ task and configure it for our use case." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "task = hk.TaskFactory.get(\"denoise\")" + "model.compile(optimizer=optimizer, loss=loss, metrics=metrics)\n", + "flops = nse.metrics.flops.get_flops(model, batch_size=1, fpath=os.devnull)\n", + "logger.info(f\"Model requires {flops/1e6:0.2f} MFLOPS\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Train the model\n", - "\n", - "The task's __train__ method accepts a high-level configuration that includes dataset, model, classes, preprocessing, and training parameters. We will provide the following configuration to train the model." + "## Train the model" ] }, { "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "train_params = hk.HKTrainParams(\n", - " job_dir=job_dir, # Directory to store all output artifacts\n", - " datasets=datasets, # Datasets to train on\n", - " sampling_rate=sampling_rate, # Target sampling rate\n", - " frame_size=frame_size, # Target frame size\n", - " # Training parameters\n", - " samples_per_patient=samples_per_patient, # Samples per train patient\n", - " val_samples_per_patient=samples_per_patient, # Samples per val patient\n", - " val_patients=val_percentage, # Percentage of patients used for validation\n", - " val_file=val_file, # Validation file (cached)\n", - " batch_size=batch_size, # Batch size\n", - " buffer_size=buffer_size, # Buffer size\n", - " epochs=epochs, # Number of epochs to train\n", - " steps_per_epoch=steps_per_epoch, # Steps per epoch\n", - " val_metric=\"loss\", # Metric to monitor for early stopping\n", - " lr_rate=learning_rate, # Learning rate\n", - " lr_cycles=1, # Number of learning rate cycles for cosine decay\n", - " class_weights=\"balanced\", # Utilize class weights to balance training\n", - " preprocesses=preprocesses, # Preprocessing pipeline\n", - " augmentations=augmentations, # Augmentation pipeline\n", - " architecture=architecture, # Model architecture\n", - " model_file=model_file, # File to save model\n", - " verbose=verbose # Verbosity level\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 13, + "execution_count": 16, "metadata": {}, "outputs": [ { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Epoch 1/100\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "WARNING: All log messages before absl::InitializeLog() is called are written to STDERR\n",
+      "I0000 00:00:1723573267.969050 1620307 service.cc:146] XLA service 0x797c7800b300 initialized for platform CUDA (this does not guarantee that XLA will be used). Devices:\n",
+      "I0000 00:00:1723573267.969070 1620307 service.cc:154]   StreamExecutor device (0): NVIDIA GeForce RTX 4090, Compute Capability 8.9\n"
+     ]
     },
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Epoch 1/50\n"
+      "\u001b[1m13/50\u001b[0m \u001b[32m━━━━━\u001b[0m\u001b[37m━━━━━━━━━━━━━━━\u001b[0m \u001b[1m0s\u001b[0m 13ms/step - cos: 0.1119 - loss: 1.8925 - mae: 0.8850 - mse: 1.5532 - snr: -4.3028"
      ]
     },
     {
      "name": "stderr",
      "output_type": "stream",
      "text": [
-      "WARNING: All log messages before absl::InitializeLog() is called are written to STDERR\n",
-      "I0000 00:00:1721248273.976760  773045 service.cc:146] XLA service 0x7efdf0024350 initialized for platform CUDA (this does not guarantee that XLA will be used). Devices:\n",
-      "I0000 00:00:1721248273.976801  773045 service.cc:154]   StreamExecutor device (0): NVIDIA GeForce RTX 4090, Compute Capability 8.9\n",
-      "I0000 00:00:1721248280.084769  773045 device_compiler.h:188] Compiled cluster using XLA!  This line is logged at most once for the lifetime of the process.\n"
+      "I0000 00:00:1723573276.273866 1620307 device_compiler.h:188] Compiled cluster using XLA!  This line is logged at most once for the lifetime of the process.\n"
      ]
     },
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m172s\u001b[0m 1s/step - cosine: 0.0173 - loss: 1.2401 - mae: 0.7030 - mse: 1.0883 - val_cosine: 0.2671 - val_loss: 1.0405 - val_mae: 0.4550 - val_mse: 0.8911\n",
-      "Epoch 2/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m57s\u001b[0m 1s/step - cosine: 0.3424 - loss: 0.5309 - mae: 0.4269 - mse: 0.3830 - val_cosine: 0.2670 - val_loss: 1.0345 - val_mae: 0.4534 - val_mse: 0.8915\n",
-      "Epoch 3/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m46s\u001b[0m 940ms/step - cosine: 0.4254 - loss: 0.4207 - mae: 0.3632 - mse: 0.2794 - val_cosine: 0.2615 - val_loss: 1.0199 - val_mae: 0.4532 - val_mse: 0.8841\n",
-      "Epoch 4/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m38s\u001b[0m 769ms/step - cosine: 0.4711 - loss: 0.3707 - mae: 0.3319 - mse: 0.2368 - val_cosine: 0.2471 - val_loss: 0.9974 - val_mae: 0.4525 - val_mse: 0.8693\n",
-      "Epoch 5/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m52s\u001b[0m 1s/step - cosine: 0.4974 - loss: 0.3347 - mae: 0.3098 - mse: 0.2085 - val_cosine: 0.2387 - val_loss: 0.9550 - val_mae: 0.4517 - val_mse: 0.8345\n",
-      "Epoch 6/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m38s\u001b[0m 769ms/step - cosine: 0.5114 - loss: 0.3075 - mae: 0.2940 - mse: 0.1889 - val_cosine: 0.3069 - val_loss: 0.9090 - val_mae: 0.4500 - val_mse: 0.7960\n",
-      "Epoch 7/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m31s\u001b[0m 633ms/step - cosine: 0.5253 - loss: 0.2807 - mae: 0.2785 - mse: 0.1694 - val_cosine: 0.0917 - val_loss: 0.8503 - val_mae: 0.4423 - val_mse: 0.7444\n",
-      "Epoch 8/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m44s\u001b[0m 891ms/step - cosine: 0.5421 - loss: 0.2650 - mae: 0.2711 - mse: 0.1608 - val_cosine: 0.0542 - val_loss: 0.7833 - val_mae: 0.4354 - val_mse: 0.6842\n",
-      "Epoch 9/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m51s\u001b[0m 1s/step - cosine: 0.5578 - loss: 0.2446 - mae: 0.2583 - mse: 0.1470 - val_cosine: 0.1658 - val_loss: 0.6950 - val_mae: 0.4125 - val_mse: 0.6022\n",
-      "Epoch 10/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m51s\u001b[0m 1s/step - cosine: 0.5738 - loss: 0.2327 - mae: 0.2534 - mse: 0.1414 - val_cosine: 0.4603 - val_loss: 0.6101 - val_mae: 0.3853 - val_mse: 0.5233\n",
-      "Epoch 11/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m20s\u001b[0m 394ms/step - cosine: 0.5944 - loss: 0.2191 - mae: 0.2457 - mse: 0.1338 - val_cosine: 0.4658 - val_loss: 0.5266 - val_mae: 0.3602 - val_mse: 0.4454\n",
-      "Epoch 12/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m48s\u001b[0m 969ms/step - cosine: 0.6062 - loss: 0.2090 - mae: 0.2406 - mse: 0.1291 - val_cosine: 0.4720 - val_loss: 0.4508 - val_mae: 0.3403 - val_mse: 0.3747\n",
-      "Epoch 13/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m50s\u001b[0m 1s/step - cosine: 0.6215 - loss: 0.1931 - mae: 0.2297 - mse: 0.1182 - val_cosine: 0.4664 - val_loss: 0.3970 - val_mae: 0.3272 - val_mse: 0.3257\n",
-      "Epoch 14/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m50s\u001b[0m 1s/step - cosine: 0.6284 - loss: 0.1869 - mae: 0.2279 - mse: 0.1167 - val_cosine: 0.4988 - val_loss: 0.3484 - val_mae: 0.3078 - val_mse: 0.2815\n",
-      "Epoch 15/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m47s\u001b[0m 947ms/step - cosine: 0.6372 - loss: 0.1785 - mae: 0.2234 - mse: 0.1125 - val_cosine: 0.5839 - val_loss: 0.2929 - val_mae: 0.2845 - val_mse: 0.2300\n",
-      "Epoch 16/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m39s\u001b[0m 783ms/step - cosine: 0.6423 - loss: 0.1701 - mae: 0.2190 - mse: 0.1081 - val_cosine: 0.6217 - val_loss: 0.2372 - val_mae: 0.2521 - val_mse: 0.1779\n",
-      "Epoch 17/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m49s\u001b[0m 995ms/step - cosine: 0.6378 - loss: 0.1681 - mae: 0.2211 - mse: 0.1096 - val_cosine: 0.6379 - val_loss: 0.2117 - val_mae: 0.2411 - val_mse: 0.1558\n",
-      "Epoch 18/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m35s\u001b[0m 714ms/step - cosine: 0.6437 - loss: 0.1609 - mae: 0.2170 - mse: 0.1057 - val_cosine: 0.5878 - val_loss: 0.2094 - val_mae: 0.2419 - val_mse: 0.1566\n",
-      "Epoch 19/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m51s\u001b[0m 1s/step - cosine: 0.6424 - loss: 0.1598 - mae: 0.2182 - mse: 0.1076 - val_cosine: 0.6757 - val_loss: 0.1633 - val_mae: 0.2073 - val_mse: 0.1132\n",
-      "Epoch 20/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m51s\u001b[0m 1s/step - cosine: 0.6513 - loss: 0.1533 - mae: 0.2140 - mse: 0.1038 - val_cosine: 0.6543 - val_loss: 0.1584 - val_mae: 0.2099 - val_mse: 0.1108\n",
-      "Epoch 21/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m50s\u001b[0m 1s/step - cosine: 0.6545 - loss: 0.1494 - mae: 0.2132 - mse: 0.1024 - val_cosine: 0.6635 - val_loss: 0.1500 - val_mae: 0.2076 - val_mse: 0.1047\n",
-      "Epoch 22/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m40s\u001b[0m 812ms/step - cosine: 0.6556 - loss: 0.1448 - mae: 0.2096 - mse: 0.1001 - val_cosine: 0.6840 - val_loss: 0.1482 - val_mae: 0.2054 - val_mse: 0.1050\n",
-      "Epoch 23/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m52s\u001b[0m 1s/step - cosine: 0.6538 - loss: 0.1439 - mae: 0.2115 - mse: 0.1012 - val_cosine: 0.6901 - val_loss: 0.1333 - val_mae: 0.1954 - val_mse: 0.0920\n",
-      "Epoch 24/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m52s\u001b[0m 1s/step - cosine: 0.6564 - loss: 0.1427 - mae: 0.2119 - mse: 0.1019 - val_cosine: 0.7098 - val_loss: 0.1287 - val_mae: 0.1919 - val_mse: 0.0891\n",
-      "Epoch 25/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m50s\u001b[0m 1s/step - cosine: 0.6569 - loss: 0.1414 - mae: 0.2118 - mse: 0.1023 - val_cosine: 0.7069 - val_loss: 0.1211 - val_mae: 0.1882 - val_mse: 0.0831\n",
-      "Epoch 26/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m50s\u001b[0m 1s/step - cosine: 0.6541 - loss: 0.1399 - mae: 0.2121 - mse: 0.1023 - val_cosine: 0.7056 - val_loss: 0.1172 - val_mae: 0.1867 - val_mse: 0.0806\n",
-      "Epoch 27/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m51s\u001b[0m 1s/step - cosine: 0.6538 - loss: 0.1371 - mae: 0.2112 - mse: 0.1008 - val_cosine: 0.7265 - val_loss: 0.1085 - val_mae: 0.1767 - val_mse: 0.0732\n",
-      "Epoch 28/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m40s\u001b[0m 819ms/step - cosine: 0.6564 - loss: 0.1359 - mae: 0.2115 - mse: 0.1008 - val_cosine: 0.7088 - val_loss: 0.1057 - val_mae: 0.1768 - val_mse: 0.0715\n",
-      "Epoch 29/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m45s\u001b[0m 907ms/step - cosine: 0.6645 - loss: 0.1334 - mae: 0.2095 - mse: 0.0995 - val_cosine: 0.7276 - val_loss: 0.1037 - val_mae: 0.1747 - val_mse: 0.0706\n",
-      "Epoch 30/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m50s\u001b[0m 1s/step - cosine: 0.6587 - loss: 0.1303 - mae: 0.2072 - mse: 0.0973 - val_cosine: 0.7141 - val_loss: 0.0985 - val_mae: 0.1720 - val_mse: 0.0663\n",
-      "Epoch 31/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m50s\u001b[0m 1s/step - cosine: 0.6628 - loss: 0.1304 - mae: 0.2068 - mse: 0.0983 - val_cosine: 0.6357 - val_loss: 0.0993 - val_mae: 0.1810 - val_mse: 0.0678\n",
-      "Epoch 32/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m51s\u001b[0m 1s/step - cosine: 0.6616 - loss: 0.1273 - mae: 0.2055 - mse: 0.0961 - val_cosine: 0.7752 - val_loss: 0.0918 - val_mae: 0.1570 - val_mse: 0.0611\n",
-      "Epoch 33/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m51s\u001b[0m 1s/step - cosine: 0.6631 - loss: 0.1272 - mae: 0.2063 - mse: 0.0967 - val_cosine: 0.7410 - val_loss: 0.0888 - val_mae: 0.1594 - val_mse: 0.0588\n",
-      "Epoch 34/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m51s\u001b[0m 1s/step - cosine: 0.6680 - loss: 0.1235 - mae: 0.2024 - mse: 0.0936 - val_cosine: 0.7340 - val_loss: 0.0862 - val_mae: 0.1581 - val_mse: 0.0567\n",
-      "Epoch 35/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m51s\u001b[0m 1s/step - cosine: 0.6640 - loss: 0.1236 - mae: 0.2035 - mse: 0.0943 - val_cosine: 0.7692 - val_loss: 0.0800 - val_mae: 0.1452 - val_mse: 0.0511\n",
-      "Epoch 36/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 752ms/step - cosine: 0.6683 - loss: 0.1233 - mae: 0.2038 - mse: 0.0946 - val_cosine: 0.7843 - val_loss: 0.0762 - val_mae: 0.1377 - val_mse: 0.0478\n",
-      "Epoch 37/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m31s\u001b[0m 627ms/step - cosine: 0.6669 - loss: 0.1219 - mae: 0.2021 - mse: 0.0936 - val_cosine: 0.7967 - val_loss: 0.0747 - val_mae: 0.1344 - val_mse: 0.0467\n",
-      "Epoch 38/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m44s\u001b[0m 891ms/step - cosine: 0.6654 - loss: 0.1222 - mae: 0.2037 - mse: 0.0942 - val_cosine: 0.7940 - val_loss: 0.0722 - val_mae: 0.1310 - val_mse: 0.0445\n",
-      "Epoch 39/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m38s\u001b[0m 772ms/step - cosine: 0.6618 - loss: 0.1229 - mae: 0.2053 - mse: 0.0952 - val_cosine: 0.7925 - val_loss: 0.0700 - val_mae: 0.1273 - val_mse: 0.0426\n",
-      "Epoch 40/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m28s\u001b[0m 573ms/step - cosine: 0.6638 - loss: 0.1231 - mae: 0.2046 - mse: 0.0958 - val_cosine: 0.8037 - val_loss: 0.0681 - val_mae: 0.1230 - val_mse: 0.0409\n",
-      "Epoch 41/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m51s\u001b[0m 1s/step - cosine: 0.6652 - loss: 0.1219 - mae: 0.2052 - mse: 0.0948 - val_cosine: 0.8067 - val_loss: 0.0668 - val_mae: 0.1218 - val_mse: 0.0398\n",
-      "Epoch 42/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m43s\u001b[0m 874ms/step - cosine: 0.6717 - loss: 0.1201 - mae: 0.2013 - mse: 0.0931 - val_cosine: 0.8054 - val_loss: 0.0660 - val_mae: 0.1203 - val_mse: 0.0392\n",
-      "Epoch 43/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m44s\u001b[0m 889ms/step - cosine: 0.6696 - loss: 0.1207 - mae: 0.2025 - mse: 0.0939 - val_cosine: 0.8053 - val_loss: 0.0654 - val_mae: 0.1198 - val_mse: 0.0387\n",
-      "Epoch 44/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m29s\u001b[0m 582ms/step - cosine: 0.6657 - loss: 0.1212 - mae: 0.2038 - mse: 0.0945 - val_cosine: 0.8061 - val_loss: 0.0648 - val_mae: 0.1183 - val_mse: 0.0382\n",
-      "Epoch 45/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m32s\u001b[0m 642ms/step - cosine: 0.6696 - loss: 0.1199 - mae: 0.2021 - mse: 0.0933 - val_cosine: 0.8034 - val_loss: 0.0644 - val_mae: 0.1171 - val_mse: 0.0378\n",
-      "Epoch 46/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m27s\u001b[0m 552ms/step - cosine: 0.6688 - loss: 0.1219 - mae: 0.2040 - mse: 0.0953 - val_cosine: 0.8081 - val_loss: 0.0643 - val_mae: 0.1182 - val_mse: 0.0378\n",
-      "Epoch 47/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m26s\u001b[0m 519ms/step - cosine: 0.6690 - loss: 0.1195 - mae: 0.2024 - mse: 0.0930 - val_cosine: 0.8092 - val_loss: 0.0637 - val_mae: 0.1166 - val_mse: 0.0373\n",
-      "Epoch 48/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 754ms/step - cosine: 0.6697 - loss: 0.1203 - mae: 0.2032 - mse: 0.0938 - val_cosine: 0.8091 - val_loss: 0.0636 - val_mae: 0.1163 - val_mse: 0.0371\n",
-      "Epoch 49/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m42s\u001b[0m 851ms/step - cosine: 0.6629 - loss: 0.1242 - mae: 0.2057 - mse: 0.0978 - val_cosine: 0.8085 - val_loss: 0.0635 - val_mae: 0.1161 - val_mse: 0.0370\n",
-      "Epoch 50/50\n",
-      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m45s\u001b[0m 923ms/step - cosine: 0.6663 - loss: 0.1214 - mae: 0.2032 - mse: 0.0949 - val_cosine: 0.8085 - val_loss: 0.0633 - val_mae: 0.1158 - val_mse: 0.0369\n"
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m16s\u001b[0m 51ms/step - cos: 0.1007 - loss: 1.3767 - mae: 0.7174 - mse: 1.0393 - snr: -4.2311 - val_cos: 0.2703 - val_loss: 1.0319 - val_mae: 0.3945 - val_mse: 0.7054 - val_snr: -0.0017\n",
+      "Epoch 2/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 18ms/step - cos: 0.2336 - loss: 0.5753 - mae: 0.3641 - mse: 0.2541 - snr: 3.7060 - val_cos: 0.2679 - val_loss: 1.0092 - val_mae: 0.3928 - val_mse: 0.7053 - val_snr: -0.0016\n",
+      "Epoch 3/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 18ms/step - cos: 0.3244 - loss: 0.4547 - mae: 0.2796 - mse: 0.1569 - snr: 6.1683 - val_cos: 0.2662 - val_loss: 0.9830 - val_mae: 0.3954 - val_mse: 0.7038 - val_snr: 0.0078\n",
+      "Epoch 4/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 32ms/step - cos: 0.4013 - loss: 0.3898 - mae: 0.2370 - mse: 0.1169 - snr: 7.7768 - val_cos: 0.1827 - val_loss: 0.9553 - val_mae: 0.3978 - val_mse: 0.7007 - val_snr: 0.0266\n",
+      "Epoch 5/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 33ms/step - cos: 0.4554 - loss: 0.3489 - mae: 0.2162 - mse: 0.1003 - snr: 8.4476 - val_cos: 0.1883 - val_loss: 0.9265 - val_mae: 0.3962 - val_mse: 0.6955 - val_snr: 0.0580\n",
+      "Epoch 6/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 35ms/step - cos: 0.5028 - loss: 0.3146 - mae: 0.2012 - mse: 0.0892 - snr: 8.7577 - val_cos: 0.2073 - val_loss: 0.8936 - val_mae: 0.3928 - val_mse: 0.6847 - val_snr: 0.1259\n",
+      "Epoch 7/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 34ms/step - cos: 0.5492 - loss: 0.2815 - mae: 0.1864 - mse: 0.0777 - snr: 9.5363 - val_cos: 0.1099 - val_loss: 0.8528 - val_mae: 0.3929 - val_mse: 0.6641 - val_snr: 0.2579\n",
+      "Epoch 8/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 35ms/step - cos: 0.6010 - loss: 0.2512 - mae: 0.1712 - mse: 0.0673 - snr: 10.2711 - val_cos: 0.1661 - val_loss: 0.8020 - val_mae: 0.3849 - val_mse: 0.6319 - val_snr: 0.4717\n",
+      "Epoch 9/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 31ms/step - cos: 0.6311 - loss: 0.2277 - mae: 0.1629 - mse: 0.0619 - snr: 10.5462 - val_cos: 0.2125 - val_loss: 0.7477 - val_mae: 0.3756 - val_mse: 0.5942 - val_snr: 0.7327\n",
+      "Epoch 10/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 37ms/step - cos: 0.6549 - loss: 0.2070 - mae: 0.1554 - mse: 0.0574 - snr: 10.8175 - val_cos: 0.1299 - val_loss: 0.6832 - val_mae: 0.3679 - val_mse: 0.5447 - val_snr: 1.1079\n",
+      "Epoch 11/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 38ms/step - cos: 0.6779 - loss: 0.1865 - mae: 0.1463 - mse: 0.0515 - snr: 11.0805 - val_cos: 0.2047 - val_loss: 0.6205 - val_mae: 0.3523 - val_mse: 0.4953 - val_snr: 1.5123\n",
+      "Epoch 12/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 37ms/step - cos: 0.7079 - loss: 0.1676 - mae: 0.1369 - mse: 0.0455 - snr: 11.3521 - val_cos: 0.3223 - val_loss: 0.5628 - val_mae: 0.3347 - val_mse: 0.4495 - val_snr: 1.9304\n",
+      "Epoch 13/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 33ms/step - cos: 0.7250 - loss: 0.1511 - mae: 0.1286 - mse: 0.0405 - snr: 12.7226 - val_cos: 0.4194 - val_loss: 0.5233 - val_mae: 0.3210 - val_mse: 0.4206 - val_snr: 2.2110\n",
+      "Epoch 14/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 33ms/step - cos: 0.7275 - loss: 0.1400 - mae: 0.1283 - mse: 0.0397 - snr: 12.2387 - val_cos: 0.5007 - val_loss: 0.4881 - val_mae: 0.3081 - val_mse: 0.3948 - val_snr: 2.4814\n",
+      "Epoch 15/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 36ms/step - cos: 0.7397 - loss: 0.1276 - mae: 0.1220 - mse: 0.0365 - snr: 12.6243 - val_cos: 0.5350 - val_loss: 0.4388 - val_mae: 0.2936 - val_mse: 0.3538 - val_snr: 2.9598\n",
+      "Epoch 16/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 37ms/step - cos: 0.7444 - loss: 0.1196 - mae: 0.1222 - mse: 0.0366 - snr: 13.0157 - val_cos: 0.5199 - val_loss: 0.4065 - val_mae: 0.2870 - val_mse: 0.3289 - val_snr: 3.2739\n",
+      "Epoch 17/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 33ms/step - cos: 0.7604 - loss: 0.1086 - mae: 0.1150 - mse: 0.0327 - snr: 13.5769 - val_cos: 0.5125 - val_loss: 0.3781 - val_mae: 0.2835 - val_mse: 0.3071 - val_snr: 3.5680\n",
+      "Epoch 18/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 31ms/step - cos: 0.7563 - loss: 0.1031 - mae: 0.1169 - mse: 0.0336 - snr: 12.8908 - val_cos: 0.4988 - val_loss: 0.3691 - val_mae: 0.2845 - val_mse: 0.3038 - val_snr: 3.6137\n",
+      "Epoch 19/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 34ms/step - cos: 0.7582 - loss: 0.0953 - mae: 0.1134 - mse: 0.0314 - snr: 13.0128 - val_cos: 0.4503 - val_loss: 0.3642 - val_mae: 0.2880 - val_mse: 0.3042 - val_snr: 3.6095\n",
+      "Epoch 20/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 35ms/step - cos: 0.7610 - loss: 0.0894 - mae: 0.1125 - mse: 0.0305 - snr: 13.3877 - val_cos: 0.3801 - val_loss: 0.3525 - val_mae: 0.2941 - val_mse: 0.2970 - val_snr: 3.7022\n",
+      "Epoch 21/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 33ms/step - cos: 0.7672 - loss: 0.0838 - mae: 0.1098 - mse: 0.0294 - snr: 13.3445 - val_cos: 0.3764 - val_loss: 0.3439 - val_mae: 0.2931 - val_mse: 0.2925 - val_snr: 3.7707\n",
+      "Epoch 22/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 31ms/step - cos: 0.7720 - loss: 0.0787 - mae: 0.1080 - mse: 0.0282 - snr: 13.6978 - val_cos: 0.3703 - val_loss: 0.3287 - val_mae: 0.2916 - val_mse: 0.2808 - val_snr: 3.9512\n",
+      "Epoch 23/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 33ms/step - cos: 0.7776 - loss: 0.0746 - mae: 0.1062 - mse: 0.0276 - snr: 13.8593 - val_cos: 0.3259 - val_loss: 0.3181 - val_mae: 0.2954 - val_mse: 0.2735 - val_snr: 4.0665\n",
+      "Epoch 24/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 32ms/step - cos: 0.7790 - loss: 0.0707 - mae: 0.1050 - mse: 0.0268 - snr: 13.4164 - val_cos: 0.4684 - val_loss: 0.2927 - val_mae: 0.2706 - val_mse: 0.2510 - val_snr: 4.4632\n",
+      "Epoch 25/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 33ms/step - cos: 0.7770 - loss: 0.0687 - mae: 0.1063 - mse: 0.0276 - snr: 13.9392 - val_cos: 0.4359 - val_loss: 0.2624 - val_mae: 0.2647 - val_mse: 0.2233 - val_snr: 4.9690\n",
+      "Epoch 26/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 27ms/step - cos: 0.7880 - loss: 0.0641 - mae: 0.1010 - mse: 0.0256 - snr: 14.5248 - val_cos: 0.3473 - val_loss: 0.2633 - val_mae: 0.2757 - val_mse: 0.2266 - val_snr: 4.9171\n",
+      "Epoch 27/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 34ms/step - cos: 0.7875 - loss: 0.0612 - mae: 0.1012 - mse: 0.0250 - snr: 15.0129 - val_cos: 0.5326 - val_loss: 0.2299 - val_mae: 0.2411 - val_mse: 0.1952 - val_snr: 5.5732\n",
+      "Epoch 28/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 36ms/step - cos: 0.7901 - loss: 0.0587 - mae: 0.1009 - mse: 0.0245 - snr: 14.6143 - val_cos: 0.6635 - val_loss: 0.2143 - val_mae: 0.2182 - val_mse: 0.1815 - val_snr: 5.9250\n",
+      "Epoch 29/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 31ms/step - cos: 0.7879 - loss: 0.0566 - mae: 0.1010 - mse: 0.0242 - snr: 14.2431 - val_cos: 0.6997 - val_loss: 0.1920 - val_mae: 0.2055 - val_mse: 0.1610 - val_snr: 6.4625\n",
+      "Epoch 30/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 31ms/step - cos: 0.7946 - loss: 0.0538 - mae: 0.0981 - mse: 0.0231 - snr: 15.0658 - val_cos: 0.6248 - val_loss: 0.1974 - val_mae: 0.2174 - val_mse: 0.1680 - val_snr: 6.2441\n",
+      "Epoch 31/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 35ms/step - cos: 0.7951 - loss: 0.0528 - mae: 0.0987 - mse: 0.0238 - snr: 15.2206 - val_cos: 0.6176 - val_loss: 0.2029 - val_mae: 0.2191 - val_mse: 0.1750 - val_snr: 6.0858\n",
+      "Epoch 32/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 35ms/step - cos: 0.7915 - loss: 0.0516 - mae: 0.0995 - mse: 0.0240 - snr: 14.4698 - val_cos: 0.6997 - val_loss: 0.1747 - val_mae: 0.1981 - val_mse: 0.1482 - val_snr: 6.8104\n",
+      "Epoch 33/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 31ms/step - cos: 0.7989 - loss: 0.0487 - mae: 0.0969 - mse: 0.0225 - snr: 15.0472 - val_cos: 0.6682 - val_loss: 0.1698 - val_mae: 0.1999 - val_mse: 0.1444 - val_snr: 6.9262\n",
+      "Epoch 34/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 37ms/step - cos: 0.7973 - loss: 0.0473 - mae: 0.0956 - mse: 0.0223 - snr: 15.3917 - val_cos: 0.6686 - val_loss: 0.1694 - val_mae: 0.1982 - val_mse: 0.1452 - val_snr: 6.9149\n",
+      "Epoch 35/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 37ms/step - cos: 0.7924 - loss: 0.0472 - mae: 0.0985 - mse: 0.0233 - snr: 14.3126 - val_cos: 0.7566 - val_loss: 0.1424 - val_mae: 0.1767 - val_mse: 0.1193 - val_snr: 7.7728\n",
+      "Epoch 36/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 31ms/step - cos: 0.8020 - loss: 0.0442 - mae: 0.0936 - mse: 0.0213 - snr: 15.2554 - val_cos: 0.7495 - val_loss: 0.1363 - val_mae: 0.1757 - val_mse: 0.1141 - val_snr: 7.9693\n",
+      "Epoch 37/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 29ms/step - cos: 0.8069 - loss: 0.0433 - mae: 0.0936 - mse: 0.0213 - snr: 15.9400 - val_cos: 0.7005 - val_loss: 0.1504 - val_mae: 0.1865 - val_mse: 0.1292 - val_snr: 7.4212\n",
+      "Epoch 38/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 33ms/step - cos: 0.8037 - loss: 0.0424 - mae: 0.0944 - mse: 0.0214 - snr: 15.0481 - val_cos: 0.6872 - val_loss: 0.1420 - val_mae: 0.1837 - val_mse: 0.1215 - val_snr: 7.7206\n",
+      "Epoch 39/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 33ms/step - cos: 0.8039 - loss: 0.0412 - mae: 0.0927 - mse: 0.0209 - snr: 15.3849 - val_cos: 0.6729 - val_loss: 0.1296 - val_mae: 0.1805 - val_mse: 0.1099 - val_snr: 8.1422\n",
+      "Epoch 40/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 30ms/step - cos: 0.8057 - loss: 0.0404 - mae: 0.0924 - mse: 0.0209 - snr: 15.1577 - val_cos: 0.6925 - val_loss: 0.1342 - val_mae: 0.1806 - val_mse: 0.1152 - val_snr: 7.9484\n",
+      "Epoch 41/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 35ms/step - cos: 0.8108 - loss: 0.0390 - mae: 0.0915 - mse: 0.0203 - snr: 15.0379 - val_cos: 0.6835 - val_loss: 0.1117 - val_mae: 0.1692 - val_mse: 0.0934 - val_snr: 8.8438\n",
+      "Epoch 42/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 33ms/step - cos: 0.8044 - loss: 0.0392 - mae: 0.0934 - mse: 0.0211 - snr: 14.9561 - val_cos: 0.6988 - val_loss: 0.1058 - val_mae: 0.1669 - val_mse: 0.0881 - val_snr: 9.0532\n",
+      "Epoch 43/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 29ms/step - cos: 0.8124 - loss: 0.0376 - mae: 0.0911 - mse: 0.0201 - snr: 15.2787 - val_cos: 0.5931 - val_loss: 0.1177 - val_mae: 0.1843 - val_mse: 0.1007 - val_snr: 8.5046\n",
+      "Epoch 44/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 33ms/step - cos: 0.8109 - loss: 0.0363 - mae: 0.0891 - mse: 0.0194 - snr: 15.2912 - val_cos: 0.6494 - val_loss: 0.1075 - val_mae: 0.1741 - val_mse: 0.0910 - val_snr: 8.9004\n",
+      "Epoch 45/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 33ms/step - cos: 0.8141 - loss: 0.0356 - mae: 0.0891 - mse: 0.0193 - snr: 15.2001 - val_cos: 0.6863 - val_loss: 0.1130 - val_mae: 0.1728 - val_mse: 0.0970 - val_snr: 8.6605\n",
+      "Epoch 46/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 35ms/step - cos: 0.8078 - loss: 0.0369 - mae: 0.0930 - mse: 0.0210 - snr: 14.5843 - val_cos: 0.6935 - val_loss: 0.0959 - val_mae: 0.1626 - val_mse: 0.0804 - val_snr: 9.4516\n",
+      "Epoch 47/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 35ms/step - cos: 0.8103 - loss: 0.0353 - mae: 0.0904 - mse: 0.0199 - snr: 15.7789 - val_cos: 0.7251 - val_loss: 0.0915 - val_mae: 0.1560 - val_mse: 0.0765 - val_snr: 9.6838\n",
+      "Epoch 48/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 27ms/step - cos: 0.8127 - loss: 0.0341 - mae: 0.0891 - mse: 0.0192 - snr: 16.5469 - val_cos: 0.7214 - val_loss: 0.0926 - val_mae: 0.1577 - val_mse: 0.0780 - val_snr: 9.6104\n",
+      "Epoch 49/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 35ms/step - cos: 0.8171 - loss: 0.0327 - mae: 0.0871 - mse: 0.0182 - snr: 16.5750 - val_cos: 0.7358 - val_loss: 0.0838 - val_mae: 0.1494 - val_mse: 0.0696 - val_snr: 10.1036\n",
+      "Epoch 50/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 34ms/step - cos: 0.8028 - loss: 0.0353 - mae: 0.0947 - mse: 0.0212 - snr: 14.5850 - val_cos: 0.7540 - val_loss: 0.0753 - val_mae: 0.1480 - val_mse: 0.0615 - val_snr: 10.5979\n",
+      "Epoch 51/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 31ms/step - cos: 0.8126 - loss: 0.0332 - mae: 0.0895 - mse: 0.0195 - snr: 16.2668 - val_cos: 0.7027 - val_loss: 0.0914 - val_mae: 0.1584 - val_mse: 0.0779 - val_snr: 9.6309\n",
+      "Epoch 52/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 31ms/step - cos: 0.8142 - loss: 0.0322 - mae: 0.0886 - mse: 0.0188 - snr: 15.1897 - val_cos: 0.7682 - val_loss: 0.0797 - val_mae: 0.1425 - val_mse: 0.0665 - val_snr: 10.2973\n",
+      "Epoch 53/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 33ms/step - cos: 0.8129 - loss: 0.0326 - mae: 0.0900 - mse: 0.0195 - snr: 15.5567 - val_cos: 0.7554 - val_loss: 0.0746 - val_mae: 0.1433 - val_mse: 0.0618 - val_snr: 10.5957\n",
+      "Epoch 54/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 24ms/step - cos: 0.8123 - loss: 0.0322 - mae: 0.0896 - mse: 0.0195 - snr: 15.3820 - val_cos: 0.6952 - val_loss: 0.0748 - val_mae: 0.1518 - val_mse: 0.0622 - val_snr: 10.5582\n",
+      "Epoch 55/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 27ms/step - cos: 0.8218 - loss: 0.0304 - mae: 0.0853 - mse: 0.0179 - snr: 16.4468 - val_cos: 0.7383 - val_loss: 0.0686 - val_mae: 0.1399 - val_mse: 0.0563 - val_snr: 11.0081\n",
+      "Epoch 56/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 22ms/step - cos: 0.8137 - loss: 0.0305 - mae: 0.0877 - mse: 0.0183 - snr: 15.4239 - val_cos: 0.7563 - val_loss: 0.0651 - val_mae: 0.1356 - val_mse: 0.0531 - val_snr: 11.2572\n",
+      "Epoch 57/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 27ms/step - cos: 0.8176 - loss: 0.0309 - mae: 0.0883 - mse: 0.0190 - snr: 15.8349 - val_cos: 0.7482 - val_loss: 0.0563 - val_mae: 0.1298 - val_mse: 0.0445 - val_snr: 12.0380\n",
+      "Epoch 58/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 22ms/step - cos: 0.8229 - loss: 0.0298 - mae: 0.0865 - mse: 0.0181 - snr: 15.4568 - val_cos: 0.7121 - val_loss: 0.0618 - val_mae: 0.1381 - val_mse: 0.0503 - val_snr: 11.4883\n",
+      "Epoch 59/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 29ms/step - cos: 0.8189 - loss: 0.0297 - mae: 0.0867 - mse: 0.0182 - snr: 15.8944 - val_cos: 0.7772 - val_loss: 0.0558 - val_mae: 0.1253 - val_mse: 0.0445 - val_snr: 12.0029\n",
+      "Epoch 60/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 21ms/step - cos: 0.8167 - loss: 0.0292 - mae: 0.0865 - mse: 0.0179 - snr: 16.4350 - val_cos: 0.7391 - val_loss: 0.0634 - val_mae: 0.1358 - val_mse: 0.0524 - val_snr: 11.3357\n",
+      "Epoch 61/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 27ms/step - cos: 0.8167 - loss: 0.0291 - mae: 0.0872 - mse: 0.0181 - snr: 15.7276 - val_cos: 0.7804 - val_loss: 0.0539 - val_mae: 0.1253 - val_mse: 0.0430 - val_snr: 12.1341\n",
+      "Epoch 62/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 28ms/step - cos: 0.8181 - loss: 0.0294 - mae: 0.0867 - mse: 0.0186 - snr: 16.1558 - val_cos: 0.7621 - val_loss: 0.0524 - val_mae: 0.1272 - val_mse: 0.0417 - val_snr: 12.2439\n",
+      "Epoch 63/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 22ms/step - cos: 0.8079 - loss: 0.0308 - mae: 0.0911 - mse: 0.0202 - snr: 15.5905 - val_cos: 0.7695 - val_loss: 0.0555 - val_mae: 0.1271 - val_mse: 0.0449 - val_snr: 11.9835\n",
+      "Epoch 64/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 29ms/step - cos: 0.8296 - loss: 0.0269 - mae: 0.0814 - mse: 0.0165 - snr: 16.4142 - val_cos: 0.7906 - val_loss: 0.0421 - val_mae: 0.1112 - val_mse: 0.0317 - val_snr: 13.4501\n",
+      "Epoch 65/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 21ms/step - cos: 0.8187 - loss: 0.0277 - mae: 0.0854 - mse: 0.0174 - snr: 16.0411 - val_cos: 0.7838 - val_loss: 0.0482 - val_mae: 0.1190 - val_mse: 0.0380 - val_snr: 12.6759\n",
+      "Epoch 66/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 27ms/step - cos: 0.8100 - loss: 0.0295 - mae: 0.0894 - mse: 0.0193 - snr: 15.3396 - val_cos: 0.8025 - val_loss: 0.0454 - val_mae: 0.1133 - val_mse: 0.0353 - val_snr: 12.9731\n",
+      "Epoch 67/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 28ms/step - cos: 0.8188 - loss: 0.0283 - mae: 0.0864 - mse: 0.0182 - snr: 16.0004 - val_cos: 0.7764 - val_loss: 0.0436 - val_mae: 0.1164 - val_mse: 0.0337 - val_snr: 13.1719\n",
+      "Epoch 68/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 26ms/step - cos: 0.8202 - loss: 0.0280 - mae: 0.0865 - mse: 0.0181 - snr: 16.2730 - val_cos: 0.7787 - val_loss: 0.0477 - val_mae: 0.1198 - val_mse: 0.0379 - val_snr: 12.6531\n",
+      "Epoch 69/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 25ms/step - cos: 0.8280 - loss: 0.0271 - mae: 0.0832 - mse: 0.0173 - snr: 16.3682 - val_cos: 0.7794 - val_loss: 0.0456 - val_mae: 0.1169 - val_mse: 0.0359 - val_snr: 12.9183\n",
+      "Epoch 70/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 26ms/step - cos: 0.8217 - loss: 0.0271 - mae: 0.0847 - mse: 0.0174 - snr: 16.6814 - val_cos: 0.7840 - val_loss: 0.0449 - val_mae: 0.1155 - val_mse: 0.0354 - val_snr: 12.9648\n",
+      "Epoch 71/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 30ms/step - cos: 0.8285 - loss: 0.0267 - mae: 0.0829 - mse: 0.0171 - snr: 16.6853 - val_cos: 0.8066 - val_loss: 0.0376 - val_mae: 0.1039 - val_mse: 0.0281 - val_snr: 13.9514\n",
+      "Epoch 72/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 24ms/step - cos: 0.8234 - loss: 0.0267 - mae: 0.0847 - mse: 0.0173 - snr: 16.2299 - val_cos: 0.8104 - val_loss: 0.0408 - val_mae: 0.1069 - val_mse: 0.0315 - val_snr: 13.4943\n",
+      "Epoch 73/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 28ms/step - cos: 0.8229 - loss: 0.0265 - mae: 0.0836 - mse: 0.0172 - snr: 16.1746 - val_cos: 0.8173 - val_loss: 0.0349 - val_mae: 0.0986 - val_mse: 0.0256 - val_snr: 14.3747\n",
+      "Epoch 74/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 22ms/step - cos: 0.8256 - loss: 0.0265 - mae: 0.0830 - mse: 0.0172 - snr: 15.9057 - val_cos: 0.8169 - val_loss: 0.0379 - val_mae: 0.1032 - val_mse: 0.0287 - val_snr: 13.8832\n",
+      "Epoch 75/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 22ms/step - cos: 0.8245 - loss: 0.0267 - mae: 0.0837 - mse: 0.0175 - snr: 16.2338 - val_cos: 0.8137 - val_loss: 0.0362 - val_mae: 0.1011 - val_mse: 0.0271 - val_snr: 14.1791\n",
+      "Epoch 76/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 24ms/step - cos: 0.8236 - loss: 0.0265 - mae: 0.0830 - mse: 0.0174 - snr: 16.2230 - val_cos: 0.8141 - val_loss: 0.0360 - val_mae: 0.1005 - val_mse: 0.0270 - val_snr: 14.1546\n",
+      "Epoch 77/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 30ms/step - cos: 0.8213 - loss: 0.0266 - mae: 0.0854 - mse: 0.0176 - snr: 15.7473 - val_cos: 0.8149 - val_loss: 0.0325 - val_mae: 0.0960 - val_mse: 0.0235 - val_snr: 14.7467\n",
+      "Epoch 78/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 26ms/step - cos: 0.8232 - loss: 0.0258 - mae: 0.0834 - mse: 0.0168 - snr: 15.6876 - val_cos: 0.8110 - val_loss: 0.0324 - val_mae: 0.0961 - val_mse: 0.0235 - val_snr: 14.8023\n",
+      "Epoch 79/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 23ms/step - cos: 0.8152 - loss: 0.0266 - mae: 0.0870 - mse: 0.0177 - snr: 15.8633 - val_cos: 0.8188 - val_loss: 0.0328 - val_mae: 0.0957 - val_mse: 0.0239 - val_snr: 14.7028\n",
+      "Epoch 80/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 23ms/step - cos: 0.8348 - loss: 0.0245 - mae: 0.0791 - mse: 0.0157 - snr: 17.1155 - val_cos: 0.8257 - val_loss: 0.0328 - val_mae: 0.0947 - val_mse: 0.0240 - val_snr: 14.6474\n",
+      "Epoch 81/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 26ms/step - cos: 0.8125 - loss: 0.0266 - mae: 0.0858 - mse: 0.0178 - snr: 16.5905 - val_cos: 0.8231 - val_loss: 0.0293 - val_mae: 0.0891 - val_mse: 0.0206 - val_snr: 15.3167\n",
+      "Epoch 82/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 20ms/step - cos: 0.8152 - loss: 0.0277 - mae: 0.0872 - mse: 0.0189 - snr: 15.6371 - val_cos: 0.8232 - val_loss: 0.0296 - val_mae: 0.0905 - val_mse: 0.0209 - val_snr: 15.2771\n",
+      "Epoch 83/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 23ms/step - cos: 0.8242 - loss: 0.0263 - mae: 0.0836 - mse: 0.0176 - snr: 15.1988 - val_cos: 0.8232 - val_loss: 0.0298 - val_mae: 0.0904 - val_mse: 0.0211 - val_snr: 15.2139\n",
+      "Epoch 84/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 26ms/step - cos: 0.8190 - loss: 0.0272 - mae: 0.0864 - mse: 0.0185 - snr: 15.4599 - val_cos: 0.8166 - val_loss: 0.0289 - val_mae: 0.0902 - val_mse: 0.0202 - val_snr: 15.4350\n",
+      "Epoch 85/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 21ms/step - cos: 0.8156 - loss: 0.0268 - mae: 0.0864 - mse: 0.0182 - snr: 15.9789 - val_cos: 0.8194 - val_loss: 0.0291 - val_mae: 0.0904 - val_mse: 0.0205 - val_snr: 15.3796\n",
+      "Epoch 86/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 27ms/step - cos: 0.8163 - loss: 0.0268 - mae: 0.0866 - mse: 0.0182 - snr: 15.2712 - val_cos: 0.8258 - val_loss: 0.0280 - val_mae: 0.0871 - val_mse: 0.0194 - val_snr: 15.6015\n",
+      "Epoch 87/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 22ms/step - cos: 0.8230 - loss: 0.0263 - mae: 0.0842 - mse: 0.0177 - snr: 15.3978 - val_cos: 0.8237 - val_loss: 0.0281 - val_mae: 0.0881 - val_mse: 0.0196 - val_snr: 15.5894\n",
+      "Epoch 88/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 23ms/step - cos: 0.8182 - loss: 0.0265 - mae: 0.0864 - mse: 0.0180 - snr: 15.0980 - val_cos: 0.8272 - val_loss: 0.0284 - val_mae: 0.0878 - val_mse: 0.0198 - val_snr: 15.5125\n",
+      "Epoch 89/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 28ms/step - cos: 0.8169 - loss: 0.0262 - mae: 0.0860 - mse: 0.0177 - snr: 15.9089 - val_cos: 0.8285 - val_loss: 0.0273 - val_mae: 0.0857 - val_mse: 0.0187 - val_snr: 15.7466\n",
+      "Epoch 90/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 29ms/step - cos: 0.8182 - loss: 0.0260 - mae: 0.0842 - mse: 0.0174 - snr: 15.6634 - val_cos: 0.8285 - val_loss: 0.0270 - val_mae: 0.0853 - val_mse: 0.0185 - val_snr: 15.8044\n",
+      "Epoch 91/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 21ms/step - cos: 0.8343 - loss: 0.0240 - mae: 0.0792 - mse: 0.0154 - snr: 17.3884 - val_cos: 0.8296 - val_loss: 0.0270 - val_mae: 0.0849 - val_mse: 0.0185 - val_snr: 15.7994\n",
+      "Epoch 92/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 26ms/step - cos: 0.8238 - loss: 0.0255 - mae: 0.0826 - mse: 0.0170 - snr: 16.4601 - val_cos: 0.8312 - val_loss: 0.0267 - val_mae: 0.0840 - val_mse: 0.0182 - val_snr: 15.8516\n",
+      "Epoch 93/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 28ms/step - cos: 0.8242 - loss: 0.0261 - mae: 0.0842 - mse: 0.0176 - snr: 15.4825 - val_cos: 0.8315 - val_loss: 0.0263 - val_mae: 0.0832 - val_mse: 0.0178 - val_snr: 15.9499\n",
+      "Epoch 94/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 25ms/step - cos: 0.8195 - loss: 0.0261 - mae: 0.0850 - mse: 0.0176 - snr: 15.9241 - val_cos: 0.8300 - val_loss: 0.0260 - val_mae: 0.0829 - val_mse: 0.0175 - val_snr: 16.0251\n",
+      "Epoch 95/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 26ms/step - cos: 0.8259 - loss: 0.0254 - mae: 0.0829 - mse: 0.0169 - snr: 16.1511 - val_cos: 0.8305 - val_loss: 0.0257 - val_mae: 0.0822 - val_mse: 0.0172 - val_snr: 16.1082\n",
+      "Epoch 96/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 24ms/step - cos: 0.8178 - loss: 0.0254 - mae: 0.0843 - mse: 0.0170 - snr: 15.9857 - val_cos: 0.8314 - val_loss: 0.0261 - val_mae: 0.0829 - val_mse: 0.0176 - val_snr: 16.0047\n",
+      "Epoch 97/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 27ms/step - cos: 0.8338 - loss: 0.0249 - mae: 0.0802 - mse: 0.0165 - snr: 16.4559 - val_cos: 0.8313 - val_loss: 0.0256 - val_mae: 0.0819 - val_mse: 0.0171 - val_snr: 16.1225\n",
+      "Epoch 98/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 23ms/step - cos: 0.8218 - loss: 0.0257 - mae: 0.0837 - mse: 0.0172 - snr: 16.6451 - val_cos: 0.8313 - val_loss: 0.0254 - val_mae: 0.0814 - val_mse: 0.0169 - val_snr: 16.1837\n",
+      "Epoch 99/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 19ms/step - cos: 0.8187 - loss: 0.0254 - mae: 0.0843 - mse: 0.0169 - snr: 15.8321 - val_cos: 0.8316 - val_loss: 0.0254 - val_mae: 0.0815 - val_mse: 0.0169 - val_snr: 16.1731\n",
+      "Epoch 100/100\n",
+      "\u001b[1m50/50\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 26ms/step - cos: 0.8245 - loss: 0.0244 - mae: 0.0812 - mse: 0.0159 - snr: 17.1558 - val_cos: 0.8316 - val_loss: 0.0255 - val_mae: 0.0817 - val_mse: 0.0170 - val_snr: 16.1516\n"
      ]
     }
    ],
    "source": [
-    "task.train(train_params)"
+    "history = model.fit(\n",
+    "    train_ds,\n",
+    "    steps_per_epoch=steps_per_epoch,\n",
+    "    verbose=verbose,\n",
+    "    epochs=epochs,\n",
+    "    validation_data=val_ds,\n",
+    "    callbacks=model_callbacks,\n",
+    ")"
    ]
   },
   {
@@ -798,12 +1114,12 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 13,
+   "execution_count": 17,
    "metadata": {},
    "outputs": [
     {
      "data": {
-      "image/png": "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",
+      "image/png": "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",
       "text/plain": [
        "
" ] @@ -813,15 +1129,16 @@ } ], "source": [ - "history = pd.read_csv(job_dir / \"history.csv\")\n", - "fig, ax = plt.subplots(2, 1, figsize=(9, 5))\n", - "history[[\"loss\", \"val_loss\"]].plot(ax=ax[0], color=[primary_color, secondary_color])\n", - "history[[\"cosine\", \"val_cosine\"]].plot(ax=ax[1], color=[primary_color, secondary_color])\n", - "# set x-axis label\n", - "ax[1].set_xlabel(\"Epoch\")\n", - "ax[0].set_ylabel(\"Loss\")\n", - "ax[1].set_ylabel(\"Cosine Similarity\")\n", - "plt.show()" + "fig, _ = nse.plotting.plot_history_metrics(\n", + " history.history,\n", + " metrics=[\"loss\", \"cos\"],\n", + " title=\"Training History\",\n", + " colors=[plot_theme.primary_color, plot_theme.secondary_color],\n", + " stack=True,\n", + " figsize=(9, 5),\n", + ")\n", + "fig.tight_layout()\n", + "fig.show()" ] }, { @@ -830,82 +1147,29 @@ "source": [ "## Model evaluation\n", "\n", - "Now that we have trained the model, we will evaluate the model on the test dataset. Similar to training, we will provide a high-level configuration to the task process." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "test_params = hk.HKTestParams(\n", - " job_dir=job_dir, # Directory to store all output artifacts\n", - " datasets=datasets, # Datasets to test on\n", - " sampling_rate=sampling_rate, # Target sampling rate\n", - " frame_size=frame_size, # Target frame size\n", - " test_samples_per_patient=samples_per_patient, # Samples per test patient\n", - " test_size=test_size, # Number of samples to test\n", - " test_file=val_file, # Validation file (cached)\n", - " preprocesses=preprocesses, # Preprocessing pipeline\n", - " model_file=model_file, # Model file to load\n", - " verbose=verbose # Verbosity level\n", - ")" + "Now that we have trained the model, we will evaluate the model on the test dataset. The model's built-in `evaluate` method will be used to calculate the loss and metrics on the dataset." ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 18, "metadata": {}, "outputs": [ - { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "WARNING: All log messages before absl::InitializeLog() is called are written to STDERR\n",
-      "I0000 00:00:1721319410.488057  950921 service.cc:146] XLA service 0x79fec0014730 initialized for platform CUDA (this does not guarantee that XLA will be used). Devices:\n",
-      "I0000 00:00:1721319410.488079  950921 service.cc:154]   StreamExecutor device (0): NVIDIA GeForce RTX 4090, Compute Capability 8.9\n"
-     ]
-    },
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "\u001b[1m114/157\u001b[0m \u001b[32m━━━━━━━━━━━━━━\u001b[0m\u001b[37m━━━━━━\u001b[0m \u001b[1m0s\u001b[0m 445us/step  "
-     ]
-    },
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "I0000 00:00:1721319411.201827  950921 device_compiler.h:188] Compiled cluster using XLA!  This line is logged at most once for the lifetime of the process.\n"
-     ]
-    },
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "\u001b[1m157/157\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 4ms/step\n"
+      "\u001b[1m39/39\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m0s\u001b[0m 1ms/step - cos: 0.8275 - loss: 0.0258 - mae: 0.0831 - mse: 0.0173 - snr: 16.1862\n"
      ]
     },
     {
      "data": {
       "text/html": [
-       "
[07/18/24 16:16:52] INFO     [TEST SET] MAE=11.40%, MSE=3.60%, COSSIM=98.11%                         evaluate.py:70\n",
+       "
INFO     [VAL SET] COS=0.8313, LOSS=0.0254, MAE=0.0814, MSE=0.0169, SNR=16.1837                      935393270.py:2\n",
        "
\n" ], "text/plain": [ - "\u001b[2;36m[07/18/24 16:16:52]\u001b[0m\u001b[2;36m \u001b[0m\u001b[34mINFO \u001b[0m \u001b[1m[\u001b[0mTEST SET\u001b[1m]\u001b[0m \u001b[33mMAE\u001b[0m=\u001b[1;36m11\u001b[0m\u001b[1;36m.40\u001b[0m%, \u001b[33mMSE\u001b[0m=\u001b[1;36m3\u001b[0m\u001b[1;36m.60\u001b[0m%, \u001b[33mCOSSIM\u001b[0m=\u001b[1;36m98\u001b[0m\u001b[1;36m.11\u001b[0m% \u001b]8;id=882450;file:///workspaces/heartkit/heartkit/tasks/denoise/evaluate.py\u001b\\\u001b[2mevaluate.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=36148;file:///workspaces/heartkit/heartkit/tasks/denoise/evaluate.py#70\u001b\\\u001b[2m70\u001b[0m\u001b]8;;\u001b\\\n" + "\u001b[34mINFO \u001b[0m \u001b[1m[\u001b[0mVAL SET\u001b[1m]\u001b[0m \u001b[33mCOS\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.8313\u001b[0m, \u001b[33mLOSS\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.0254\u001b[0m, \u001b[33mMAE\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.0814\u001b[0m, \u001b[33mMSE\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.0169\u001b[0m, \u001b[33mSNR\u001b[0m=\u001b[1;36m16\u001b[0m\u001b[1;36m.1837\u001b[0m \u001b]8;id=332795;file:///tmp/ipykernel_1619872/935393270.py\u001b\\\u001b[2m935393270.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=588225;file:///tmp/ipykernel_1619872/935393270.py#2\u001b\\\u001b[2m2\u001b[0m\u001b]8;;\u001b\\\n" ] }, "metadata": {}, @@ -913,7 +1177,8 @@ } ], "source": [ - "task.evaluate(test_params)" + "rst = model.evaluate(val_ds, return_dict=True)\n", + "logger.info(\"[VAL SET] \" + \", \".join([f\"{k.upper()}={v:.4f}\" for k, v in rst.items()]))" ] }, { @@ -935,129 +1200,123 @@ "metadata": {}, "outputs": [], "source": [ - "quantization = hk.QuantizationParams(\n", - " enabled=True,\n", - " format=\"FP32\",\n", - " io_type=\"float32\",\n", - " conversion=\"CONCRETE\",\n", - ")" + "# Convert validation dataset to numpy arrays\n", + "test_x = np.concatenate([x for x, _ in val_ds.as_numpy_iterator()])\n", + "test_y = np.concatenate([y for _, y in val_ds.as_numpy_iterator()])\n" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "W0000 00:00:1723573422.366791 1619872 tf_tfl_flatbuffer_helpers.cc:392] Ignored output_format.\n", + "W0000 00:00:1723573422.366803 1619872 tf_tfl_flatbuffer_helpers.cc:395] Ignored drop_control_dependency.\n" + ] + } + ], + "source": [ + "converter = nse.converters.tflite.TfLiteKerasConverter(model=model)\n", + "\n", + "# Redirect stdout and stderr to devnull since TFLite converter is very verbose\n", + "with open(os.devnull, 'w') as devnull:\n", + " with contextlib.redirect_stdout(devnull), contextlib.redirect_stderr(devnull):\n", + " tflite_content = converter.convert(\n", + " test_x=test_x,\n", + " quantization=\"FP32\",\n", + " io_type=\"float32\",\n", + " mode=\"KERAS\",\n", + " strict=False,\n", + " verbose=verbose\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Save TFLite model as both a file and C header" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, "outputs": [], "source": [ - "export_params = hk.HKExportParams(\n", - " job_dir=job_dir, # Directory to store all output artifacts\n", - " datasets=datasets, # Datasets to export on\n", - " sampling_rate=sampling_rate, # Target sampling rate\n", - " frame_size=frame_size, # Target frame size\n", - " # Test params\n", - " test_samples_per_patient=samples_per_patient, # Samples per test patient\n", - " test_size=test_size, # Number of samples to test\n", - " test_file=val_file, # Validation file (cached)\n", - " preprocesses=preprocesses, # Preprocessing pipeline\n", - " model_file=model_file, # Model file to load\n", - " val_acc_threshold=0.9, # Validation accuracy threshold\n", - " quantization=quantization, # Quantization parameters\n", - " tflm_var_name=\"rhythm\", # TFLite model variable name\n", - " tflm_file=job_dir / \"rhythm_flatbuffer.h\", # TFLite model file\n", - " verbose=verbose # Verbosity level\n", + "converter.export(\n", + " tflite_path=job_dir / \"model.tflite\"\n", + ")\n", + "\n", + "converter.export_header(\n", + " header_path=job_dir / \"model.h\",\n", + " name=\"model\",\n", ")\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Evaluate TFLite model against TensorFlow model\n", + "\n", + "We will instantiate a tflite interpreter and evaluate the model on the test dataset. This will help us ensure that the model has been exported correctly and is ready for deployment." + ] + }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "metadata": {}, "outputs": [ - { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
[07/18/24 16:17:17] WARNING  WARNING:absl:Please consider providing the trackable_obj argument in the  lite.py:2166\n",
-       "                             from_concrete_functions. Providing without the trackable_obj argument is              \n",
-       "                             deprecated and it will use the deprecated conversion path.                            \n",
-       "
\n" - ], - "text/plain": [ - "\u001b[2;36m[07/18/24 16:17:17]\u001b[0m\u001b[2;36m \u001b[0m\u001b[31mWARNING \u001b[0m WARNING:absl:Please consider providing the trackable_obj argument in the \u001b]8;id=561352;file:///workspaces/heartkit/.venv/lib/python3.12/site-packages/tensorflow/lite/python/lite.py\u001b\\\u001b[2mlite.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=369565;file:///workspaces/heartkit/.venv/lib/python3.12/site-packages/tensorflow/lite/python/lite.py#2166\u001b\\\u001b[2m2166\u001b[0m\u001b]8;;\u001b\\\n", - "\u001b[2;36m \u001b[0m from_concrete_functions. Providing without the trackable_obj argument is \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m deprecated and it will use the deprecated conversion path. \u001b[2m \u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
                    INFO     INFO:absl:Using new converter: If you encounter a problem please file a   lite.py:1459\n",
-       "                             bug. You can opt-out by setting experimental_new_converter=False                      \n",
-       "
\n" - ], - "text/plain": [ - "\u001b[2;36m \u001b[0m\u001b[2;36m \u001b[0m\u001b[34mINFO \u001b[0m INFO:absl:Using new converter: If you encounter a problem please file a \u001b]8;id=853103;file:///workspaces/heartkit/.venv/lib/python3.12/site-packages/tensorflow/lite/python/lite.py\u001b\\\u001b[2mlite.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=282503;file:///workspaces/heartkit/.venv/lib/python3.12/site-packages/tensorflow/lite/python/lite.py#1459\u001b\\\u001b[2m1459\u001b[0m\u001b]8;;\u001b\\\n", - "\u001b[2;36m \u001b[0m bug. You can opt-out by setting \u001b[33mexperimental_new_converter\u001b[0m=\u001b[3;91mFalse\u001b[0m \u001b[2m \u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
[07/18/24 16:17:17] INFO     Validating model results                                                  export.py:86\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[2;36m[07/18/24 16:17:17]\u001b[0m\u001b[2;36m \u001b[0m\u001b[34mINFO \u001b[0m Validating model results \u001b]8;id=592597;file:///workspaces/heartkit/heartkit/tasks/denoise/export.py\u001b\\\u001b[2mexport.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=498654;file:///workspaces/heartkit/heartkit/tasks/denoise/export.py#86\u001b\\\u001b[2m86\u001b[0m\u001b]8;;\u001b\\\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, { "name": "stderr", "output_type": "stream", "text": [ - "I0000 00:00:1721319437.392916 909880 devices.cc:67] Number of eligible GPUs (core count >= 8, compute capability >= 0.0): 1\n", - "W0000 00:00:1721319437.474874 909880 tf_tfl_flatbuffer_helpers.cc:392] Ignored output_format.\n", - "W0000 00:00:1721319437.474884 909880 tf_tfl_flatbuffer_helpers.cc:395] Ignored drop_control_dependency.\n" + "INFO: Created TensorFlow Lite XNNPACK delegate for CPU.\n" ] - }, + } + ], + "source": [ + "tflite = nse.interpreters.tflite.TfLiteKerasInterpreter(tflite_content)\n", + "tflite.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ { - "data": { - "text/html": [ - "
[07/18/24 16:17:18] INFO     [TF SET] MAE=11.36%, RMSE=18.92%                                          export.py:93\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[2;36m[07/18/24 16:17:18]\u001b[0m\u001b[2;36m \u001b[0m\u001b[34mINFO \u001b[0m \u001b[1m[\u001b[0mTF SET\u001b[1m]\u001b[0m \u001b[33mMAE\u001b[0m=\u001b[1;36m11\u001b[0m\u001b[1;36m.36\u001b[0m%, \u001b[33mRMSE\u001b[0m=\u001b[1;36m18\u001b[0m\u001b[1;36m.92\u001b[0m% \u001b]8;id=176553;file:///workspaces/heartkit/heartkit/tasks/denoise/export.py\u001b\\\u001b[2mexport.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=724123;file:///workspaces/heartkit/heartkit/tasks/denoise/export.py#93\u001b\\\u001b[2m93\u001b[0m\u001b]8;;\u001b\\\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m312/312\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 1ms/step\n" + ] + } + ], + "source": [ + "y_true = test_y\n", + "y_pred_tf = model.predict(test_x)\n", + "y_pred_tfl = tflite.predict(x=test_x)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ { "data": { "text/html": [ - "
                    INFO     [TFL SET] MAE=11.36%, RMSE=18.92%                                         export.py:97\n",
+       "
INFO     [TF METRICS] MAE=0.0814 MSE=0.0169 COS=0.8313 SNR=16.2114                                   776805021.py:3\n",
        "
\n" ], "text/plain": [ - "\u001b[2;36m \u001b[0m\u001b[2;36m \u001b[0m\u001b[34mINFO \u001b[0m \u001b[1m[\u001b[0mTFL SET\u001b[1m]\u001b[0m \u001b[33mMAE\u001b[0m=\u001b[1;36m11\u001b[0m\u001b[1;36m.36\u001b[0m%, \u001b[33mRMSE\u001b[0m=\u001b[1;36m18\u001b[0m\u001b[1;36m.92\u001b[0m% \u001b]8;id=616120;file:///workspaces/heartkit/heartkit/tasks/denoise/export.py\u001b\\\u001b[2mexport.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=443937;file:///workspaces/heartkit/heartkit/tasks/denoise/export.py#97\u001b\\\u001b[2m97\u001b[0m\u001b]8;;\u001b\\\n" + "\u001b[34mINFO \u001b[0m \u001b[1m[\u001b[0mTF METRICS\u001b[1m]\u001b[0m \u001b[33mMAE\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.0814\u001b[0m \u001b[33mMSE\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.0169\u001b[0m \u001b[33mCOS\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.8313\u001b[0m \u001b[33mSNR\u001b[0m=\u001b[1;36m16\u001b[0m\u001b[1;36m.2114\u001b[0m \u001b]8;id=370040;file:///tmp/ipykernel_1619872/776805021.py\u001b\\\u001b[2m776805021.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=166028;file:///tmp/ipykernel_1619872/776805021.py#3\u001b\\\u001b[2m3\u001b[0m\u001b]8;;\u001b\\\n" ] }, "metadata": {}, @@ -1066,11 +1325,11 @@ { "data": { "text/html": [ - "
                    INFO     Validation passed (0.00%)                                                export.py:104\n",
+       "
INFO     [TFL METRICS] MAE=0.0814 MSE=0.0169 COS=0.8313 SNR=16.2097                                  776805021.py:4\n",
        "
\n" ], "text/plain": [ - "\u001b[2;36m \u001b[0m\u001b[2;36m \u001b[0m\u001b[34mINFO \u001b[0m Validation passed \u001b[1m(\u001b[0m\u001b[1;36m0.00\u001b[0m%\u001b[1m)\u001b[0m \u001b]8;id=737251;file:///workspaces/heartkit/heartkit/tasks/denoise/export.py\u001b\\\u001b[2mexport.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=594534;file:///workspaces/heartkit/heartkit/tasks/denoise/export.py#104\u001b\\\u001b[2m104\u001b[0m\u001b]8;;\u001b\\\n" + "\u001b[34mINFO \u001b[0m \u001b[1m[\u001b[0mTFL METRICS\u001b[1m]\u001b[0m \u001b[33mMAE\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.0814\u001b[0m \u001b[33mMSE\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.0169\u001b[0m \u001b[33mCOS\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1;36m.8313\u001b[0m \u001b[33mSNR\u001b[0m=\u001b[1;36m16\u001b[0m\u001b[1;36m.2097\u001b[0m \u001b]8;id=881444;file:///tmp/ipykernel_1619872/776805021.py\u001b\\\u001b[2m776805021.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=427708;file:///tmp/ipykernel_1619872/776805021.py#4\u001b\\\u001b[2m4\u001b[0m\u001b]8;;\u001b\\\n" ] }, "metadata": {}, @@ -1078,116 +1337,57 @@ } ], "source": [ - "# TF dumps a lot of info to stdout, so we redirect it to /dev/null\n", - "with open(os.devnull, 'w') as devnull:\n", - " with contextlib.redirect_stdout(devnull), contextlib.redirect_stderr(devnull):\n", - " task.export(export_params)\n" + "tf_rst = nse.metrics.compute_metrics(metrics, y_true, y_pred_tf)\n", + "tfl_rst = nse.metrics.compute_metrics(metrics, y_true, y_pred_tfl)\n", + "logger.info(\"[TF METRICS] \" + \" \".join([f\"{k.upper()}={v:.4f}\" for k, v in tf_rst.items()]))\n", + "logger.info(\"[TFL METRICS] \" + \" \".join([f\"{k.upper()}={v:.4f}\" for k, v in tfl_rst.items()]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Run inference demo\n", + "## ECG Denoising Demo\n", "\n", - "We will run a demo on the PC to verify that the model is working as expected. The demo will load the model, run inferences across a randomly selected ECG signal, and generate a interactive report. The report will provide the original, noisy, and denoised ECG signals for comparison. " + "Finally, we will demonstrate how to use the trained ECG denoiser model to remove noise and artifacts from raw ECG signals. We will load a sample ECG signal, add noise to it, and then denoise it using the trained model. We will visualize the original, noisy, and denoised ECG signals to compare the results." ] }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 25, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m1/1\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 729ms/step\n" + ] + } + ], "source": [ - "demo_params = hk.HKDemoParams(\n", - " job_dir=job_dir, # Directory to store all output artifacts\n", - " datasets=[datasets[0]], # Datasets to demo on\n", - " sampling_rate=sampling_rate, # Target sampling rate\n", - " frame_size=frame_size, # Target frame size\n", - " preprocesses=preprocesses, # Preprocessing pipeline\n", - " augmentations=augmentations, # Augmentation pipeline\n", - " model_file=model_file, # Model file to load\n", - " # Demo params\n", - " threshold=0.5, # Threshold for classification\n", - " demo_size=500, # Number of samples to demo (8 sec)\n", - " backend=\"pc\", # Backend to use\n", - " display_report=True, # Display a report\n", - ")" + "sample_idx = np.random.randint(0, len(test_x))\n", + "ecg = test_y[sample_idx].squeeze()\n", + "aug_ecg = test_x[sample_idx].squeeze()\n", + "clean_ecg = model.predict(np.reshape(aug_ecg, (1, -1, 1)))\n", + "snr = nse.metrics.Snr()\n", + "snr.update_state(ecg.reshape(1, -1, 1), aug_ecg.reshape(1, -1, 1))\n", + "aug_snr = snr.result().numpy()\n", + "snr.reset_state()\n", + "snr.update_state(ecg.reshape(1, -1, 1), clean_ecg.reshape(1, -1, 1))\n", + "clean_snr = snr.result().numpy()" ] }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 26, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Inference: 100%|██████████| 2/2 [00:00<00:00, 5.42it/s]\n", - "Inference: 100%|██████████| 2/2 [00:00<00:00, 48.44it/s]\n", - "Inference: 100%|██████████| 2/2 [00:00<00:00, 49.64it/s]\n", - "Inference: 100%|██████████| 2/2 [00:00<00:00, 43.49it/s]\n", - "Inference: 100%|██████████| 2/2 [00:00<00:00, 45.52it/s]\n" - ] - }, - { - "data": { - "text/html": [ - " \n", - " " - ] - }, - "metadata": {}, - "output_type": "display_data" - }, { "data": { - "text/html": [ - "
" + "image/png": "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", + "text/plain": [ + "
" ] }, "metadata": {}, @@ -1195,15 +1395,29 @@ } ], "source": [ - "pio.renderers.default = \"notebook\"\n", - "task.demo(params=demo_params)" + "fig, ax = plt.subplots(3, 1, figsize=(9, 5), sharex=True)\n", + "ax[0].plot(ts, ecg.squeeze(), color=plot_theme.primary_color, lw=3)\n", + "ax[1].plot(ts, aug_ecg.squeeze(), color=plot_theme.secondary_color, lw=3)\n", + "ax[2].plot(ts, clean_ecg.squeeze(), color=plot_theme.tertiary_color, lw=3)\n", + "\n", + "ax[0].set_ylabel(\"Reference\")\n", + "ax[1].set_ylabel(\"Noisy\")\n", + "ax[2].set_ylabel(\"Denoised\")\n", + "\n", + "ax[1].text(0.98, 0.15, f\"{aug_snr:4.02f} dB SNR\", transform=ax[1].transAxes, ha=\"right\", va=\"top\", weight='bold')\n", + "ax[2].text(0.98, 0.15, f\"{clean_snr:4.02f} dB SNR\", transform=ax[2].transAxes, ha=\"right\", va=\"top\", weight='bold')\n", + "# Disable y-axis ticks for all plots\n", + "for axes in ax:\n", + " axes.yaxis.set_ticks([])\n", + "ax[-1].set_xlabel(\"Time (s)\")\n", + "fig.suptitle(\"ECG Denoising Demo\")\n", + "fig.tight_layout()\n", + "fig.show()" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [] } ], diff --git a/docs/guides/train-ecg-segmentation.ipynb b/docs/guides/train-ecg-segmentation.ipynb index 78f4695e..b69525fc 100644 --- a/docs/guides/train-ecg-segmentation.ipynb +++ b/docs/guides/train-ecg-segmentation.ipynb @@ -151,8 +151,8 @@ "source": [ "datasets = [\n", " dict(\n", - " name=\"synthetic\",\n", - " path=datasets_dir / \"synthetic\",\n", + " name=\"ecg-synthetic\",\n", + " path=datasets_dir / \"ecg-synthetic\",\n", " params=dict(\n", " num_pts=num_synthetic_patients,\n", " params=dict(\n", diff --git a/docs/index.md b/docs/index.md index 835b0aa4..2f0b44f4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,7 +12,7 @@ --- -Introducing HeartKit, an AI Development Kit (ADK) that enables developers to easily train and deploy real-time __heart-monitoring__ models onto [Ambiq's family of ultra-low power SoCs](https://ambiq.com/soc/). The kit provides a variety of datasets, efficient model architectures, and heart-related tasks. In addition, HeartKit provides optimization and deployment routines to generate efficient inference models. Finally, the kit includes a number of pre-trained models and task-level demos to showcase the capabilities. +Introducing HeartKit, an AI Development Kit (ADK) that enables developers to easily train and deploy real-time __heart-monitoring__ models onto [Ambiq's family of ultra-low power SoCs](https://ambiq.com/soc/). The kit provides a variety of datasets, efficient model architectures, and heart-related tasks out of the box. In addition, HeartKit provides optimization and deployment routines to generate efficient inference models. Finally, the kit includes a number of pre-trained models and task-level demos to showcase the capabilities. **Key Features:** @@ -30,29 +30,34 @@ Please explore the HeartKit Docs, a comprehensive resource designed to help you - **Tasks** `HeartKit` provides tasks like rhythm, segment, and denoising   [:material-magnify-expand: Explore Tasks](tasks/index.md){ .md-button } - **Datasets** Several built-in datasets can be leveraged   [:material-database-outline: Explore Datasets](./datasets/index.md){ .md-button } - **Model Zoo** Pre-trained models are available for each task   [:material-download: Explore Models](./zoo/index.md){ .md-button } +- **Guides** Detailed guides on tasks, models, and datasets   [:material-book-open-page-variant: Explore Guides](./guides/index.md){ .md-button } ## Installation -To get started, first install the local python package `heartkit` along with its dependencies via `pip` or `Poetry`: - -=== "Poetry install" +To get started, first install the python package `heartkit` along with its dependencies via `Git` or `PyPi`: +=== "PyPI install" +
```console - $ poetry install . + $ pip install heartkit ---> 100% ```
-=== "Pip install" - +=== "Git clone" +
```console - $ pip install heartkit + $ git clone https://github.com/AmbiqAI/heartkit.git + Cloning into 'heartkit'... + Resolving deltas: 100% (3491/3491), done. + $ cd heartkit + $ poetry install ---> 100% ``` @@ -63,45 +68,38 @@ To get started, first install the local python package `heartkit` along with its ## Usage -__HeartKit__ can be used as either a CLI-based tool or as a Python package to perform advanced development. In both forms, HeartKit exposes a number of modes and tasks outlined below. In addition, by leveraging highly-customizable configurations, HeartKit can be used to create custom workflows for a given application with minimal coding. Refer to the [Quickstart](./quickstart.md) to quickly get up and running in minutes. +__HeartKit__ can be used as either a CLI-based tool or as a Python package to perform advanced development. In both forms, HeartKit exposes a number of modes and tasks outlined below. In addition, by leveraging highly-customizable configurations and extendable factories, HeartKit can be used to create custom workflows for a given application with minimal coding. Refer to the [Quickstart](./quickstart.md) to quickly get up and running in minutes. --- -## Modes +## [Tasks](./tasks/index.md) -__HeartKit__ provides a number of [modes](./modes/index.md) that can be invoked for a given task. These modes can be accessed via the CLI or directly from the `task` within the Python package. +__HeartKit__ includes a number of built-in [tasks](./tasks/index.md). Each task provides reference routines for training, evaluating, and exporting the model. The routines can be customized by providing highly flexibile configuration files/objects. Additionally, new tasks can be added to the __HeartKit__ framework by defining a new [Task class](./tasks/byot.md) and registering it to the [__Task Factory__](./tasks/byot.md). -- **[Download](./modes/download.md)**: Download specified datasets -- **[Train](./modes/train.md)**: Train a model for specified task and datasets -- **[Evaluate](./modes/evaluate.md)**: Evaluate a model for specified task and datasets -- **[Export](./modes/export.md)**: Export a trained model to TensorFlow Lite and TFLM -- **[Demo](./modes/demo.md)**: Run task-level demo on PC or remotely on Ambiq EVB - ---- - -## Task Factory - -__HeartKit__ includes a number of built-in [tasks](./tasks/index.md). Each task provides reference routines for training, evaluating, and exporting the model. The routines can be customized by providing a configuration file or by setting the parameters directly in the code. Additional tasks can be easily added to the __HeartKit__ framework by creating a new task class and registering it to the __task factory__. - -- **[Denoise](./tasks/denoise.md)**: Denoise ECG signal -- **[Segmentation](./tasks/segmentation.md)**: Perform ECG based segmentation (P-Wave, QRS, T-Wave) +- **[Denoise](./tasks/denoise.md)**: Remove noise and artifacts from ECG signals +- **[Segmentation](./tasks/segmentation.md)**: Perform ECG/PPG based segmentation - **[Rhythm](./tasks/rhythm.md)**: Heart rhythm classification (AFIB, AFL) - **[Beat](./tasks/beat.md)**: Beat-level classification (NORM, PAC, PVC, NOISE) -- **[BYOT](./tasks/byot.md)**: Bring-Your-Own-Task (BYOT) to create custom tasks +- **[Bring-Your-Own-Task (BYOT)](./tasks/byot.md)**: Create and register custom tasks --- -## Model Factory +## [Modes](./modes/index.md) -__HeartKit__ provides a __model factory__ that allows you to easily create and train customized models. The model factory includes a number of modern networks well suited for efficient, real-time edge applications. Each model architecture exposes a number of high-level parameters that can be used to customize the network for a given application. These parameters can be set as part of the configuration accessible via the CLI and Python package. Check out the [Model Factory Guide](./models/index.md) to learn more about the available network architectures. +__HeartKit__ provides a number of [modes](./modes/index.md) that can be invoked for a given task. These modes can be accessed via the CLI or directly from a [Task](./tasks/index.md) within code. Each mode is accompanied by a set of [task parameters](./modes/configuration.md#hktaskparams) that can be customized to fit the user's needs. ---- +- **[Download](./modes/download.md)**: Download specified datasets +- **[Train](./modes/train.md)**: Train a model for specified task and datasets +- **[Evaluate](./modes/evaluate.md)**: Evaluate a model for specified task and datasets +- **[Export](./modes/export.md)**: Export a trained model to TensorFlow Lite and TFLM +- **[Demo](./modes/demo.md)**: Run task-level demo on PC or remotely on Ambiq EVB -## Dataset Factory +--- -__HeartKit__ exposes several open-source datasets for training each of the HeartKit tasks via the __dataset factory__. For certain tasks, we also provide synthetic data provided by [PhysioKit](https://ambiqai.github.io/physiokit) to help improve model generalization. Each dataset has a corresponding Python class to aid in downloading and generating data for the given task. Additional datasets can be added to the HeartKit framework by creating a new dataset class and registering it to the dataset factory. Check out the [Dataset Factory Guide](./datasets/index.md) to learn more about the available datasets along with their corresponding licenses and limitations. +## [Datasets](./datasets/index.md) +The ADK includes several built-in [datasets](./datasets/index.md) for training __heart-monitoring__ related tasks. We also provide synthetic dataset generators for signals such as ECG, PPG, and RSP along with segmentation and fiducials. Each included dataset inherits from [HKDataset](./datasets/dataset.md) that provides consistent interface for downloading and accessing the data. Additional datasets can be added to the HeartKit framework by creating a new dataset class and registering it to the dataset factory, DatasetFactory. Check out the [Datasets Guide](./datasets/index.md) to learn more about the available datasets along with their corresponding licenses and limitations. * **[Icentia11k](./datasets/icentia11k.md)**: 11-lead ECG data collected from 11,000 subjects captured continously over two weeks. * **[LUDB](./datasets/ludb.md)**: 200 ten-second 12-lead ECG records w/ annotated P-wave, QRS, and T-wave boundaries. @@ -109,12 +107,35 @@ __HeartKit__ exposes several open-source datasets for training each of the Heart * **[LSAD](./datasets/lsad.md)**: 10-second, 12-lead ECG dataset collected from 45,152 subjects w/ over 100 scp codes. * **[PTB-XL](./datasets/ptbxl.md)**: 10-second, 12-lead ECG dataset collected from 18,885 subjects w/ 72 different diagnostic classes. * **[Synthetic](./datasets/synthetic.md)**: A synthetic dataset generator provided by [PhysioKit](https://ambiqai.github.io/physiokit). -* **[BYOD](./datasets/byod.md)**: Bring-Your-Own-Dataset (BYOD) to add additional datasets. +* **[Bring-Your-Own-Dataset (BYOD)](./datasets/byod.md)**: Add and register new datasets to the framework. + +--- + +## [Models](./models/index.md) + +__HeartKit__ provides a variety of model architectures geared towards efficient, real-time edge applications. These models are provided by Ambiq's [neuralspot-edge](https://ambiqai.github.io/neuralspot-edge/) and expose a set of parameters that can be used to fully customize the network for a given application. In addition, HeartKit includes a model factory, [ModelFactory](./models/index.md#model-factory) to register current models as well as allow new custom architectures to be added. Check out the [Models Guide](./models/index.md) to learn more about the available network architectures and model factory. + +- **[TCN](https://ambiqai.github.io/neuralspot-edge/models/tcn)**: A CNN leveraging dilated convolutions (key=`tcn`) +- **[U-Net](https://ambiqai.github.io/neuralspot-edge/models/unet)**: A CNN with encoder-decoder architecture for segmentation tasks (key=`unet`) +- **[U-NeXt](https://ambiqai.github.io/neuralspot-edge/models/unext)**: A U-Net variant leveraging MBConv blocks (key=`unext`) +- **[EfficientNetV2](https://ambiqai.github.io/neuralspot-edge/models/efficientnet)**: A CNN leveraging MBConv blocks (key=`efficientnet`) +- **[MobileOne](https://ambiqai.github.io/neuralspot-edge/models/mobileone)**: A CNN aimed at sub-1ms inference (key=`mobileone`) +- **[ResNet](https://ambiqai.github.io/neuralspot-edge/models/resnet)**: A popular CNN often used for vision tasks (key=`resnet`) +- **[Conformer](https://ambiqai.github.io/neuralspot-edge/models/conformer)**: A transformer composed of both convolutional and self-attention blocks (key=`conformer`) +- **[MetaFormer](https://ambiqai.github.io/neuralspot-edge/models/metaformer)**: A transformer composed of both spatial mixing and channel mixing blocks (key=`metaformer`) +- **[TSMixer](https://ambiqai.github.io/neuralspot-edge/models/tsmixer)**: An All-MLP Architecture for Time Series Classification (key=`tsmixer`) +- **[Bring-Your-Own-Model (BYOM)](https://ambiqai.github.io/neuralspot-edge/models/byom)**: Register new SoTA model architectures w/ custom configurations + +--- + +## [Model Zoo](./zoo/index.md) + +The ADK includes a number of pre-trained models and configurationn recipes for the built-in tasks. These models are trained on a variety of datasets and are optimized for deployment on Ambiq's ultra-low power SoCs. In addition to providing links to download the models, __HeartKit__ provides the corresponding configuration files and performance metrics. The configuration files allow you to easily recreate the models or use them as a starting point for custom solutions. Furthermore, the performance metrics provide insights into the trade-offs between model complexity and performance. Check out the [Model Zoo](./zoo/index.md) to learn more about the available models and their corresponding performance metrics. --- -## Model Zoo +## [Guides](./guides/index.md) -A number of pre-trained models are available for each task. These models are trained on a variety of datasets and are optimized for deployment on Ambiq's ultra-low power SoCs. In addition to providing links to download the models, __HeartKit__ provides the corresponding configuration files and performance metrics. The configuration files allow you to easily recreate the models or use them as a starting point for custom solutions. Furthermore, the performance metrics provide insights into the model's accuracy, precision, recall, and F1 score. For a number of the models, we provide experimental and ablation studies to showcase the impact of various design choices. Check out the [Model Zoo](./zoo/index.md) to learn more about the available models and their corresponding performance metrics. Also explore the [Experiments](./experiments/index.md) to learn more about the ablation studies and experimental results. +Checkout the [Guides](./guides/index.md) to see detailed examples and tutorials on how to use HeartKit for a variety of tasks. The guides provide step-by-step instructions on how to train, evaluate, and deploy models for a given task. In addition, the guides provide insights into the design choices and performance metrics for the models. The guides are designed to help you get up and running quickly and to provide a deeper understanding of the models and tasks available in HeartKit. --- diff --git a/docs/models/byom.md b/docs/models/byom.md new file mode 100644 index 00000000..98f54026 --- /dev/null +++ b/docs/models/byom.md @@ -0,0 +1,88 @@ +# Bring-Your-Own-Model (BYOM) + +The model factory can be extended to include custom models. This is useful when you have a custom model architecture that you would like to use for training. The custom model can be registered with the model factory by defining a custom model function and registering it with the `ModelFactory`. + +## How it Works + +1. **Create a Model**: Define a new model function that takes a `keras.Input`, model parameters, and number of classes as arguments and returns a `keras.Model`. + + ```python + + import keras + import heartkit as hk + + def custom_model_from_object( + x: keras.KerasTensor, + params: dict, + num_classes: int | None = None, + ) -> keras.Model: + + y = x + # Create fully connected network from params + for layer in params["layers"]: + y = keras.layers.Dense(layer["units"], activation=layer["activation"])(y) + + if num_classes: + y = keras.layers.Dense(num_classes, activation="softmax")(y) + + return keras.Model(inputs=x, outputs=y) + ``` + +2. **Register the Model**: Register the new model function with the `ModelFactory` by calling the `register` method. This method takes the model name and the callable as arguments. + + ```python + hk.ModelFactory.register("custom-model", custom_model_from_object) + ``` + +3. **Use the Model**: The new model can now be used with the `ModelFactory` to perform various operations such as downloading and generating data. + + ```python + inputs = keras.Input(shape=(100,)) + model = hk.ModelFactory.get("custom-model")( + x=inputs, + params={ + "layers": [ + {"units": 64, "activation": "relu"}, + {"units": 32, "activation": "relu"}, + ] + }, + num_classes=5, + ) + + model.summary() + + ``` + +## Better Model Params + +Rather than using a dictionary to define the model parameters, you can define a custom dataclass or [Pydantic](https://pydantic-docs.helpmanual.io/) model to enforce type checking and provide better documentation. + +```python +from pydantic import BaseModel + +class CustomLayerParams(BaseModel): + units: int + activation: str + +class CustomModelParams(BaseModel): + layers: list[CustomLayerParams] + +def custom_model_from_object( + x: keras.KerasTensor, + params: dict, + num_classes: int | None = None, +) -> keras.Model: + + # Convert and validate params + params = CustomModelParams(**params) + + y = x + # Create fully connected network from params + for layer in params.layers: + y = keras.layers.Dense(layer.units, activation=layer.activation)(y) + + if num_classes: + y = keras.layers.Dense(num_classes, activation="softmax")(y) + + return keras.Model(inputs=x, outputs=y) +``` diff --git a/docs/models/index.md b/docs/models/index.md index 2b56b52c..33ff78cc 100644 --- a/docs/models/index.md +++ b/docs/models/index.md @@ -1,67 +1,114 @@ -# :factory: Model Factory +# :material-graph-outline: Models -HeartKit provides a model factory that allows you to easily create and train customized models via [KerasEdge](). KerasEdge includes a growing number of state-of-the-art models that can be easily configured and trained using high-level parameters. The models are designed to be efficient and well-suited for real-time edge applications. Most of the models are based on state-of-the-art architectures that have been modified to allow for more fine-grain customization. The also support 1D variants to allow for training on time-series data. The included models are well suited for efficient, real-time edge applications. +HeartKit provides a number of model architectures that can be used for training __heart-monitoring tasks__. While a number of off-the-shelf models exist, they are often not efficient nor optimized for real-time, edge applications. To address this, HeartKit provides a model factory that allows you to easily create and train customized models via [neuralspot-edge](https://ambiqai.github.io/neuralspot-edge/). `neuralspot-edge` includes a growing number of state-of-the-art models that can be easily configured and trained using high-level parameters. The models are designed to be efficient and well-suited for real-time, edge applications. Most of the models are based on state-of-the-art architectures that have been modified to allow for more fine-grain customization. In addition, the models support 1D variants to allow for training on time-series data. Please check [neuralspot-edge](https://ambiqai.github.io/neuralspot-edge/) for list of available models and their configurations. -Please check [KerasEdge]() for list of available models and their configurations. +--- + +## Available Models + +- **[TCN](https://ambiqai.github.io/neuralspot-edge/models/tcn)**: A CNN leveraging dilated convolutions (key=`tcn`) +- **[U-Net](https://ambiqai.github.io/neuralspot-edge/models/unet)**: A CNN with encoder-decoder architecture for segmentation tasks (key=`unet`) +- **[U-NeXt](https://ambiqai.github.io/neuralspot-edge/models/unext)**: A U-Net variant leveraging MBConv blocks (key=`unext`) +- **[EfficientNetV2](https://ambiqai.github.io/neuralspot-edge/models/efficientnet)**: A CNN leveraging MBConv blocks (key=`efficientnet`) +- **[MobileOne](https://ambiqai.github.io/neuralspot-edge/models/mobileone)**: A CNN aimed at sub-1ms inference (key=`mobileone`) +- **[ResNet](https://ambiqai.github.io/neuralspot-edge/models/resnet)**: A popular CNN often used for vision tasks (key=`resnet`) +- **[Conformer](https://ambiqai.github.io/neuralspot-edge/models/conformer)**: A transformer composed of both convolutional and self-attention blocks (key=`conformer`) +- **[MetaFormer](https://ambiqai.github.io/neuralspot-edge/models/metaformer)**: A transformer composed of both spatial mixing and channel mixing blocks (key=`metaformer`) +- **[TSMixer](https://ambiqai.github.io/neuralspot-edge/models/tsmixer)**: An All-MLP Architecture for Time Series Classification (key=`tsmixer`) +* **[Bring-Your-Own-Model](./byom.md)**: Add a custom model architecture to HeartKit. + +--- + +## Model Factory + +HeartKit includes a model factory, `ModelFactory`, that eases the processes of creating models for training. The factory allows you to create models by specifying the model key and the model parameters. The factory will then create the model using the specified parameters. The factory also allows you to register custom models that can be used for training. By leveraring a factory, a task only needs to provide the architecture key and the parameters, and the factory will take care of the rest. + +The model factory provides the following methods: + +* **hk.ModelFactory.register**: Register a custom model +* **hk.ModelFactory.unregister**: Unregister a custom model +* **hk.ModelFactory.has**: Check if a model is registered +* **hk.ModelFactory.get**: Get a model from the factory +* **hk.ModelFactory.list**: List all available models --- ## Usage -The model factory can be invoked either via CLI or within the `heartkit` python package. At a high level, the model factory performs the following actions based on the provided configuration parameters: - -!!! Example - - === "JSON" - - ```json - { - "name": "tcn", - "params": { - "input_kernel": [1, 3], - "input_norm": "batch", - "blocks": [ - {"depth": 1, "branch": 1, "filters": 12, "kernel": [1, 3], "dilation": [1, 1], "dropout": 0.10, "ex_ratio": 1, "se_ratio": 0, "norm": "batch"}, - {"depth": 1, "branch": 1, "filters": 20, "kernel": [1, 3], "dilation": [1, 1], "dropout": 0.10, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, - {"depth": 1, "branch": 1, "filters": 28, "kernel": [1, 3], "dilation": [1, 2], "dropout": 0.10, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, - {"depth": 1, "branch": 1, "filters": 36, "kernel": [1, 3], "dilation": [1, 4], "dropout": 0.10, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, - {"depth": 1, "branch": 1, "filters": 40, "kernel": [1, 3], "dilation": [1, 8], "dropout": 0.10, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"} - ], - "output_kernel": [1, 3], - "include_top": true, - "use_logits": true, - "model_name": "tcn" - } +### Defining a model in configuration file + +A model can be created when invoking a command via the CLI by setting [architecture](../modes/configuration.md#hktaskparams) in the configuration file. The task will use the supplied name to get the registered model and instantiate with the provided parameters. + +Given the following configuration file `configuration.json`: + +```json +{ + ... + "architecture:" { + "name": "tcn", + "params": { + "input_kernel": [1, 3], + "input_norm": "batch", + "blocks": [ + {"depth": 1, "branch": 1, "filters": 12, "kernel": [1, 3], "dilation": [1, 1], "dropout": 0.10, "ex_ratio": 1, "se_ratio": 0, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 20, "kernel": [1, 3], "dilation": [1, 1], "dropout": 0.10, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 28, "kernel": [1, 3], "dilation": [1, 2], "dropout": 0.10, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 36, "kernel": [1, 3], "dilation": [1, 4], "dropout": 0.10, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 40, "kernel": [1, 3], "dilation": [1, 8], "dropout": 0.10, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"} + ], + "output_kernel": [1, 3], + "include_top": true, + "use_logits": true, + "model_name": "tcn" } - ``` - - === "Python" - - ```python - import keras - from heartkit.models import Tcn, TcnParams, TcnBlockParams - - inputs = keras.Input(shape=(800, 1)) - num_classes = 5 - - model = Tcn( - x=inputs, - params=TcnParams( - input_kernel=(1, 3), - input_norm="batch", - blocks=[ - TcnBlockParams(filters=8, kernel=(1, 3), dilation=(1, 1), dropout=0.1, ex_ratio=1, se_ratio=0, norm="batch"), - TcnBlockParams(filters=16, kernel=(1, 3), dilation=(1, 2), dropout=0.1, ex_ratio=1, se_ratio=0, norm="batch"), - TcnBlockParams(filters=24, kernel=(1, 3), dilation=(1, 4), dropout=0.1, ex_ratio=1, se_ratio=4, norm="batch"), - TcnBlockParams(filters=32, kernel=(1, 3), dilation=(1, 8), dropout=0.1, ex_ratio=1, se_ratio=4, norm="batch"), - ], - output_kernel=(1, 3), - include_top=True, - use_logits=True, - model_name="tcn", - ), - num_classes=num_classes, - ) - ``` + } +} +``` + +### Defining a model in code + +The model can be created using the following command: + +```bash +heartkit --mode train --task rhythm --config config.json +``` + +Alternatively, the model can be created directly in code using the following snippet: + +```python + +import keras +import heartkit as hk + +architecture = { + "name": "tcn", + "params": { + "input_kernel": [1, 3], + "input_norm": "batch", + "blocks": [ + {"depth": 1, "branch": 1, "filters": 12, "kernel": [1, 3], "dilation": [1, 1], "dropout": 0.10, "ex_ratio": 1, "se_ratio": 0, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 20, "kernel": [1, 3], "dilation": [1, 1], "dropout": 0.10, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 28, "kernel": [1, 3], "dilation": [1, 2], "dropout": 0.10, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 36, "kernel": [1, 3], "dilation": [1, 4], "dropout": 0.10, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"}, + {"depth": 1, "branch": 1, "filters": 40, "kernel": [1, 3], "dilation": [1, 8], "dropout": 0.10, "ex_ratio": 1, "se_ratio": 2, "norm": "batch"} + ], + "output_kernel": [1, 3], + "include_top": True, + "use_logits": True, + "model_name": "tcn" + } +} + +inputs = keras.Input(shape=(256,1), dtype="float32") +num_classes = 5 + +model = hk.ModelFactory.get(architecture["name"])( + x=inputs, + params=architecture["params"], + num_classes=num_classes, +) + +model.summary() +``` --- diff --git a/docs/modes/configuration.md b/docs/modes/configuration.md index 39a0fa7d..52855473 100644 --- a/docs/modes/configuration.md +++ b/docs/modes/configuration.md @@ -2,176 +2,94 @@ For each mode, a set of parameters are required to run the task. The following sections provide details on the parameters required for each mode. -### QuantizationParams +## QuantizationParams + +Quantization parameters define the quantization-aware training (QAT) and post-training quantization (PTQ) settings. This is used for modes: train, evaluate, export, and demo. | Argument | Type | Opt/Req | Default | Description | | --- | --- | --- | --- | --- | | enabled | bool | Optional | False | Enable quantization | | qat | bool | Optional | False | Enable quantization aware training (QAT) | -| format | QuantizationType | Optional | INT8 | Quantization mode | +| format | Literal["int8", "int16", "float16"] | Optional | int8 | Quantization mode | | io_type | str | Optional | int8 | I/O type | -| conversion | ConversionType | Optional | KERAS | Conversion method | +| conversion | Literal["keras", "tflite"] | Optional | keras | Conversion method | | debug | bool | Optional | False | Debug quantization | | fallback | bool | Optional | False | Fallback to float32 | +## NamedParams -### ModelArchitecture - -| Argument | Type | Opt/Req | Default | Description | -| --- | --- | --- | --- | --- | -| name | str | Required | | Model architecture name | -| params | dict[str, Any] | Optional | {} | Model architecture parameters | - -### PreprocessParams +Named parameters are used to provide custom parameters for a given object or callable. For example, a dataset, 'my-dataset', may require custom parameters such as 'path', 'label', 'sampling_rate', etc. When a task loads the dataset using `name`, the task will then unpack the custom parameters and pass them to the dataset loader. | Argument | Type | Opt/Req | Default | Description | | --- | --- | --- | --- | --- | -| name | str | Required | | Preprocess name | -| params | dict[str, Any] | Optional | {} | Preprocess parameters | +| name | str | Required | | Named parameters name | +| params | dict[str, Any] | Optional | {} | Named parameters | -### AugmentationParams - -| Argument | Type | Opt/Req | Default | Description | -| --- | --- | --- | --- | --- | -| name | str | Required | | Augmentation name | -| params | dict[str, Any] | Optional | {} | Augmentation parameters | +## HKDownloadParams -### DatasetParams - -| Argument | Type | Opt/Req | Default | Description | -| --- | --- | --- | --- | --- | -| name | str | Required | | Dataset name | -| path | Path | Optional | Path() | Dataset path | -| params | dict[str, Any] | Optional | {} | Parameters | -| weight | float | Optional | 1 | Dataset weight | - - -### HKDownloadParams +These parameters are used by `download` mode to download all supplied datasets. | Argument | Type | Opt/Req | Default | Description | | --- | --- | --- | --- | --- | | job_dir | Path | Optional | `tempfile.gettempdir` | Job output directory | -| datasets | list[DatasetParams] | Optional | | Datasets | +| datasets | list[NamedParams] | Optional | | Datasets | | progress | bool | Optional | True | Display progress bar | | force | bool | Optional | False | Force download dataset- overriding existing files | | data_parallelism | int | Optional | `os.cpu_count` | # of data loaders running in parallel | -### HKTrainParams + +## HKTaskParams + +These parameters are supplied to a [Task](../tasks/index.md) when running a given mode such as `train`, `evaluate`, `export`, or `demo`. A single configuration object is used to simplify configuration files and heavy re-use of parameters between modes. + | Argument | Type | Opt/Req | Default | Description | | --- | --- | --- | --- | --- | | name | str | Required | experiment | Experiment name | | project | str | Required | heartkit | Project name | | job_dir | Path | Optional | `tempfile.gettempdir` | Job output directory | -| datasets | list[DatasetParams] | Optional | | Datasets | +| datasets | list[NamedParams] | Optional | | Datasets | +| dataset_weights | list[float]\|None | Optional | None | Dataset weights | | sampling_rate | int | Optional | 250 | Target sampling rate (Hz) | -| frame_size | int | Optional | 1250 | Frame size | -| num_classes | int | Optional | 1 | # of classes | -| class_map | dict[int, int] | Optional | | Class/label mapping | -| class_names | list[str] | Optional | None | Class names | +| frame_size | int | Optional | 1250 | Frame size in samples | | samples_per_patient | int\|list[int] | Optional | 1000 | # train samples per patient | | val_samples_per_patient | int\|list[int] | Optional | 1000 | # validation samples per patient | +| test_samples_per_patient | int\|list[int] | Optional | 1000 | # test samples per patient | | train_patients | float\|None | Optional | None | # or proportion of patients for training | | val_patients | float\|None | Optional | None | # or proportion of patients for validation | +| test_patients | float\|None | Optional | None | # or proportion of patients for testing | | val_file | Path\|None | Optional | None | Path to load/store pickled validation file | +| test_file | Path\|None | Optional | None | Path to load/store pickled test file | | val_size | int\|None | Optional | None | # samples for validation | +| test_size | int | Optional | 10000 | # samples for testing | +| num_classes | int | Optional | 1 | # of classes | +| class_map | dict[int, int] | Optional | | Class/label mapping | +| class_names | list[str]\|None | Optional | None | Class names | | resume | bool | Optional | False | Resume training | -| architecture | ModelArchitecture | Optional | | Custom model architecture | -| model_file | Path\|None | Optional | None | Path to save model file (.keras) | -| threshold | float\|None | Optional | None | Model output threshold | -| weights_file | Path\|None | Optional | None | Path to a checkpoint weights to load | +| architecture | NamedParams\|None | Optional | None | Custom model architecture | +| model_file | Path\|None | Optional | None | Path to load/save model file (.keras) | +| use_logits | bool | Optional | True | Use logits output or softmax | +| weights_file | Path\|None | Optional | None | Path to a checkpoint weights to load/save | | quantization | QuantizationParams | Optional | | Quantization parameters | | lr_rate | float | Optional | 0.001 | Learning rate | | lr_cycles | int | Optional | 3 | Number of learning rate cycles | | lr_decay | float | Optional | 0.9 | Learning rate decay | -| class_weights | Literal["balanced", "fixed"] | Optional | fixed | Class weights | | label_smoothing | float | Optional | 0 | Label smoothing | | batch_size | int | Optional | 32 | Batch size | -| buffer_size | int | Optional | 100 | Buffer size | +| buffer_size | int | Optional | 100 | Buffer cache size | | epochs | int | Optional | 50 | Number of epochs | | steps_per_epoch | int | Optional | 10 | Number of steps per epoch | +| val_steps_per_epoch | int | Optional | 10 | Number of validation steps | | val_metric | Literal["loss", "acc", "f1"] | Optional | loss | Performance metric | -| preprocesses | list[PreprocessParams] | Optional | | Preprocesses | -| augmentations | list[AugmentationParams] | Optional | | Augmentations | -| seed | int\|None | Optional | None | Random state seed | -| data_parallelism | int | Optional | `os.cpu_count` | # of data loaders running in parallel | -| verbose | int | Optional | 1 | Verbosity level | - -### HKTestParams - -| Argument | Type | Opt/Req | Default | Description | -| --- | --- | --- | --- | --- | -| name | str | Required | experiment | Experiment name | -| project | str | Required | heartkit | Project name | -| job_dir | Path | Optional | `tempfile.gettempdir` | Job output directory | -| datasets | list[DatasetParams] | Optional | | Datasets | -| sampling_rate | int | Optional | 250 | Target sampling rate (Hz) | -| frame_size | int | Optional | 1250 | Frame size | -| num_classes | int | Optional | 1 | # of classes | -| class_map | dict[int, int] | Optional | | Class/label mapping | -| class_names | list[str] | Optional | None | Class names | -| test_samples_per_patient | int\|list[int] | Optional | 1000 | # test samples per patient | -| test_patients | float\|None | Optional | None | # or proportion of patients for testing | -| test_size | int | Optional | 200000 | # samples for testing | -| test_file | Path\|None | Optional | None | Path to load/store pickled test file | -| preprocesses | list[PreprocessParams] | Optional | | Preprocesses | -| augmentations | list[AugmentationParams] | Optional | | Augmentations | -| model_file | Path\|None | Optional | None | Path to save model file (.keras) | -| threshold | float\|None | Optional | None | Model output threshold | -| seed | int\|None | Optional | None | Random state seed | -| data_parallelism | int | Optional | `os.cpu_count` | # of data loaders running in parallel | -| verbose | int | Optional | 1 | Verbosity level | - -### HKExportParams - -| Argument | Type | Opt/Req | Default | Description | -| --- | --- | --- | --- | --- | -| name | str | Required | experiment | Experiment name | -| project | str | Required | heartkit | Project name | -| job_dir | Path | Optional | `tempfile.gettempdir` | Job output directory | -| datasets | list[DatasetParams] | Optional | | Datasets | -| sampling_rate | int | Optional | 250 | Target sampling rate (Hz) | -| frame_size | int | Optional | 1250 | Frame size | -| num_classes | int | Optional | 3 | # of classes | -| class_map | dict[int, int] | Optional | | Class/label mapping | -| class_names | list[str] | Optional | None | Class names | -| test_samples_per_patient | int\|list[int] | Optional | 100 | # test samples per patient | -| test_patients | float\|None | Optional | None | # or proportion of patients for testing | -| test_size | int | Optional | 100000 | # samples for testing | -| test_file | Path\|None | Optional | None | Path to load/store pickled test file | -| preprocesses | list[PreprocessParams] | Optional | | Preprocesses | -| augmentations | list[AugmentationParams] | Optional | | Augmentations | -| model_file | Path\|None | Optional | None | Path to save model file (.keras) | +| class_weights | Literal["balanced", "fixed"] | Optional | fixed | Class weights | | threshold | float\|None | Optional | None | Model output threshold | -| val_acc_threshold | float\|None | Optional | 0.98 | Validation accuracy threshold | -| use_logits | bool | Optional | True | Use logits output or softmax | -| quantization | QuantizationParams | Optional | | Quantization parameters | +| val_metric_threshold | float\|None | Optional | 0.98 | Validation metric threshold | | tflm_var_name | str | Optional | g_model | TFLite Micro C variable name | | tflm_file | Path\|None | Optional | None | Path to copy TFLM header file (e.g. ./model_buffer.h) | -| data_parallelism | int | Optional | `os.cpu_count` | # of data loaders running in parallel | -| model_config | ConfigDict | Optional | | Model configuration | -| verbose | int | Optional | 1 | Verbosity level | - -### HKDemoParams - -| Argument | Type | Opt/Req | Default | Description | -| --- | --- | --- | --- | --- | -| name | str | Required | experiment | Experiment name | -| project | str | Required | heartkit | Project name | -| job_dir | Path | Optional | `tempfile.gettempdir` | Job output directory | -| datasets | list[DatasetParams] | Optional | | Datasets | -| sampling_rate | int | Optional | 250 | Target sampling rate (Hz) | -| frame_size | int | Optional | 1250 | Frame size | -| num_classes | int | Optional | 1 | # of classes | -| class_map | dict[int, int] | Optional | | Class/label mapping | -| class_names | list[str] | Optional | None | Class names | -| preprocesses | list[PreprocessParams] | Optional | | Preprocesses | -| augmentations | list[AugmentationParams] | Optional | | Augmentations | -| model_file | Path\|None | Optional | None | Path to save model file (.keras) | | backend | str | Optional | pc | Backend | -| demo_size | int | Optional | 1000 | # samples for demo | +| demo_size | int\|None | Optional | 1000 | # samples for demo | | display_report | bool | Optional | True | Display report | | seed | int\|None | Optional | None | Random state seed | -| model_config | ConfigDict | Optional | | Model configuration | +| data_parallelism | int | Optional | `os.cpu_count` | # of data loaders running in parallel | | verbose | int | Optional | 1 | Verbosity level | diff --git a/docs/modes/demo.md b/docs/modes/demo.md index 2f0f0a65..d0ad2f96 100644 --- a/docs/modes/demo.md +++ b/docs/modes/demo.md @@ -4,7 +4,7 @@ Each task in HeartKit has a corresponding demo mode that allows you to run a task-level demonstration using the specified backend inference engine (e.g. PC or EVB). This is useful to showcase the model's performance in real-time and to verify its accuracy in a real-world scenario. Similar to other modes, the demo can be invoked either via CLI or within `heartkit` python package. At a high level, the demo mode performs the following actions based on the provided configuration parameters: -1. Load the configuration file (e.g. `segmentation-class-2`) +1. Load the configuration file (e.g. `configuration.json`) 1. Load the desired dataset features (e.g. `icentia11k`) 1. Load the trained model (e.g. `model.keras`) 1. Load random test subject's data @@ -15,44 +15,63 @@ Each task in HeartKit has a corresponding demo mode that allows you to run a tas ## Inference Backends -HeartKit includes two built-in backend inference engines: PC and EVB. Additional backends can be easily added to the HeartKit framework by creating a new backend class and registering it to the backend factory. +HeartKit includes two built-in backend inference engines: PC and EVB. Additional backends can be easily added to the HeartKit framework by creating a new backend class and registering it to the backend factory, `BackendFactory`. -### PC Backend +### PC Backend Inference Engine -The PC backend is used to run the task-level demo on the local machine. This is useful for quick testing and debugging of the model. +The PC backend is used to run the task-level demo on the local machine via `Keras`. This is useful for quick testing and debugging of the model. -1. Create / modify configuration file (e.g. `segmentation-class-2.json`) +1. Create / modify configuration file (e.g. `configuration.json`) 1. Ensure "pc" is selected as the backend in configuration file. -1. Run demo `heartkit --mode demo --task segmentation --config ./configs/segmentation-class-2.json` +1. Run demo `heartkit --mode demo --task segmentation --config ./configuration.json` 1. HTML report will be saved to `${job_dir}/report.html` -### EVB Backend +### EVB Backend Inference Engine -The EVB backend is used to run the task-level demo on an Ambiq EVB. This is useful to showcase the model's performance in real-time and to verify its accuracy in a real-world scenario. +The EVB backend is used to run the task-level demo on an Ambiq EVB. This is useful to showcase the model's performance in real-time and to verify its accuracy on deployed hardware. -1. Create / modify configuration file (e.g. `segmentation-class-2.json`) -1. Ensure "evb" is selected as the backend in configuration file. +1. Create / modify configuration file (e.g. `configuration.json`) +1. Ensure "evb" is selected as the `backend` in configuration file. 1. Plug EVB into PC via two USB-C cables. 1. Build and flash firmware to EVB `cd evb && make && make deploy` -1. Run demo `heartkit --mode demo --task beat --config ./configs/segmentation-class-2.json` +1. Run demo `heartkit --mode demo --task beat --config ./configuration.json` 1. HTML report will be saved to `${job_dir}/report.html` ### Bring-Your-Own-Backend -Similar to datasets, tasks, and models, the demo mode can be customized to use your own backend inference engine. HeartKit includes a backend factory (`BackendFactory`) that is used to create and run the backend engine. +Similar to datasets, dataloaders, tasks, and models, the demo mode can be customized to use your own backend inference engine. HeartKit includes a backend factory (`BackendFactory`) that is used to create and run the backend engine. #### How it Works -1. **Create a Backend**: Define a new backend by creating a new Python file. The file should contain a class that inherits from the `DemoBackend` base class and implements the required methods. +1. **Create a Backend**: Define a new backend class that inherits from the `HKInferenceBackend` base class and implements the required abstract methods. ```python import heartkit as hk - class CustomBackend(hk.HKBackend): - def __init__(self, config): - super().__init__(config) + class CustomBackend(hk.HKInferenceBackend): + """Custom backend inference engine""" - def run(self, model, data): + def __init__(self, params: hk.HKTaskParams) -> None: + self.params = params + + def open(self): + """Open backend""" + pass + + def close(self): + """Close backend""" + pass + + def set_inputs(self, inputs: npt.NDArray): + """Set inputs""" + pass + + def perform_inference(self): + """Perform inference""" + pass + + def get_outputs(self) -> npt.NDArray: + """Get outputs""" pass ``` @@ -89,7 +108,7 @@ The following is an example of a task-level demo report for the segmentation tas === "CLI" ```bash - heartkit -m export -t segmentation -c ./configs/segmentation-class-2.json + heartkit -m export -t segmentation -c ./configuration.json ``` === "Python" @@ -105,6 +124,6 @@ The following is an example of a task-level demo report for the segmentation tas ## Arguments -Please refer to [HKDemoParams](../modes/configuration.md#hkdemoparams) for the list of arguments that can be used with the `demo` command. +Please refer to [HKTaskParams](../modes/configuration.md) for the list of arguments that can be used with the `demi` command. --- diff --git a/docs/modes/download.md b/docs/modes/download.md index c7118dd1..55aa6142 100644 --- a/docs/modes/download.md +++ b/docs/modes/download.md @@ -1,24 +1,49 @@ # Download Datasets +## Introduction + The `download` command is used to download all datasets specified. Please refer to [Datasets](../datasets/index.md) for details on the available datasets. Additional datasets can be added by creating a new dataset class and registering it with __HeartKit__ dataset factory. ## Usage -!!! Example +### CLI + +Using the CLI, the `download` command can be used to download specified datasets in the configuration file or directly in the command line. + +```bash +heartkit -m download -c '{"datasets": [{"name": "ptbxl", "parameters": {"path": ".datatasets/ptbxl"}}]}' +``` + +### Python + +Using HeartKit in Python, the `download` method can be used for a specific dataset. - The following command will download and prepare four datasets. +```python +import heartkit as hk - === "CLI" +ds = hk.DatasetFactory.get("ptbxl")(path=".datasets/ptbxl") +ds.download() +``` - ```bash - heartkit -m download -c ./configs/download-datasets.json - # ^ No task is required - ``` +To download multiple datasets, the high-level `download_datasets` function can be used. - === "Python" +```python +import heartkit as hk - --8<-- "assets/modes/python-download-snippet.md" +params = hk.HKDownloadParams( + ds_path="./datasets", + datasets=[hk.NamedParams( + name="ptbxl", + parameters={"path": ".datasets/ptbxl"} + ), hk.NamedParams( + name="lsad", + parameters={"path": ".datasets/lsad"} + )] + progress=True +) +hk.datasets.download_datasets(params) +``` ## Arguments diff --git a/docs/modes/evaluate.md b/docs/modes/evaluate.md index af22cde1..7e777743 100644 --- a/docs/modes/evaluate.md +++ b/docs/modes/evaluate.md @@ -4,26 +4,51 @@ Evaluate mode is used to test the performance of the model on the reserved test set for the specified task. Similar to training, the routine can be customized via CLI configuration file or by setting the parameters directly in the code. The evaluation process involves testing the model's performance on the test data to measure its accuracy, precision, recall, and F1 score. A number of results and metrics will be generated and saved to the `job_dir`. +
+ + +1. Load the configuration data (e.g. `configuration.json`) (1) +1. Load the desired datasets and task-specific dataloaders (e.g. `icentia11k`) +1. Load the trained model +1. Evaluate the model +1. Generate evaluation report + +
+ +1. Configuration parameters: +--8<-- "assets/usage/json-configuration.md" + --- ## Usage -!!! Example +### CLI + +The following command will evaluate a rhythm model using the reference configuration. + +```bash +heartkit --task rhythm --mode evaluate --config ./configuration.json +``` + +### Python + +The model can be evaluated using the following snippet: + +```python - The following command will evaluate the rhythm model using the reference configuration: +task = hk.TaskFactory.get("rhythm") - === "CLI" +params = hk.HKTaskParams(...) # (1) - ```bash - heartkit --mode evaluate --task rhythm --config ./configs/rhythm-class-2.json - ``` +task.evaluate(params) - === "Python" +``` - --8<-- "assets/modes/python-evaluate-snippet.md" +1. Configuration parameters: +--8<-- "assets/usage/python-configuration.md" --- ## Arguments -Please refer to [HKTestParams](../modes/configuration.md#hktestparams) for the list of arguments that can be used with the `evaluate` command. +Please refer to [HKTaskParams](../modes/configuration.md#hktaskparams) for the list of arguments that can be used with the `evaluate` command. diff --git a/docs/modes/export.md b/docs/modes/export.md index 0853a290..0c0864c3 100644 --- a/docs/modes/export.md +++ b/docs/modes/export.md @@ -4,25 +4,51 @@ Export mode is used to convert the trained TensorFlow model into a format that can be used for deployment onto Ambiq's family of SoCs. Currently, the command will convert the TensorFlow model into both TensorFlow Lite (TFL) and TensorFlow Lite for micro-controller (TFLM) variants. The command will also verify the models' outputs match. The activations and weights can be quantized by configuring the `quantization` section in the configuration file or by setting the `quantization` parameter in the code. +
+ +1. Load the configuration data (e.g. `configuration.json`) (1) +1. Load the desired datasets and task-specific dataloaders (e.g. `icentia11k`) +1. Load the trained model +1. Convert the model (e.g. TFL, TFLM) +1. Verify the models' outputs match +1. Save the converted model + +
+ +1. Configuration parameters: +--8<-- "assets/usage/json-configuration.md" + --- + ## Usage -!!! Example +### CLI + +The following command will export a rhythm model using the reference configuration. + +```bash +heartkit --task rhythm --mode export --config ./configuration.json +``` + +### Python + +The model can be evaluated using the following snippet: + +```python - The following command will export the rhythm model to TF Lite and TFLM: +task = hk.TaskFactory.get("rhythm") - === "CLI" +params = hk.HKTaskParams(...) # (1) - ```bash - heartkit --mode export --task rhythm --config ./configs/rhythm-class-2.json - ``` +task.export(params) - === "Python" +``` - --8<-- "assets/modes/python-export-snippet.md" +1. Configuration parameters: +--8<-- "assets/usage/python-configuration.md" --- ## Arguments -Please refer to [HKExportParams](../modes/configuration.md#hkexportparams) for the list of arguments that can be used with the `evaluate` command. +Please refer to [HKTaskParams](../modes/configuration.md#hktaskparams) for the list of arguments that can be used with the `export` command. diff --git a/docs/modes/index.md b/docs/modes/index.md index 7f8993be..e05e5690 100644 --- a/docs/modes/index.md +++ b/docs/modes/index.md @@ -1,12 +1,14 @@ -# HeartKit Modes +# HeartKit Task Modes ## Introduction Rather than offering a handful of static models, HeartKit provides a complete framework designed to cover the entire design process of creating customized ML models well-suited for low-power, wearable applications. Each mode serves a specific purpose and is engineered to offer you the flexibility and efficiency required for different tasks and use-cases. +Besides `download`, each `Task` implementes routines for each of the modes: `train`, `evaluate`, `export`, and `demo`. These modes are designed to streamline the process of training, evaluating, exporting, and running task-level demonstrations on the trained models. + --- -## Available Modes +## Available Modes - **[Download](./download.md)**: Download specified datasets - **[Train](./train.md)**: Train a model for specified task and datasets diff --git a/docs/modes/train.md b/docs/modes/train.md index 4dc546c4..f58c00a7 100644 --- a/docs/modes/train.md +++ b/docs/modes/train.md @@ -2,38 +2,57 @@ ## Introduction -Each task provides a mode to train a model on the specified datasets. The training mode can be invoked either via CLI or within `heartkit` python package. At a high level, the training mode performs the following actions based on the provided configuration parameters: +Each task provides a mode to train a model on the specified datasets and dataloaders. The training mode can be invoked either via CLI or within `heartkit` python package. At a high level, the training mode performs the following actions based on the provided configuration parameters: -1. Load the configuration data (e.g. `rhythm-class-2.json`) -1. Load the desired datasets (e.g. `icentia11k`) +
+ +1. Load the configuration data (e.g. `configuration.json`) (1) +1. Load the desired datasets and task-specific dataloaders (e.g. `icentia11k`) 1. Load the custom model architecture (e.g. `tcn`) 1. Train the model 1. Save the trained model 1. Generate training report +
+ +1. Configuration parameters: +--8<-- "assets/usage/json-configuration.md" + --- ## Usage -!!! Example +### CLI + +The following command will train a rhythm model using the reference configuration. + +```bash +heartkit --task rhythm --mode train --config ./configuration.json +``` + +### Python + +The model can be trained using the following snippet: + +```python + +task = hk.TaskFactory.get("rhythm") - The following command will train a rhythm model using the reference configuration: +params = hk.HKTaskParams(...) # (1) - === "CLI" +task.train(params) - ```bash - heartkit --task rhythm --mode train --config ./configs/rhythm-class-2.json - ``` +``` - === "Python" +1. Configuration parameters: +--8<-- "assets/usage/python-configuration.md" - --8<-- "assets/modes/python-train-snippet.md" --- ## Arguments -Please refer to [HKTrainParams](../modes/configuration.md#hktrainparams) for the list of arguments that can be used with the `train` command. +Please refer to [HKTaskParams](../modes/configuration.md#hktaskparams) for the list of arguments that can be used with the `train` command. --- diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 00000000..702c96bf --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block content %} +{% if page.nb_url %} + + {% include ".icons/material/download.svg" %} + +{% endif %} + +{{ super() }} +{% endblock content %} diff --git a/docs/quickstart.md b/docs/quickstart.md index b83734b4..416c3612 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -25,24 +25,30 @@ We provide several installation methods including pip, poetry, and Docker. Insta When using editable mode via Poetry, be sure to activate the python environment: `poetry shell`.
On Windows using Powershell, use `.venv\Scripts\activate.ps1`. - === "Pip/Poetry install" + === "PyPI install" Install the HeartKit package using pip or Poetry. Visit the Python Package Index (PyPI) for more details on the package: [https://pypi.org/project/heartkit/](https://pypi.org/project/heartkit/) + ```bash + # Install with pip + pip install heartkit + ``` + + Or, if you prefer to use Poetry, you can install the package with the following command: ```bash # Install with poetry poetry add heartkit ``` - Or, if you prefer to use Pip, you can install the package with the following command: + Alternatively, you can install the latest development version directly from the GitHub repository. Make sure to have the Git command-line tool installed on your system. The @main command installs the main branch and may be modified to another branch, i.e. @canary. ```bash - # Install with pip - pip install heartkit + pip install git+https://github.com/AmbiqAI/heartkit.git@main ``` - Alternatively, you can install the latest development version directly from the GitHub repository. Make sure to have the Git command-line tool installed on your system. The @main command installs the main branch and may be modified to another branch, i.e. @release. + + Or, using Poetry: ```bash poetry add git+https://github.com/AmbiqAI/heartkit.git@main @@ -65,7 +71,7 @@ Once installed, __HeartKit__ can be used as either a CLI-based tool or as a Pyth ## Use HeartKit with CLI -The HeartKit command line interface (CLI) allows for simple single-line commands without the need for a Python environment. The CLI requires no customization or Python code. You can simply run all tasks from the terminal with the __heartkit__ command. Check out the [CLI Guide](./usage/cli.md) to learn more about available options. +The HeartKit command line interface (CLI) allows for simple single-line commands without the need for a Python environment. The CLI requires no customization or Python code. You can simply run all the built-in tasks from the terminal with the __heartkit__ command. Check out the [CLI Guide](./usage/cli.md) to learn more about available options. !!! example @@ -92,35 +98,35 @@ The HeartKit command line interface (CLI) allows for simple single-line commands Download datasets specified in the configuration file. ```bash - heartkit -m download -c ./configs/download-datasets.json + heartkit -m download -c ./download-datasets.json ``` === "Train" Train a rhythm model using the supplied configuration file. ```bash - heartkit -m train -t rhythm -c ./configs/rhythm-class-2.json + heartkit -m train -t rhythm -c ./configuration.json ``` === "Evaluate" Evaluate the trained rhythm model using the supplied configuration file. ```bash - heartkit -m evaluate -t rhythm -c ./configs/rhythm-class-2.json + heartkit -m evaluate -t rhythm -c ./configuration.json ``` === "Demo" Run demo on trained rhythm model using the supplied configuration file. ```bash - heartkit -m demo -t rhythm -c ./configs/rhythm-class-2.json + heartkit -m demo -t rhythm -c ./configuration.json ``` ## Use HeartKit with Python -The __HeartKit__ Python package allows for more fine-grained control and customization. You can use the package to train, evaluate, and deploy models for a variety of tasks. The package is designed to be simple and easy to use. +The __HeartKit__ Python package allows for more fine-grained control and customization. You can use the package to train, evaluate, and deploy models for a variety of tasks. You can create custom datasets, models, and tasks and register them with corresponding factories and use them like built-in tasks. -For example, you can create a custom model, train it, evaluate its performance on a validation set, and even export a quantized TensorFlow Lite model for deployment. Check out the [Python Guide](./usage/python.md) to learn more about using HeartKit as a Python package. +For example, you can create a custom task, train it, evaluate its performance on a validation set, and even export a quantized TensorFlow Lite model for deployment. Check out the [Python Guide](./usage/python.md) to learn more about using HeartKit as a Python package. !!! Example @@ -129,32 +135,31 @@ For example, you can create a custom model, train it, evaluate its performance o ds_params = hk.HKDownloadParams( ds_path="./datasets", - datasets=["ludb", "synthetic"], + datasets=["ludb", "ecg-synthetic"], progress=True ) - - with open("configuration.json", "r", encoding="utf-8") as file: - config = json.load(file) - - train_params = hk.HKTrainParams.model_validate(config) - test_params = hk.HKTestParams.model_validate(config) - export_params = hk.HKExportParams.model_validate(config) - # Download datasets hk.datasets.download_datasets(ds_params) + # Generate task parameters from configuration + params = hk.HKTaskParams(...) # Expand to see example (1) + task = hk.TaskFactory.get("rhythm") # Train rhythm model - task.train(train_params) + task.train(params) # Evaluate rhythm model - task.evaluate(test_params) + task.evaluate(params) # Export rhythm model - task.export(export_params) + task.export(params) ``` + 1. Configuration parameters: + --8<-- "assets/usage/python-configuration.md" + + --- diff --git a/docs/tasks/beat.md b/docs/tasks/beat.md index 9cb36dd8..6148a6f0 100644 --- a/docs/tasks/beat.md +++ b/docs/tasks/beat.md @@ -12,10 +12,19 @@ In beat classification, we classify individual beats as either normal or abnorma ## Characteristics -| | Atrial | Junctional | Ventricular | +| | Atrial | Junctional | Ventricular | | --- | --- | --- | --- | -| Premature | __PAC__
P-wave: Different
QRS: Narrow (normal)
Aberrated: LBBB or RBBB | __PJC__
P-wave: None / retrograde
QRS: Narrow (normal)
Compensatory SA Pause | __PVC__
P-wave: None
QRS: Wide (> 120 ms)
Compensatory SA PauseEscape | -| Atrial Escape | P-wave: Abnormal
QRS: Narrow (normal)
Ventricular rate: < 60 bpm
Junctional Escape
| P-wave: None
QRS: Narrow (normal)
Bradycardia (40-60 bpm)
Ventricular Escape | P-wave: None
QRS: Wide
Bradycardia (< 40 bpm) | +| Premature | __PAC__
P-wave: Different
QRS: Narrow (normal)
Aberrated: LBBB or RBBB | __PJC__
P-wave: None / retrograde
QRS: Narrow (normal)
Compensatory SA Pause | __PVC__
P-wave: None
QRS: Wide (> 120 ms)
Compensatory SA Pause | +| Escape | Atrial Escape | P-wave: Abnormal
QRS: Narrow (normal)
Ventricular rate: < 60 bpm
Junctional Escape
| P-wave: None
QRS: Narrow (normal)
Bradycardia (40-60 bpm)
Ventricular Escape | P-wave: None
QRS: Wide
Bradycardia (< 40 bpm) | + +--- + +## Dataloaders + +Dataloaders are available for the following datasets: + +* **[Icentia11k](../datasets/icentia11k.md)** +* **[PTB-XL](../datasets/ptbxl.md)** --- diff --git a/docs/tasks/denoise.md b/docs/tasks/denoise.md index 74a89d16..6313fd0d 100644 --- a/docs/tasks/denoise.md +++ b/docs/tasks/denoise.md @@ -1,4 +1,4 @@ -# Signal Denoising +# Signal Denoising Task ## Overview @@ -31,6 +31,17 @@ The following table summarizes the characteristics of common noise sources in PP --- +## Dataloaders + +Dataloaders are available for the following datasets: + +* **[LUDB](../datasets/ludb.md)** +* **[PTB-XL](../datasets/ptbxl.md)** +* **[ECG Synthetic](../datasets/synthetic.md)** +* **[PPG Synthetic](../datasets/synthetic.md)** + +--- + ## Pre-trained Models The following table provides the latest performance and accuracy results of denoising models. Additional result details can be found in [Model Zoo → Denoise](../zoo/denoise.md). diff --git a/docs/tasks/index.md b/docs/tasks/index.md index c3c85705..1b702f87 100644 --- a/docs/tasks/index.md +++ b/docs/tasks/index.md @@ -2,25 +2,25 @@ ## Introduction -HeartKit provides several built-in __heart-monitoring__ related tasks. Each task is designed to address a unique aspect such as ECG denoising, segmentation, and rhythm/beat classification. The tasks are designed to be modular and can be used independently or in combination to address specific use cases. In addition to the built-in tasks, custom tasks can be created by extending the `HKTask` base class and registering it with the task factory. +HeartKit provides several built-in __heart-monitoring__ tasks. Each task is designed to address a unique aspect such as ECG denoising, segmentation, and rhythm/beat classification. The tasks are designed to be modular and can be used independently or in combination to address specific use cases. In addition to the built-in tasks, custom tasks can be created by extending the `HKTask` base class and registering it with the task factory. ## Available Tasks ### [Denoise](./denoise.md) -ECG denoising is the process of removing noise from an ECG signal. This task is useful for improving the quality of the ECG signal and for further downstream tasks such as segmentation. +[Signal denoise](./denoise.md) is the process of removing noise from an ECG signal. This task is useful for improving the quality of the ECG signal and for further downstream tasks such as segmentation. ### [Segmentation](./segmentation.md) -ECG segmentation is the process of delineating an ECG signal into individual waves (e.g. P-wave, QRS, T-wave). This task is useful for extracting features (e.g. HRV) from the ECG signal and for further analysis such as rhythm classification. +[Signal segmentation](./segmentation.md) is the process of delineating a signal into its constituent parts. In the context of ECG, segmentation refers to delineating the ECG signal into individual waves (e.g. P-wave, QRS, T-wave). This task is useful for extracting features (e.g. HRV) from the ECG signal and for further analysis such as rhythm classification. ### [Rhythm](./rhythm.md) -Rhythm classification is the process of identifying abnormal heart rhythms, also known as arrhythmias, such as atrial fibrillation (AFIB) and atrial flutter (AFL). Cardiovascular diseases such as AFIB are a leading cause of morbidity and mortality worldwide. Being able to remotely identify heart arrhtyhmias is important for early detection and intervention. +[Rhythm classification](./rhythm.md) is the process of identifying abnormal heart rhythms, also known as arrhythmias, such as atrial fibrillation (AFIB) and atrial flutter (AFL). Cardiovascular diseases such as AFIB are a leading cause of morbidity and mortality worldwide. Being able to remotely identify heart arrhtyhmias is important for early detection and intervention. ### [Beat](./beat.md) -Beat classification is the process of identifying and classifying individual heart beats such as normal, premature, and escape beats. By identifying abnormal heart beats, it is possible to detect and monitor various heart conditions. +[Beat classification](./beat.md) is the process of identifying and classifying individual heart beats such as normal, premature, and escape beats. By identifying abnormal heart beats, it is possible to detect and monitor various heart conditions. +--- --> diff --git a/docs/zoo/denoise.md b/docs/zoo/denoise.md index 21a3800c..8d45ae06 100644 --- a/docs/zoo/denoise.md +++ b/docs/zoo/denoise.md @@ -68,15 +68,15 @@ The following table provides the latest pre-trained models for ECG denoising. Be | MSE | 4.4% | | COSSIM | 97.4% | -## EVB Performance + -## EVB Performance + diff --git a/docs/zoo/index.md b/docs/zoo/index.md index d137d89f..bc5e1cc7 100644 --- a/docs/zoo/index.md +++ b/docs/zoo/index.md @@ -37,3 +37,20 @@ The following table provides the latest performance and accuracy results for bea The following table provides the latest performance and accuracy results for multi-label diagnostic classification models. Additional result details can be found in [Zoo → Diagnostic](./diagnostic.md). --8<-- "assets/zoo/diagnostic/diagnostic-model-zoo-table.md" --> + + +## Reproducing results + +Each pre-trained model has a corresponding `configuration.json` file that can be used to reproduce the model and results. + +To reproduce a pre-trained rhythm model with configuration file `configuration.json`, run the following command: + +```bash +heartkit -m train -t rhythm -c configuration.json +``` + +To evaluate the trained rhythm model with configuration file `configuration.json`, run the following command: + +```bash +heartkit -m evaluate -t rhythm -c configuration.json +``` diff --git a/docs/zoo/rhythm.md b/docs/zoo/rhythm.md index 23c1e715..89a23862 100644 --- a/docs/zoo/rhythm.md +++ b/docs/zoo/rhythm.md @@ -89,7 +89,7 @@ The following table provides the latest pre-trained models for rhythm classifica --- -## EVB Performance + --- - - diff --git a/heartkit/__init__.py b/heartkit/__init__.py index 4fe8ad03..84cd5a69 100644 --- a/heartkit/__init__.py +++ b/heartkit/__init__.py @@ -1,26 +1,22 @@ import os from importlib.metadata import version -from . import cli, datasets, metrics, models, rpc, tasks -from .datasets import DatasetFactory, HKDataset +from . import cli, datasets, models, rpc, tasks +from .datasets import DatasetFactory, HKDataset, HKDataloader from .defines import ( - AugmentationParams, QuantizationParams, - DatasetParams, - HKDemoParams, HKDownloadParams, - HKExportParams, + HKTaskParams, HKMode, - HKTestParams, - HKTrainParams, - PreprocessParams, + NamedParams, ) from .models import ModelFactory from .tasks import HKBeat, HKRhythm, HKSegment, HKTask, TaskFactory -from .utils import setup_logger, silence_tensorflow +from .rpc import BackendFactory +import neuralspot_edge as nse __version__ = version(__name__) if "TF_CPP_MIN_LOG_LEVEL" not in os.environ: os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" -setup_logger(__name__) +nse.utils.setup_logger(__name__) diff --git a/heartkit/cli.py b/heartkit/cli.py index 24b47a9e..b12d14a1 100644 --- a/heartkit/cli.py +++ b/heartkit/cli.py @@ -3,20 +3,14 @@ from argdantic import ArgField, ArgParser from pydantic import BaseModel +import neuralspot_edge as nse from .datasets import download_datasets -from .defines import ( - HKDemoParams, - HKDownloadParams, - HKExportParams, - HKMode, - HKTestParams, - HKTrainParams, -) +from .defines import HKDownloadParams, HKMode, HKTaskParams from .tasks import TaskFactory -from .utils import setup_logger -logger = setup_logger(__name__) + +logger = nse.utils.setup_logger(__name__) cli = ArgParser() @@ -60,18 +54,19 @@ def _run( task_handler = TaskFactory.get(task) + params = parse_content(HKTaskParams, config) match mode: case HKMode.train: - task_handler.train(parse_content(HKTrainParams, config)) + task_handler.train(params) case HKMode.evaluate: - task_handler.evaluate(parse_content(HKTestParams, config)) + task_handler.evaluate(params) case HKMode.export: - task_handler.export(parse_content(HKExportParams, config)) + task_handler.export(params) case HKMode.demo: - task_handler.demo(parse_content(HKDemoParams, config)) + task_handler.demo(params) case _: logger.error("Error: Unknown command") diff --git a/heartkit/datasets/__init__.py b/heartkit/datasets/__init__.py index b22d290f..44ad596b 100644 --- a/heartkit/datasets/__init__.py +++ b/heartkit/datasets/__init__.py @@ -1,31 +1,25 @@ -from .augmentation import augment_pipeline +from .augmentation import create_augmentation_pipeline from .bidmc import BidmcDataset from .dataset import HKDataset -from .defines import PatientGenerator, Preprocessor +from .defines import PatientGenerator from .download import download_datasets -from .icentia11k import IcentiaDataset -from .lsad import LsadDataset -from .ludb import LudbDataset +from .dataloader import HKDataloader +from .icentia11k import IcentiaDataset, IcentiaBeat, IcentiaRhythm +from .icentia_mini import IcentiaMiniDataset, IcentiaMiniRhythm, IcentiaMiniBeat +from .lsad import LsadDataset, LsadScpCode +from .ludb import LudbDataset, LudbSegmentation from .nstdb import NstdbNoise -from .preprocessing import preprocess_pipeline -from .ptbxl import PtbxlDataset +from .ptbxl import PtbxlDataset, PtbxlScpCode from .qtdb import QtdbDataset -from .synthetic import SyntheticDataset -from .syntheticppg import SyntheticPpgDataset -from .utils import ( - create_dataset_from_data, - create_interleaved_dataset_from_generator, - random_id_generator, - uniform_id_generator, -) -from ..utils import create_factory - -DatasetFactory = create_factory(factory="HKDatasetFactory", type=HKDataset) +from .ecg_synthetic import EcgSyntheticDataset +from .ppg_synthetic import PpgSyntheticDataset +from .factory import DatasetFactory DatasetFactory.register("bidmc", BidmcDataset) -DatasetFactory.register("synthetic", SyntheticDataset) -DatasetFactory.register("syntheticppg", SyntheticPpgDataset) +DatasetFactory.register("ecg-synthetic", EcgSyntheticDataset) +DatasetFactory.register("ppg-synthetic", PpgSyntheticDataset) DatasetFactory.register("icentia11k", IcentiaDataset) +DatasetFactory.register("icentia_mini", IcentiaMiniDataset) DatasetFactory.register("lsad", LsadDataset) DatasetFactory.register("ludb", LudbDataset) DatasetFactory.register("qtdb", QtdbDataset) @@ -39,5 +33,6 @@ "LudbDataset", "PtbxlDataset", "QtdbDataset", - "SyntheticDataset", + "EcgSyntheticDataset", + "NstdbNoise", ] diff --git a/heartkit/datasets/augmentation.py b/heartkit/datasets/augmentation.py index 02ac0720..c13c00b9 100644 --- a/heartkit/datasets/augmentation.py +++ b/heartkit/datasets/augmentation.py @@ -1,124 +1,106 @@ +import keras import numpy as np -import numpy.typing as npt -import physiokit as pk +import neuralspot_edge as nse -from ..defines import AugmentationParams +from ..defines import NamedParams from .nstdb import NstdbNoise -_nstdb_glb: NstdbNoise | None = None +def create_augmentation_layer(augmentation: NamedParams, sampling_rate: int) -> keras.Layer: + """Create an augmentation layer from a configuration + + Args: + augmentation (NamedParams): Augmentation configuration + sampling_rate (int): Sampling rate of the data + + Returns: + keras.Layer: Augmentation layer + + Example: + + ```python + import heartkit as hk + x = keras.random.normal + layer = hk.datasets.augmentation.create_augmentation_layer( + hk.NamedParams(name="random_noise", params={"factor": 0.01}), + sampling_rate=100 + ) + y = layer(x) + ``` + """ + match augmentation.name: + case "amplitude_warp": + return nse.layers.preprocessing.AmplitudeWarp(sample_rate=sampling_rate, **augmentation.params) + case "augmentation_pipeline": + return create_augmentation_pipeline(augmentation.params) + case "random_augmentation": + return nse.layers.preprocessing.RandomAugmentation1DPipeline( + layers=[ + create_augmentation_layer(augmentation, sampling_rate=sampling_rate) + for augmentation in [NamedParams(**p) for p in augmentation.params["layers"]] + ], + augmentations_per_sample=augmentation.params.get("augmentations_per_sample", 3), + rate=augmentation.params.get("rate", 1.0), + batchwise=True, + ) + case "random_background_noise": + nstdb = NstdbNoise(target_rate=sampling_rate) + noises = np.hstack( + (nstdb.get_noise(noise_type="bw"), nstdb.get_noise(noise_type="ma"), nstdb.get_noise(noise_type="em")) + ) + noises = noises.astype(np.float32) + return nse.layers.preprocessing.RandomBackgroundNoises1D(noises=noises, **augmentation.params) + case "random_sine_wave": + return nse.layers.preprocessing.RandomSineWave(**augmentation.params, sample_rate=sampling_rate) + case "random_cutout": + return nse.layers.preprocessing.RandomCutout1D(**augmentation.params) + case "random_noise": + return nse.layers.preprocessing.RandomGaussianNoise1D(**augmentation.params) + case "random_noise_distortion": + return nse.layers.preprocessing.RandomNoiseDistortion1D(sample_rate=sampling_rate, **augmentation.params) + case "resizing": + return nse.layers.preprocessing.Resizing1D(**augmentation.params) + case "sine_wave": + return nse.layers.preprocessing.AddSineWave(**augmentation.params) + case "filter": + return nse.layers.preprocessing.CascadedBiquadFilter(sample_rate=sampling_rate, **augmentation.params) + case "layer_norm": + return nse.layers.preprocessing.LayerNormalization1D(**augmentation.params) + case _: + raise ValueError(f"Unknown augmentation '{augmentation.name}'") + # END MATCH -def augment_pipeline( - x: npt.NDArray, - augmentations: list[AugmentationParams] | None = None, - sample_rate: float = 1000, -) -> tuple[npt.NDArray, npt.NDArray | None]: - """Apply augmentation pipeline + +def create_augmentation_pipeline( + augmentations: list[NamedParams], sampling_rate: int +) -> nse.layers.preprocessing.AugmentationPipeline: + """Create an augmentation pipeline from a list of augmentation configurations. + + This is useful when running from a configuration file to hydrate the pipeline. Args: - x (npt.NDArray): Signal - augmentations (list[AugmentationParams]): Augmentations to apply - sample_rate: Sampling rate in Hz. + augmentations (list[NamedParams]): List of augmentation configurations + sampling_rate (int): Sampling rate of the data Returns: - npt.NDArray: Augmented signal + nse.layers.preprocessing.AugmentationPipeline: Augmentation pipeline + + Example: + + ```python + import heartkit as hk + x = keras.random.normal(shape=(256, 1), dtype="float32") + + augmenter = hk.datasets.create_augmentation_pipeline([ + hk.NamedParams(name="random_noise", params={"factor": 0.01}), + hk.NamedParams(name="random_cutout", params={"factor": 0.01, "cutouts": 2}), + ], sampling_rate=100) + + y = augmenter(x) """ - x_sd = np.nanstd(x) - augmentations = augmentations or [] - for augmentation in augmentations: - args = augmentation.params - match augmentation.name: - case "baseline_wander": - amplitude = args.get("amplitude", [0.05, 0.06]) - frequency = args.get("frequency", [0, 1]) - x = pk.signal.add_baseline_wander( - x, - amplitude=np.random.uniform(amplitude[0], amplitude[1]), - frequency=np.random.uniform(frequency[0], frequency[1]), - sample_rate=sample_rate, - signal_sd=x_sd, - ) - case "motion_noise": - amplitude = args.get("amplitude", [0.5, 1.0]) - frequency = args.get("frequency", [0.4, 0.6]) - x = pk.signal.add_motion_noise( - x, - amplitude=np.random.uniform(amplitude[0], amplitude[1]), - frequency=np.random.uniform(frequency[0], frequency[1]), - sample_rate=sample_rate, - signal_sd=x_sd, - ) - case "burst_noise": - amplitude = args.get("amplitude", [0.05, 0.5]) - frequency = args.get("frequency", [sample_rate / 4, sample_rate / 2]) - burst_number = args.get("burst_number", [0, 2]) - x = pk.signal.add_burst_noise( - x, - amplitude=np.random.uniform(amplitude[0], amplitude[1]), - frequency=np.random.uniform(frequency[0], frequency[1]), - num_bursts=np.random.randint(burst_number[0], burst_number[1]), - sample_rate=sample_rate, - signal_sd=x_sd, - ) - case "powerline_noise": - amplitude = args.get("amplitude", [0.005, 0.01]) - frequency = args.get("frequency", [50, 60]) - x = pk.signal.add_powerline_noise( - x, - amplitude=np.random.uniform(amplitude[0], amplitude[1]), - frequency=np.random.uniform(frequency[0], frequency[1]), - sample_rate=sample_rate, - signal_sd=x_sd, - ) - case "noise_sources": - num_sources = args.get("num_sources", [1, 2]) - amplitude = args.get("amplitude", [0, 0.1]) - frequency = args.get("frequency", [0, sample_rate / 2]) - num_sources: int = np.random.randint(num_sources[0], num_sources[1]) - x = pk.signal.add_noise_sources( - x, - amplitudes=[np.random.uniform(amplitude[0], amplitude[1]) for _ in range(num_sources)], - frequencies=[np.random.uniform(frequency[0], frequency[1]) for _ in range(num_sources)], - noise_shapes=["laplace" for _ in range(num_sources)], - sample_rate=sample_rate, - signal_sd=x_sd, - ) - case "lead_noise": - scale = args.get("scale", [0.05, 0.25]) - x = pk.signal.add_lead_noise( - x, - scale=x_sd * np.random.uniform(scale[0], scale[1]), - ) - case "cutout": - feat_len = x.shape[0] - prob = args.get("probability", [0, 0.25])[1] - amp = args.get("amplitude", [0, 0]) - width = args.get("width", [0, 1]) - ctype = args.get("type", "cut")[0] - if np.random.rand() < prob: - dur = int(np.random.uniform(width[0], width[1]) * feat_len) - start = np.random.randint(0, feat_len - dur) - stop = start + dur - scale = np.random.uniform(amp[0], amp[1]) * x_sd - if ctype == 0: # Cut - x[start:stop] = 0 - else: # noise - x[start:stop] += np.random.normal(0, scale, size=x[start:stop].shape) - # END IF - # END IF - - case "nstdb": - global _nstdb_glb # pylint: disable=global-statement - if _nstdb_glb is None: - _nstdb_glb = NstdbNoise(target_rate=sample_rate) - _nstdb_glb.set_target_rate(sample_rate) - noise_range = args.get("noise_level", [0.1, 0.1]) - noise_level = np.random.uniform(noise_range[0], noise_range[1]) - x = _nstdb_glb.apply_noise(x, noise_level) - - case _: # default - pass - # raise ValueError(f"Unknown augmentation '{augmentation.name}'") - # END MATCH - # END FOR - return x + if not augmentations: + return keras.layers.Lambda(lambda x: x) + aug = nse.layers.preprocessing.AugmentationPipeline( + layers=[create_augmentation_layer(augmentation, sampling_rate=sampling_rate) for augmentation in augmentations] + ) + return aug diff --git a/heartkit/datasets/bidmc.py b/heartkit/datasets/bidmc.py index 7a31444b..c5660509 100644 --- a/heartkit/datasets/bidmc.py +++ b/heartkit/datasets/bidmc.py @@ -1,7 +1,6 @@ import contextlib import functools import logging -import os import random from typing import Generator @@ -24,12 +23,10 @@ class BidmcDataset(HKDataset): def __init__( self, - ds_path: os.PathLike, leads: list[int] | None = None, + **kwargs, ) -> None: - super().__init__( - ds_path=ds_path, - ) + super().__init__(**kwargs) self.leads = leads or list(BidmcLeadsMap.values()) @property @@ -94,7 +91,7 @@ def patient_data(self, patient_id: int) -> Generator[h5py.Group, None, None]: Returns: Generator[h5py.Group, None, None]: Patient data """ - with h5py.File(self.ds_path / f"{self._pt_key(patient_id)}.h5", mode="r") as h5: + with h5py.File(self.path / f"{self._pt_key(patient_id)}.h5", mode="r") as h5: yield h5 def signal_generator( @@ -118,7 +115,7 @@ def signal_generator( if target_rate is None: target_rate = self.sampling_rate - input_size = int(np.round((self.sampling_rate / target_rate) * frame_size)) + input_size = int(np.ceil((self.sampling_rate / target_rate) * frame_size)) for pt in patient_generator: with self.patient_data(pt) as h5: data: h5py.Dataset = h5["data"][:] @@ -130,6 +127,7 @@ def signal_generator( x = np.nan_to_num(x).astype(np.float32) if self.sampling_rate != target_rate: x = pk.signal.resample_signal(x, self.sampling_rate, target_rate, axis=0) + x = x[:frame_size] # END IF yield x # END FOR diff --git a/heartkit/datasets/dataloader.py b/heartkit/datasets/dataloader.py index 33263f91..df4122fb 100644 --- a/heartkit/datasets/dataloader.py +++ b/heartkit/datasets/dataloader.py @@ -1,192 +1,210 @@ import functools import logging -import math -import os -from typing import Callable, Generator +from typing import Generator +from collections.abc import Iterable import numpy as np import numpy.typing as npt import tensorflow as tf +import neuralspot_edge as nse + -from ..utils import load_pkl, save_pkl from .dataset import HKDataset -from .defines import PatientGenerator, Preprocessor -from .utils import ( - create_dataset_from_data, - create_interleaved_dataset_from_generator, - uniform_id_generator, -) logger = logging.getLogger(__name__) -def train_val_dataloader( - ds: HKDataset, - spec: tuple[tf.TensorSpec, tf.TensorSpec], - data_generator: Callable[ - [PatientGenerator, int | list[int]], Generator[tuple[npt.NDArray, npt.NDArray], None, None] - ], - id_generator: PatientGenerator | None = None, - train_patients: float | None = None, - val_patients: float | None = None, - val_pt_samples: int | None = None, - val_file: os.PathLike | None = None, - val_size: int | None = None, - label_map: dict[int, int] | None = None, - label_type: str | None = None, - preprocess: Preprocessor | None = None, - val_preprocess: Preprocessor | None = None, - num_workers: int = 1, -) -> tuple[tf.data.Dataset, tf.data.Dataset]: - """Load training and validation TF datasets - - Args: - train_patients (float | None, optional): # or proportion of train patients. Defaults to None. - val_patients (float | None, optional): # or proportion of val patients. Defaults to None. - train_pt_samples (int | list[int] | None, optional): # samples per patient for training. Defaults to None. - val_pt_samples (int | list[int] | None, optional): # samples per patient for validation. Defaults to None. - val_size (int | None, optional): Validation size. Defaults to 200*len(val_patient_ids). - val_file (str | None, optional): Path to existing pickled validation file. Defaults to None. - num_workers (int, optional): # of parallel workers. Defaults to 1. - - Returns: - tuple[tf.data.Dataset, tf.data.Dataset]: Training and validation datasets - """ - - if id_generator is None: - id_generator = functools.partial(uniform_id_generator, repeat=True) - - if val_patients is not None and val_patients >= 1: - val_patients = int(val_patients) - - if val_preprocess is None: - val_preprocess = preprocess - - val_pt_samples = val_pt_samples or 100 - - # Get train patients - train_patient_ids = ds.get_train_patient_ids() - train_patient_ids = ds.filter_patients_for_labels( - patient_ids=train_patient_ids, - label_map=label_map, - label_type=label_type, - ) - - # Use subset of training patients - if train_patients is not None: - num_pts = int(train_patients) if train_patients > 1 else int(train_patients * len(train_patient_ids)) - train_patient_ids = train_patient_ids[:num_pts] - logger.debug(f"Using {len(train_patient_ids)} training patients") - # END IF - - if ds.cachable and val_file and os.path.isfile(val_file): - logger.debug(f"Loading validation data from file {val_file}") - val = load_pkl(val_file) - val_patient_ids = val["patient_ids"] - train_patient_ids = np.setdiff1d(train_patient_ids, val_patient_ids) - val_ds = create_dataset_from_data(val["x"], val["y"], spec) - - else: - logger.debug("Splitting patients into train and validation") - train_patient_ids, val_patient_ids = ds.split_train_test_patients( +class HKDataloader: + ds: HKDataset + frame_size: int + sampling_rate: int + label_map: dict[int, int] | None + label_type: str | None + + def __init__( + self, + ds: HKDataset, + frame_size: int = 1000, + sampling_rate: int = 100, + label_map: dict[int, int] | None = None, + label_type: str | None = None, + **kwargs, + ): + """HKDataloader is used to create a task specific dataloader for a dataset. + This class should be subclassed for specific task and dataset. If multiple datasets are needed for given task, + multiple dataloaders can be created. To simplify the process, the dataloaders can be placed in an ItemFactory. + + Args: + ds (HKDataset): Dataset + frame_size (int, optional): Frame size. Defaults to 1000. + sampling_rate (int, optional): Sampling rate. Defaults to 100. + label_map (dict[int, int], optional): Label map. Defaults to None. + label_type (str, optional): Label type. Defaults to None. + + Example: + ```python + from typing import Generator + import numpy as np + import numpy.typing as npt + import heartkit as hk + + class MyDataloader(hk.HKDataloader): + def __init__(self, ds: hk.HKDataset, **kwargs): + super().__init__(ds=ds, **kwargs) + + def patient_generator( + self, + patient_id: int, + samples_per_patient: list[int], + ) -> Generator[npt.NDArray, None, None]: + + # Implement patient generator + with ds.patient_data(patient_id) as pt: + for _ in range(samples_per_patient): + data = pt["data"][:] + # Grab random frame and lead + lead = np.random.randint(0, data.shape[0]) + start = np.random.randint(0, data.shape[1] - self.frame_size) + frame = data[lead, start : start + self.frame_size] + yield frame + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[npt.NDArray, None, None]: + for pt_id in nse.utils.uniform_id_generator(patient_ids, shuffle=shuffle): + # Implement data generator + yield data + # END FOR + + """ + self.ds = ds + self.frame_size = frame_size + self.sampling_rate = sampling_rate + self.label_map = label_map + self.label_type = label_type + + def split_train_val_patients( + self, + train_patients: list[int] | float | None = None, + val_patients: list[int] | float | None = None, + ) -> tuple[list[int], list[int]]: + """Split patients into training and validation sets. Unless train_patients or + val_patients are provided, the default is to call the dataset's split_train_test_patients + + Args: + train_patients (list[int] | float | None, optional): Training patients. Defaults to None. + val_patients (list[int] | float | None, optional): Validation patients. Defaults to None. + + Returns: + tuple[list[int], list[int]]: Training and validation patient ids + """ + # Get train patients + train_patient_ids = self.ds.get_train_patient_ids() + train_patient_ids = self.ds.filter_patients_for_labels( patient_ids=train_patient_ids, - test_size=val_patients, - label_map=label_map, - label_type=label_type, - ) - if val_size is None: - num_samples = np.mean(val_pt_samples) if isinstance(val_pt_samples, list) else val_pt_samples - val_size = math.ceil(num_samples * len(val_patient_ids)) - - logger.debug(f"Collecting {val_size} validation samples") - - val_ds = create_interleaved_dataset_from_generator( - data_generator=data_generator, - id_generator=id_generator, - ids=val_patient_ids, - spec=spec, - preprocess=val_preprocess, - num_workers=num_workers, + label_map=self.label_map, + label_type=self.label_type, ) - val_x, val_y = next(val_ds.batch(val_size).as_numpy_iterator()) - val_ds = create_dataset_from_data(val_x, val_y, spec) + # Use subset of training patients + if isinstance(train_patients, Iterable): + train_patient_ids = train_patients - # Cache validation set - if ds.cachable and val_file: - logger.debug(f"Caching the validation set in {val_file}") - os.makedirs(os.path.dirname(val_file), exist_ok=True) - save_pkl(val_file, x=val_x, y=val_y, patient_ids=val_patient_ids) + if train_patients is not None: + num_pts = int(train_patients) if train_patients > 1 else int(train_patients * len(train_patient_ids)) + train_patient_ids = train_patient_ids[:num_pts] + logger.debug(f"Using {len(train_patient_ids)} training patients") # END IF - # END IF - - logger.debug("Building train dataset") - - train_ds = create_interleaved_dataset_from_generator( - data_generator=data_generator, - id_generator=id_generator, - ids=train_patient_ids, - spec=spec, - preprocess=preprocess, - num_workers=num_workers, - ) - - return train_ds, val_ds - - -def test_dataloader( - ds: HKDataset, - spec: tuple[tf.TensorSpec, tf.TensorSpec], - data_generator: Callable[ - [PatientGenerator, int | list[int]], Generator[tuple[npt.NDArray, npt.NDArray], None, None] - ], - id_generator: PatientGenerator | None = None, - test_patients: float | None = None, - test_file: os.PathLike | None = None, - label_map: dict[int, int] | None = None, - label_type: str | None = None, - preprocess: Preprocessor | None = None, - num_workers: int = 1, -) -> tf.data.Dataset: - """Load testing datasets - - Args: - test_patients (float | None, optional): # or proportion of test patients. Defaults to None. - test_pt_samples (int | None, optional): # samples per patient for testing. Defaults to None. - test_file (str | None, optional): Path to existing pickled test file. Defaults to None. - repeat (bool, optional): Restart generator when dataset is exhausted. Defaults to True. - num_workers (int, optional): # of parallel workers. Defaults to 1. - - Returns: - tf.data.Dataset: Test dataset - """ - - # Get test patients - test_patient_ids = ds.get_test_patient_ids() - test_patient_ids = ds.filter_patients_for_labels( - patient_ids=test_patient_ids, - label_map=label_map, - label_type=label_type, - ) - - if test_patients is not None: - num_pts = int(test_patients) if test_patients > 1 else int(test_patients * len(test_patient_ids)) - test_patient_ids = test_patient_ids[:num_pts] - - # Use existing validation data - if ds.cachable and test_file and os.path.isfile(test_file): - logger.debug(f"Loading test data from file {test_file}") - test = load_pkl(test_file) - test_ds = create_dataset_from_data(test["x"], test["y"], spec) - test_patient_ids = test["patient_ids"] - else: - test_ds = create_interleaved_dataset_from_generator( - data_generator=data_generator, - id_generator=id_generator, - ids=test_patient_ids, - spec=spec, - preprocess=preprocess, - num_workers=num_workers, + + # Use subset of validation patients + if isinstance(val_patients, Iterable): + val_patient_ids = val_patients + train_patient_ids = np.setdiff1d(train_patient_ids, val_patient_ids).tolist() + return train_patient_ids, val_patient_ids + + if val_patients is not None and val_patients >= 1: + val_patients = int(val_patients) + + train_patient_ids, val_patient_ids = self.ds.split_train_test_patients( + patient_ids=train_patient_ids, + test_size=val_patients, + label_map=self.label_map, + label_type=self.label_type, + ) + + return train_patient_ids, val_patient_ids + + def test_patient_ids( + self, + test_patients: float | None = None, + ) -> list[int]: + """Get test patient ids + + Args: + test_patients (float | None, optional): Test patients. Defaults to None. + + Returns: + list[int]: Test patient ids + """ + test_patient_ids = self.ds.get_test_patient_ids() + test_patient_ids = self.ds.filter_patients_for_labels( + patient_ids=test_patient_ids, + label_map=self.label_map, + label_type=self.label_type, ) - return test_ds + if test_patients is not None: + num_pts = int(test_patients) if test_patients > 1 else int(test_patients * len(test_patient_ids)) + test_patient_ids = test_patient_ids[:num_pts] + + return test_patient_ids + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, ...], None, None]: + """Generate data for given patient ids + + !!! note + This method should be implemented in the subclass + + Args: + patient_ids (list[int]): Patient IDs + samples_per_patient (int | list[int]): Samples per patient + shuffle (bool, optional): Shuffle data. Defaults to False. + """ + raise NotImplementedError() + + def create_dataloader( + self, patient_ids: list[int], samples_per_patient: int | list[int], shuffle: bool = False + ) -> tf.data.Dataset: + """Create tf.data.Dataset from internal data generator + + Args: + patient_ids (list[int]): Patient IDs + samples_per_patient (int | list[int]): Samples per patient + shuffle (bool, optional): Shuffle data. Defaults to False. + + Returns: + tf.data.Dataset: Dataset + """ + data_gen = functools.partial( + self.data_generator, + patient_ids=patient_ids, + samples_per_patient=samples_per_patient, + shuffle=shuffle, + ) + + # Compute output signature from generator + sig = nse.utils.get_output_signature_from_gen(data_gen) + + ds = tf.data.Dataset.from_generator( + data_gen, + output_signature=sig, + ) + return ds diff --git a/heartkit/datasets/dataset.py b/heartkit/datasets/dataset.py index 1dfd3faf..70bb3fcf 100644 --- a/heartkit/datasets/dataset.py +++ b/heartkit/datasets/dataset.py @@ -5,33 +5,95 @@ from pathlib import Path from typing import Generator -import h5py import numpy.typing as npt -import sklearn +import sklearn.model_selection -from .defines import PatientGenerator +from .defines import PatientGenerator, PatientData logger = logging.getLogger(__name__) class HKDataset(abc.ABC): - """HeartKit dataset base class""" + path: Path + _cacheable: bool + _cached_data: dict[str, npt.NDArray] - ds_path: Path + def __init__(self, path: os.PathLike, cacheable: bool = True) -> None: + """HKDataset serves as a base class to download and provide unified access to datasets. - def __init__(self, ds_path: os.PathLike) -> None: - """HeartKit dataset base class""" - self.ds_path = Path(ds_path) + Args: + path (os.PathLike): Path to dataset + cacheable (bool, optional): If dataset supports file caching. Defaults + + Example: + ```python + import numpy as np + import heartkit as hk + + class MyDataset(hk.HKDataset): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @property + def name(self) -> str: + return 'my-dataset' + + @property + def sampling_rate(self) -> int: + return 100 + + def get_train_patient_ids(self) -> npt.NDArray: + return np.arange(80) + + def get_test_patient_ids(self) -> npt.NDArray: + return np.arange(80, 100) + + @contextlib.contextmanager + def patient_data(self, patient_id: int) -> Generator[PatientData, None, None]: + data = np.random.randn(1000) + segs = np.random.randint(0, 1000, (10, 2)) + yield {"data": data, "segmentations": segs} + + def signal_generator( + self, + patient_generator: PatientGenerator, + frame_size: int, + samples_per_patient: int = 1, + target_rate: int | None = None, + ) -> Generator[npt.NDArray, None, None]: + for patient in patient_generator: + for _ in range(samples_per_patient): + with self.patient_data(patient) as pt: + yield pt["data"] + + def download(self, num_workers: int | None = None, force: bool = False): + pass + + # Register dataset + hk.DatasetFactory.register("my-dataset", MyDataset) + ``` + """ + self.path = Path(path) + self._cacheable = cacheable + self._cached_data = {} @property def name(self) -> str: """Dataset name""" - return self.ds_path.stem + return self.path.stem @property - def cachable(self) -> bool: - """If dataset supports file caching.""" - return True + def cacheable(self) -> bool: + """If dataset supports in-memory caching. + + On smaller datasets, it is recommended to cache the entire dataset in memory. + """ + return self._cacheable + + @cacheable.setter + def cacheable(self, value: bool): + """Set if in-memory caching is enabled""" + self._cacheable = value @property def sampling_rate(self) -> int: @@ -49,7 +111,7 @@ def std(self) -> float: return 1 def get_train_patient_ids(self) -> npt.NDArray: - """Get training patient IDs + """Get dataset's defined training patient IDs Returns: npt.NDArray: patient IDs @@ -57,7 +119,7 @@ def get_train_patient_ids(self) -> npt.NDArray: raise NotImplementedError() def get_test_patient_ids(self) -> npt.NDArray: - """Get patient IDs reserved for testing only + """Get dataset's patient IDs reserved for testing only Returns: npt.NDArray: patient IDs @@ -65,14 +127,14 @@ def get_test_patient_ids(self) -> npt.NDArray: raise NotImplementedError() @contextlib.contextmanager - def patient_data(self, patient_id: int) -> Generator[h5py.Group, None, None]: + def patient_data(self, patient_id: int) -> Generator[PatientData, None, None]: """Get patient data Args: patient_id (int): Patient ID Returns: - Generator[h5py.Group, None, None]: Patient data + Generator[PatientData, None, None]: Patient data """ raise NotImplementedError() @@ -86,12 +148,13 @@ def signal_generator( """Generate random frames. Args: - patient_generator (PatientGenerator): Generator that yields a tuple of patient id and patient data. - Patient data may contain only signals, since labels are not used. - samples_per_patient (int): Samples per patient. + patient_generator (PatientGenerator): Generator that yields patient data. + frame_size (int): Frame size + samples_per_patient (int, optional): Samples per patient. Defaults to 1. + target_rate (int | None, optional): Target rate. Defaults to None. Returns: - Generator[npt.NDArray, None, None]: Generator of input data of shape (frame_size, 1) + Generator[npt.NDArray, None, None]: Generator sample of data """ raise NotImplementedError() diff --git a/heartkit/datasets/defines.py b/heartkit/datasets/defines.py index ce4e5cce..28334f8c 100644 --- a/heartkit/datasets/defines.py +++ b/heartkit/datasets/defines.py @@ -1,7 +1,8 @@ -from typing import Callable, Generator +from typing import Generator, TypeAlias import numpy.typing as npt - -Preprocessor = Callable[[tuple[npt.NDArray, npt.NDArray]], tuple[npt.NDArray, npt.NDArray]] +import h5py PatientGenerator = Generator[int, None, None] + +PatientData: TypeAlias = dict[str, npt.NDArray] | h5py.Group diff --git a/heartkit/datasets/download.py b/heartkit/datasets/download.py index 41671256..3659ddd3 100644 --- a/heartkit/datasets/download.py +++ b/heartkit/datasets/download.py @@ -1,19 +1,37 @@ import logging import os +import neuralspot_edge as nse from ..defines import HKDownloadParams -from ..utils import setup_logger -from . import DatasetFactory +from . import HKDataset +from .factory import DatasetFactory -logger = setup_logger(__name__) + +logger = nse.utils.setup_logger(__name__) def download_datasets(params: HKDownloadParams): """Download specified datasets. Args: - params (HeartDownloadParams): Download parameters - + params (HKDownloadParams): Download parameters + + Example: + ```python + import heartkit as hk + + # Download datasets + params = hk.HKDownloadParams( + datasets=[ + hk.NamedParams(name="ptbxl", params={ + "path": "./datasets/ptbxl", + }), + ], + data_parallelism=4, + force=False, + ) + hk.datasets.download_datasets(params) + ``` """ os.makedirs(params.job_dir, exist_ok=True) logger.debug(f"Creating working directory in {params.job_dir}") @@ -24,9 +42,8 @@ def download_datasets(params: HKDownloadParams): for ds in params.datasets: if DatasetFactory.has(ds.name): - os.makedirs(ds.path, exist_ok=True) Dataset = DatasetFactory.get(ds.name) - ds = Dataset(ds_path=ds.path, **ds.params) + ds: HKDataset = Dataset(**ds.params) ds.download( num_workers=params.data_parallelism, force=params.force, diff --git a/heartkit/datasets/synthetic.py b/heartkit/datasets/ecg_synthetic.py similarity index 67% rename from heartkit/datasets/synthetic.py rename to heartkit/datasets/ecg_synthetic.py index 143acfdb..defc3db3 100644 --- a/heartkit/datasets/synthetic.py +++ b/heartkit/datasets/ecg_synthetic.py @@ -1,9 +1,7 @@ import contextlib -import io -import logging -import os import random -import uuid +import tempfile +from pathlib import Path from typing import Generator import h5py @@ -11,16 +9,19 @@ import numpy.typing as npt import physiokit as pk from pydantic import BaseModel, Field +import neuralspot_edge as nse +from tqdm.contrib.concurrent import process_map + from .dataset import HKDataset -from .defines import PatientGenerator +from .defines import PatientGenerator, PatientData from .nstdb import NstdbNoise -logger = logging.getLogger(__name__) +logger = nse.utils.setup_logger(__name__) -class SyntheticParams(BaseModel, extra="allow"): - """Synthetic parameters""" +class EcgSyntheticParams(BaseModel, extra="allow"): + """ECG Synthetic ECG generator parameters""" presets: list[pk.ecg.EcgPreset] = Field( default_factory=lambda: [ @@ -48,36 +49,53 @@ class SyntheticParams(BaseModel, extra="allow"): voltage_factor: tuple[float, float] = Field((800, 1000), description="Voltage factor range") -class SyntheticDataset(HKDataset): - """Synthetic dataset""" - +class EcgSyntheticDataset(HKDataset): def __init__( self, - ds_path: os.PathLike, num_pts: int = 250, leads: list[int] | None = None, params: dict | None = None, + path: str = Path(tempfile.gettempdir()) / "ecg-synthetic", + **kwargs, ) -> None: - super().__init__( - ds_path=ds_path, + """ECG synthetic dataset creates 12-lead ECG signals using PhysioKit. + + Args: + num_pts (int, optional): Number of patients. Defaults to 250. + leads (list[int] | None, optional): Leads to use. Defaults to None. + params (dict | None, optional): ECG synthetic parameters for EcgSyntheticParams. Defaults to None. + path (str, optional): Path to store dataset. Defaults to Path(tempfile.gettempdir()) / "ecg-synthetic". + + Example: + ```python + import heartkit as hk + + ds = hk.datasets.EcgSyntheticDataset( + num_pts=100, + params=dict( + sample_rate=1000, # Hz + duration=10, # seconds + heart_rate=(40, 120), + ) ) + + with ds.patient_data(patient_id=ds.patient_ids[0]) as pt: + ecg = pt["data"][:] + segs = pt["segmentations"][:] + fids = pt["fiducials"][:] + # END WITH + ``` + """ + super().__init__(path=path, **kwargs) self._noise_gen = None self._num_pts = num_pts self.leads = leads or list(range(12)) - self.params = SyntheticParams(**params or {}) - self._unique_id = str(uuid.uuid4()) - self._cache: dict[str, io.BytesIO] = {} - os.makedirs(self.ds_path, exist_ok=True) + self.params = EcgSyntheticParams(**params or {}) @property def name(self) -> str: """Dataset name""" - return "synthetic" - - @property - def cachable(self) -> bool: - """If dataset supports file caching.""" - return True + return "ecg-synthetic" @property def sampling_rate(self) -> int: @@ -125,37 +143,48 @@ def pt_key(self, patient_id: int): """Get patient key""" return f"{patient_id:05d}" + def load_patient_data(self, patient_id: int): + ecg, segs, fids = self._synthesize_signal( + frame_size=int(self.params.duration * self.sampling_rate), target_rate=self.sampling_rate + ) + pt_data = { + "data": ecg, + "segmentations": segs, + "fiducials": fids, + } + return pt_data + + def build_cache(self): + """Build in-memory cache to speed up data access""" + logger.info(f"Creating synthetic dataset cache with {self._num_pts} patients") + pts_data = process_map(self.load_patient_data, self.patient_ids) + self._cached_data = {self.pt_key(i): pt_data for i, pt_data in enumerate(pts_data)} + @contextlib.contextmanager - def patient_data(self, patient_id: int) -> Generator[h5py.Group, None, None]: - """Get patient data + def patient_data(self, patient_id: int) -> Generator[PatientData, None, None]: + """Get access to patient data + + Patient data contains following fields: + - data: ECG signal of shape (12, N) + - segmentations: Segmentation of ECG signal + - fiducials: Fiducials of ECG signal Args: patient_id (int): Patient ID Returns: - Generator[h5py.Group, None, None]: Patient data + Generator[PatientData, None, None]: Patient data """ - pt_key = self.pt_key(patient_id) - if pt_key not in self._cache: - ecg, segs, fids = self._synthesize_signal( - frame_size=int(self.params.duration * self.sampling_rate), target_rate=self.sampling_rate - ) - fp = io.BytesIO() - with h5py.File(fp, mode="w") as h5: - h5.create_dataset("data", data=ecg) - h5.create_dataset("segmentations", data=segs) - h5.create_dataset("fiducials", data=fids) - h5.attrs["unique_id"] = self._unique_id - # END WITH - fp.seek(0) - self._cache[pt_key] = fp + if self.cacheable: + if pt_key not in self._cached_data: + self.build_cache() + yield self._cached_data[pt_key] + else: + pt_data = self.load_patient_data(patient_id) + yield pt_data # END IF - with h5py.File(self._cache[pt_key], mode="r") as h5: - yield h5 - # END WITH - def signal_generator( self, patient_generator: PatientGenerator, @@ -166,8 +195,10 @@ def signal_generator( """Generate frames using patient generator. Args: - patient_generator (PatientGenerator): Generator that yields a tuple of patient id and patient data. - samples_per_patient (int): Samples per patient. + patient_generator (PatientGenerator): Generator that yields patient data. + frame_size (int): Frame size + samples_per_patient (int, optional): Samples per patient. Defaults to 1. + target_rate (int | None, optional): Target rate. Defaults to None. Returns: SampleGenerator: Generator of input data of shape (frame_size, 1) @@ -175,7 +206,7 @@ def signal_generator( if target_rate is None: target_rate = self.sampling_rate - input_size = int(np.round((self.sampling_rate / target_rate) * frame_size)) + input_size = int(np.ceil((self.sampling_rate / target_rate) * frame_size)) for pt in patient_generator: with self.patient_data(pt) as h5: @@ -189,6 +220,7 @@ def signal_generator( x = self.add_noise(x) if self.sampling_rate != target_rate: x = pk.signal.resample_signal(x, self.sampling_rate, target_rate, axis=0) + x = x[:frame_size] # END IF yield x # END FOR @@ -228,7 +260,7 @@ def _synthesize_signal( frame_size: int, target_rate: float | None = None, ) -> tuple[npt.NDArray, npt.NDArray, npt.NDArray]: - """Generate synthetic signal of given length + """Private method to generate synthetic signal of given length Args: frame_size (int): Frame size diff --git a/heartkit/datasets/factory.py b/heartkit/datasets/factory.py new file mode 100644 index 00000000..146d116c --- /dev/null +++ b/heartkit/datasets/factory.py @@ -0,0 +1,10 @@ +"""DatasetFactory is used to store and retrieve datasets that inherit from HKDataset. +key (str): Dataset name slug (e.g. "ptbxl") +value (HKDataset): Dataset class +""" + +import neuralspot_edge as nse + +from .dataset import HKDataset + +DatasetFactory = nse.utils.create_factory(factory="HKDatasetFactory", type=HKDataset) diff --git a/heartkit/datasets/icentia11k.py b/heartkit/datasets/icentia11k.py index 4f29a0ba..2567bc4e 100644 --- a/heartkit/datasets/icentia11k.py +++ b/heartkit/datasets/icentia11k.py @@ -1,12 +1,10 @@ import contextlib import functools -import logging import os import random import tempfile import zipfile from enum import IntEnum -from multiprocessing import Pool from typing import Generator import h5py @@ -15,14 +13,13 @@ import physiokit as pk import sklearn.model_selection import sklearn.preprocessing -from tqdm import tqdm +from tqdm.contrib.concurrent import process_map +import neuralspot_edge as nse -from ..utils import download_file from .dataset import HKDataset from .defines import PatientGenerator -from .utils import download_s3_objects -logger = logging.getLogger(__name__) +logger = nse.utils.setup_logger(__name__) class IcentiaRhythm(IntEnum): @@ -51,14 +48,17 @@ class IcentiaBeat(IntEnum): class IcentiaDataset(HKDataset): - """Icentia dataset""" - def __init__( self, - ds_path: os.PathLike, leads: list[int] | None = None, + **kwargs, ) -> None: - super().__init__(ds_path=ds_path) + """Icentia11kDataset consists of ECG recordings from 11,000 patients and 2 billion labelled beats. + + Args: + leads (list[int] | None, optional): List of leads to include. Defaults to None. + """ + super().__init__(**kwargs) self.leads = leads or list(IcentiaLeadsMap.values()) @property @@ -107,10 +107,11 @@ def get_test_patient_ids(self) -> npt.NDArray: return self.patient_ids[10_000:] def _pt_key(self, patient_id: int): + """Get patient key for HDF5 file""" return f"p{patient_id:05d}" def label_key(self, label_type: str = "rhythm") -> str: - """Get label key + """Get local label key for HDF5 file Args: label_type (str, optional): Label type. Defaults to "rhythm". @@ -128,13 +129,19 @@ def label_key(self, label_type: str = "rhythm") -> str: def patient_data(self, patient_id: int) -> Generator[h5py.Group, None, None]: """Get patient data + Patient data is stored in HDF5 format with the following structure: + - {segment_id}/data: ECG data (1 x N) + - {segment_id}/rlabels: Rhythm labels (N x 2) + - {segment_id}/blabels: Beat labels (N x 2) + segment_id is sequential number for each segment in the patient data. + Args: patient_id (int): Patient ID Returns: Generator[h5py.Group, None, None]: Patient data """ - with h5py.File(self.ds_path / f"{self._pt_key(patient_id)}.h5", mode="r") as h5: + with h5py.File(self.path / f"{self._pt_key(patient_id)}.h5", mode="r") as h5: yield h5[self._pt_key(patient_id)] def signal_generator( @@ -147,7 +154,7 @@ def signal_generator( """Generate random frames. Args: - patient_generator (PatientGenerator): Patient generator + patient_generator (PatientGenerator): Generator that yields patient data. frame_size (int): Frame size samples_per_patient (int, optional): Samples per patient. Defaults to 1. target_rate (int | None, optional): Target rate. Defaults to None. @@ -158,7 +165,7 @@ def signal_generator( if target_rate is None: target_rate = self.sampling_rate - input_size = int(np.round((self.sampling_rate / target_rate) * frame_size)) + input_size = int(np.ceil((self.sampling_rate / target_rate) * frame_size)) for pt in patient_generator: with self.patient_data(pt) as segments: for _ in range(samples_per_patient): @@ -170,6 +177,7 @@ def signal_generator( x = np.nan_to_num(x).astype(np.float32) if self.sampling_rate != target_rate: x = pk.signal.resample_signal(x, self.sampling_rate, target_rate, axis=0) + x = x[:frame_size] # END IF yield x # END FOR @@ -185,10 +193,10 @@ def download(self, num_workers: int | None = None, force: bool = False): num_workers (int | None, optional): # parallel workers. Defaults to None. force (bool, optional): Force redownload. Defaults to False. """ - download_s3_objects( + nse.utils.download_s3_objects( bucket="ambiq-ai-datasets", - prefix=self.ds_path.stem, - dst=self.ds_path.parent, + prefix=self.path.stem, + dst=self.path.parent, checksum="size", progress=True, num_workers=num_workers, @@ -284,8 +292,7 @@ def get_patients_labels( """ ids = patient_ids.tolist() func = functools.partial(self.get_patient_labels, label_map=label_map, label_type=label_type) - with Pool() as pool: - pts_labels = list(pool.imap(func, ids)) + pts_labels = process_map(func, ids) return pts_labels def get_patient_labels(self, patient_id: int, label_map: dict[int, int], label_type: str = "rhythm") -> list[int]: @@ -328,14 +335,14 @@ def download_raw_dataset(self, num_workers: int | None = None, force: bool = Fal "https://physionet.org/static/published-projects/icentia11k-continuous-ecg/" "icentia11k-single-lead-continuous-raw-electrocardiogram-dataset-1.0.zip" ) - ds_zip_path = self.ds_path / "icentia11k.zip" - os.makedirs(self.ds_path, exist_ok=True) + ds_zip_path = self.path / "icentia11k.zip" + os.makedirs(self.path, exist_ok=True) if os.path.exists(ds_zip_path) and not force: logger.warning( f"Zip file already exists. Please delete or set `force` flag to redownload. PATH={ds_zip_path}" ) else: - download_file(ds_url, ds_zip_path, progress=True) + nse.utils.download_file(ds_url, ds_zip_path, progress=True) # 2. Extract and convert patient ECG data to H5 files logger.debug("Generating icentia11k patient data") @@ -376,7 +383,7 @@ def _convert_dataset_pt_zip_to_hdf5(self, patient: int, zip_path: os.PathLike, f logger.debug(f"Processing patient {patient}") pt_id = self._pt_key(patient) - pt_path = self.ds_path / f"{pt_id}.h5" + pt_path = self.path / f"{pt_id}.h5" if not force and os.path.exists(pt_path): logger.debug(f"Skipping patient {pt_id}") return @@ -450,5 +457,4 @@ def _convert_dataset_zip_to_hdf5( if not patient_ids: patient_ids = self.patient_ids f = functools.partial(self._convert_dataset_pt_zip_to_hdf5, zip_path=zip_path, force=force) - with Pool(processes=num_workers) as pool: - _ = list(tqdm(pool.imap(f, patient_ids), total=len(patient_ids))) + _ = process_map(f, patient_ids) diff --git a/heartkit/datasets/icentia_mini.py b/heartkit/datasets/icentia_mini.py new file mode 100644 index 00000000..63019827 --- /dev/null +++ b/heartkit/datasets/icentia_mini.py @@ -0,0 +1,325 @@ +import contextlib +import functools +import os +import random +import zipfile +from enum import IntEnum +from typing import Generator + +import h5py +import numpy as np +import numpy.typing as npt +import physiokit as pk +import sklearn.model_selection +import sklearn.preprocessing +from tqdm.contrib.concurrent import process_map + +import neuralspot_edge as nse + +from .dataset import HKDataset +from .defines import PatientGenerator, PatientData + +logger = nse.utils.setup_logger(__name__) + + +class IcentiaMiniRhythm(IntEnum): + """Icentia rhythm labels""" + + normal = 1 + afib = 2 + aflut = 3 + end = 4 + + +class IcentiaMiniBeat(IntEnum): + """Incentia mini beat labels""" + + normal = 1 + pac = 2 + aberrated = 3 + pvc = 4 + + +IcentiaMiniLeadsMap = { + "i": 0, # Modified lead I +} + + +class IcentiaMiniDataset(HKDataset): + """Icentia-mini dataset""" + + def __init__( + self, + leads: list[int] | None = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.leads = leads or list(IcentiaMiniLeadsMap.values()) + + @property + def name(self) -> str: + """Dataset name""" + return "icentia_mini" + + @property + def sampling_rate(self) -> int: + """Sampling rate in Hz""" + return 250 + + @property + def mean(self) -> float: + """Dataset mean""" + return 0.0018 + + @property + def std(self) -> float: + """Dataset st dev""" + return 1.3711 + + @property + def patient_ids(self) -> npt.NDArray: + """Get dataset patient IDs + + Returns: + npt.NDArray: patient IDs + """ + return np.arange(11_000) + + def get_train_patient_ids(self) -> npt.NDArray: + """Get training patient IDs + + Returns: + npt.NDArray: patient IDs + """ + return self.patient_ids[:10_000] + + def get_test_patient_ids(self) -> npt.NDArray: + """Get patient IDs reserved for testing only + + Returns: + npt.NDArray: patient IDs + """ + return self.patient_ids[10_000:] + + def _pt_key(self, patient_id: int): + return f"p{patient_id:05d}" + + def label_key(self, label_type: str = "rhythm") -> str: + """Get label key + + Args: + label_type (str, optional): Label type. Defaults to "rhythm". + + Returns: + str: Label key + """ + if label_type == "rhythm": + return "rlabels" + if label_type == "beat": + return "blabels" + raise ValueError(f"Invalid label type: {label_type}") + + @contextlib.contextmanager + def patient_data(self, patient_id: int) -> Generator[PatientData, None, None]: + """Get patient data + + Args: + patient_id (int): Patient ID + + Returns: + Generator[h5py.Group, None, None]: Patient data + """ + h5_path = self.path / "icentia_mini.h5" + pt_key = self._pt_key(patient_id) + if self.cacheable: + if patient_id not in self._cached_data: + pt_data = {} + with h5py.File(h5_path, mode="r") as h5: + pt = h5[pt_key] + pt_data["data"] = pt["data"][:] + pt_data["rlabels"] = pt["rlabels"][:] + pt_data["blabels"] = pt["blabels"][:] + self._cached_data[patient_id] = pt_data + # END IF + yield self._cached_data[patient_id] + else: + with h5py.File(h5_path, mode="r") as h5: + pt = h5[pt_key] + yield h5 + # END WITH + # END IF + + def signal_generator( + self, + patient_generator: PatientGenerator, + frame_size: int, + samples_per_patient: int = 1, + target_rate: int | None = None, + ) -> Generator[npt.NDArray, None, None]: + """Generate random frames. + + Args: + patient_generator (PatientGenerator): Generator that yields patient data. + frame_size (int): Frame size + samples_per_patient (int, optional): Samples per patient. Defaults to 1. + target_rate (int | None, optional): Target rate. Defaults to None. + + Returns: + SampleGenerator: Generator of input data of shape (frame_size, 1) + """ + if target_rate is None: + target_rate = self.sampling_rate + + input_size = int(np.ceil((self.sampling_rate / target_rate) * frame_size)) + for pt in patient_generator: + with self.patient_data(pt) as segments: + for _ in range(samples_per_patient): + segment = segments[np.random.choice(list(segments.keys()))] + segment_size = segment["data"].shape[0] + frame_start = np.random.randint(segment_size - input_size) + frame_end = frame_start + input_size + x = segment["data"][frame_start:frame_end].squeeze() + x = np.nan_to_num(x).astype(np.float32) + if self.sampling_rate != target_rate: + x = pk.signal.resample_signal(x, self.sampling_rate, target_rate, axis=0) + x = x[:frame_size] + # END IF + yield x + # END FOR + # END WITH + # END FOR + + def download(self, num_workers: int | None = None, force: bool = False): + """Download dataset + + This will download preprocessed HDF5 files from S3. + + Args: + num_workers (int | None, optional): # parallel workers. Defaults to None. + force (bool, optional): Force redownload. Defaults to False. + """ + os.makedirs(self.path, exist_ok=True) + zip_path = self.path / f"{self.name}.zip" + + did_download = nse.utils.download_s3_file( + key=f"{self.name}/{self.name}.zip", + dst=zip_path, + bucket="ambiq-ai-datasets", + checksum="size", + ) + if did_download: + with zipfile.ZipFile(zip_path, "r") as zf: + zf.extractall(self.path) + + def split_train_test_patients( + self, + patient_ids: npt.NDArray, + test_size: float, + label_map: dict[int, int] | None = None, + label_type: str | None = None, + ) -> list[list[int]]: + """Perform train/test split on patients for given task. + + Args: + patient_ids (npt.NDArray): Patient Ids + test_size (float): Test size + label_map (dict[int, int], optional): Label map. Defaults to None. + label_type (str, optional): Label type. Defaults to None. + + Returns: + list[list[int]]: Train and test sets of patient ids + """ + stratify = None + + if label_map is not None and label_type is not None: + # Use stratified split for rhythm task + patients_labels = self.get_patients_labels(patient_ids, label_map=label_map, label_type=label_type) + # Select random label for stratification or -1 if no labels + stratify = np.array([random.choice(x) if len(x) > 0 else -1 for x in patients_labels]) + # Remove patients w/o labels + neg_mask = stratify == -1 + stratify = stratify[~neg_mask] + patient_ids = patient_ids[~neg_mask] + num_neg = neg_mask.sum() + if num_neg > 0: + logger.debug(f"Removed {num_neg} patients w/ no target class") + # END IF + # END IF + + return sklearn.model_selection.train_test_split( + patient_ids, + test_size=test_size, + shuffle=True, + stratify=stratify, + ) + + def filter_patients_for_labels( + self, + patient_ids: npt.NDArray, + label_map: dict[int, int] | None = None, + label_type: str | None = None, + ) -> npt.NDArray: + """Filter patients based on labels. + Useful to remove patients w/o labels for task to speed up data loading. + + Args: + patient_ids (npt.NDArray): Patient ids + label_map (dict[int, int], optional): Label map. Defaults to None. + label_type (str, optional): Label type. Defaults to None. + + Returns: + npt.NDArray: Filtered patient ids + """ + if label_map is None or label_type is None: + return patient_ids + + patients_labels = self.get_patients_labels(patient_ids, label_map, label_type) + # Find any patient with empty list + label_mask = np.array([len(x) > 0 for x in patients_labels]) + neg_mask = label_mask == -1 + num_neg = neg_mask.sum() + if num_neg > 0: + logger.debug(f"Removed {num_neg} of {patient_ids.size} patients w/ no target class") + return patient_ids[~neg_mask] + + def get_patients_labels( + self, + patient_ids: npt.NDArray, + label_map: dict[int, int], + label_type: str = "rhythm", + ) -> list[list[int]]: + """Get class labels for each patient + + Args: + patient_ids (npt.NDArray): Patient ids + label_map (dict[int, int]): Label map + label_type (str, optional): Label type. Defaults to "rhythm". + + Returns: + list[list[int]]: List of class labels per patient + + """ + ids = patient_ids.tolist() + func = functools.partial(self.get_patient_labels, label_map=label_map, label_type=label_type) + pts_labels = process_map(func, ids) + return pts_labels + + def get_patient_labels(self, patient_id: int, label_map: dict[int, int], label_type: str = "rhythm") -> list[int]: + """Get class labels for patient + + Args: + patient_id (int): Patient id + label_map (dict[int, int]): Label map + label_type (str, optional): Label type. Defaults to "rhythm". + + Returns: + list[int]: List of class labels + + """ + label_key = self.label_key(label_type) + with self.patient_data(patient_id) as pt: + mask = pt[label_key][:] + labels = np.unique(mask) + labels: list[int] = [label_map[lbl] for lbl in labels if label_map.get(lbl, -1) != -1] + # END WITH + return list(labels) diff --git a/heartkit/datasets/lsad.py b/heartkit/datasets/lsad.py index 7b3cbf0f..72fc7050 100644 --- a/heartkit/datasets/lsad.py +++ b/heartkit/datasets/lsad.py @@ -1,12 +1,10 @@ import contextlib import functools -import logging import os import zipfile import random from collections.abc import Iterable from enum import IntEnum -from multiprocessing import Pool from typing import Generator import h5py @@ -15,13 +13,13 @@ import physiokit as pk import sklearn.model_selection from tqdm import tqdm +from tqdm.contrib.concurrent import process_map +import neuralspot_edge as nse -from ..utils import download_file from .dataset import HKDataset -from .defines import PatientGenerator -from .utils import download_s3_file +from .defines import PatientGenerator, PatientData -logger = logging.getLogger(__name__) +logger = nse.utils.setup_logger(__name__) class LsadScpCode(IntEnum): @@ -174,12 +172,10 @@ class LsadDataset(HKDataset): def __init__( self, - ds_path: os.PathLike, leads: list[int] | None = None, + **kwargs, ) -> None: - super().__init__( - ds_path=ds_path, - ) + super().__init__(**kwargs) self.leads = leads or list(LsadLeadsMap.values()) @property @@ -209,7 +205,7 @@ def patient_ids(self) -> npt.NDArray: Returns: npt.NDArray: patient IDs """ - pts = np.array([int(p.stem) for p in self.ds_path.glob("*.h5")]) + pts = np.array([int(p.stem) for p in self.path.glob("*.h5")]) pts.sort() return pts @@ -249,17 +245,31 @@ def label_key(self, label_type: str = "scp") -> str: raise ValueError(f"Invalid label type: {label_type}") @contextlib.contextmanager - def patient_data(self, patient_id: int) -> Generator[h5py.Group, None, None]: + def patient_data(self, patient_id: int) -> Generator[PatientData, None, None]: """Get patient data Args: patient_id (int): Patient ID Returns: - Generator[h5py.Group, None, None]: Patient data + Generator[PatientData, None, None]: Patient data """ - with h5py.File(self.ds_path / f"{self._pt_key(patient_id)}.h5", mode="r") as h5: - yield h5 + pt_key = self._pt_key(patient_id) + pt_path = self.path / f"{pt_key}.h5" + if self.cacheable: + if pt_key not in self._cached_data: + pt_data = {} + with h5py.File(pt_path, mode="r") as h5: + pt_data["data"] = h5["data"][:] + pt_data[self.label_key("scp")] = h5[self.label_key("scp")][:] + self._cached_data[pt_key] = pt_data + # END IF + yield self._cached_data[pt_key] + else: + with h5py.File(pt_path, mode="r") as h5: + yield h5 + # END WITH + # END IF def signal_generator( self, @@ -271,10 +281,10 @@ def signal_generator( """Generate random frames. Args: - patient_generator (PatientGenerator): Patient Generator + patient_generator (PatientGenerator): Generator that yields patient data. frame_size (int): Frame size samples_per_patient (int, optional): Samples per patient. Defaults to 1. - target_rate (int, optional): Target rate. Defaults to None. + target_rate (int | None, optional): Target rate. Defaults to None. Returns: Generator[npt.NDArray, None, None]: Generator of input data @@ -282,10 +292,10 @@ def signal_generator( if target_rate is None: target_rate = self.sampling_rate - input_size = int(np.round((self.sampling_rate / target_rate) * frame_size)) + input_size = int(np.ceil((self.sampling_rate / target_rate) * frame_size)) for pt in patient_generator: with self.patient_data(pt) as h5: - data: h5py.Dataset = h5["data"][:] + data = h5["data"][:] # END WITH for _ in range(samples_per_patient): lead = random.choice(self.leads) @@ -294,6 +304,7 @@ def signal_generator( x = np.nan_to_num(x).astype(np.float32) if self.sampling_rate != target_rate: x = pk.signal.resample_signal(x, self.sampling_rate, target_rate, axis=0) + x = x[:frame_size] # END IF yield x # END FOR @@ -342,7 +353,7 @@ def signal_label_generator( num_per_tgt = int(max(1, samples_per_patient / num_classes)) samples_per_tgt = num_classes * [num_per_tgt] - input_size = int(np.round((self.sampling_rate / target_rate) * frame_size)) + input_size = int(np.ceil((self.sampling_rate / target_rate) * frame_size)) for pt in patient_generator: # 1. Grab patient scp label (fixed for all samples) @@ -395,6 +406,8 @@ def signal_label_generator( # Resample if needed if self.sampling_rate != target_rate: x = pk.signal.resample_signal(x, self.sampling_rate, target_rate, axis=0) + x = x[:frame_size] # truncate to frame size + x = np.reshape(x, (frame_size, 1)) yield x, y # END FOR # END FOR @@ -480,6 +493,8 @@ def get_patients_labels( Args: patient_ids (npt.NDArray): Patient ids + label_map (dict[int, int]): Label map + label_type (str, optional): Label type. Defaults to "scp". Returns: list[list[int]]: List of class labels per patient @@ -487,8 +502,7 @@ def get_patients_labels( """ ids = patient_ids.tolist() func = functools.partial(self.get_patient_labels, label_map=label_map, label_type=label_type) - with Pool() as pool: - pts_labels = list(pool.imap(func, ids)) + pts_labels = process_map(func, ids) return pts_labels def get_patient_labels(self, patient_id: int, label_map: dict[int, int], label_type: str = "scp") -> list[int]: @@ -496,6 +510,8 @@ def get_patient_labels(self, patient_id: int, label_map: dict[int, int], label_t Args: patient_id (int): Patient id + label_map (dict[int, int]): Label map + label_type (str, optional): Label type. Defaults to "scp". Returns: list[int]: List of class labels @@ -516,10 +532,10 @@ def download(self, num_workers: int | None = None, force: bool = False): num_workers (int | None, optional): # parallel workers. Defaults to None. force (bool, optional): Force redownload. Defaults to False. """ - os.makedirs(self.ds_path, exist_ok=True) - zip_path = self.ds_path / f"{self.name}.zip" + os.makedirs(self.path, exist_ok=True) + zip_path = self.path / f"{self.name}.zip" - did_download = download_s3_file( + did_download = nse.utils.download_s3_file( key=f"{self.name}/{self.name}.zip", dst=zip_path, bucket="ambiq-ai-datasets", @@ -527,7 +543,7 @@ def download(self, num_workers: int | None = None, force: bool = False): ) if did_download: with zipfile.ZipFile(zip_path, "r") as zf: - zf.extractall(self.ds_path) + zf.extractall(self.path) def download_raw_dataset(self, num_workers: int | None = None, force: bool = False): """Downloads full dataset zipfile and converts into individial patient HDF5 files. @@ -541,14 +557,14 @@ def download_raw_dataset(self, num_workers: int | None = None, force: bool = Fal "https://www.physionet.org/static/published-projects/ecg-arrhythmia/" "a-large-scale-12-lead-electrocardiogram-database-for-arrhythmia-study-1.0.0.zip" ) - ds_zip_path = self.ds_path / "lsad.zip" - os.makedirs(self.ds_path, exist_ok=True) + ds_zip_path = self.path / "lsad.zip" + os.makedirs(self.path, exist_ok=True) if os.path.exists(ds_zip_path) and not force: logger.warning( f"Zip file already exists. Please delete or set `force` flag to redownload. PATH={ds_zip_path}" ) else: - download_file(ds_url, ds_zip_path, progress=True) + nse.utils.download_file(ds_url, ds_zip_path, progress=True) # 2. Extract and convert patient ECG data to H5 files logger.debug("Processing LSAD patient data") @@ -600,7 +616,7 @@ def _convert_dataset_zip_to_hdf5( try: # Extract patient ID by remove JS prefix and .mat suffix pt_id = os.path.basename(zp_rec_name).removeprefix("JS").removesuffix(".mat") - pt_path = self.ds_path / f"{pt_id}.h5" + pt_path = self.path / f"{pt_id}.h5" with tempfile.TemporaryDirectory() as tmpdir: rec_fpath = os.path.join(tmpdir, f"JS{pt_id}") diff --git a/heartkit/datasets/ludb.py b/heartkit/datasets/ludb.py index fe2cab14..0ca5c232 100644 --- a/heartkit/datasets/ludb.py +++ b/heartkit/datasets/ludb.py @@ -1,12 +1,10 @@ import contextlib import functools -import logging import os import random import tempfile import zipfile from enum import IntEnum -from multiprocessing import Pool from pathlib import Path from typing import Generator @@ -14,14 +12,13 @@ import numpy as np import numpy.typing as npt import physiokit as pk -from tqdm import tqdm +from tqdm.contrib.concurrent import process_map +import neuralspot_edge as nse -from ..utils import download_file from .dataset import HKDataset -from .defines import PatientGenerator -from .utils import download_s3_file +from .defines import PatientGenerator, PatientData -logger = logging.getLogger(__name__) +logger = nse.utils.setup_logger(__name__) LudbSymbolMap = { "o": 0, # Other @@ -69,12 +66,10 @@ class LudbDataset(HKDataset): def __init__( self, - ds_path: os.PathLike, leads: list[int] | None = None, + **kwargs, ) -> None: - super().__init__( - ds_path=ds_path, - ) + super().__init__(**kwargs) self.leads = leads or list(LudbLeadsMap.values()) @property @@ -127,17 +122,33 @@ def _pt_key(self, patient_id: int): return f"p{patient_id:05d}" @contextlib.contextmanager - def patient_data(self, patient_id: int) -> Generator[h5py.Group, None, None]: + def patient_data(self, patient_id: int) -> Generator[PatientData, None, None]: """Get patient data Args: patient_id (int): Patient ID Returns: - Generator[h5py.Group, None, None]: Patient data + Generator[PatientData, None, None]: Patient data """ - with h5py.File(self.ds_path / f"{self._pt_key(patient_id)}.h5", mode="r") as h5: - yield h5 + pt_key = self._pt_key(patient_id) + pt_path = self.path / f"{pt_key}.h5" + if self.cacheable: + if pt_key not in self._cached_data: + pt_data = {} + with h5py.File(pt_path, mode="r") as h5: + pt_data["data"] = h5["data"][:] + pt_data["segmentations"] = h5["segmentations"][:] + pt_data["fiducials"] = h5["fiducials"][:] + # END WITH + self._cached_data[pt_key] = pt_data + # END IF + yield self._cached_data[pt_key] + else: + with h5py.File(pt_path, mode="r") as h5: + yield h5 + # END WITH + # END IF def signal_generator( self, @@ -149,10 +160,10 @@ def signal_generator( """Generate random frames. Args: - patient_generator (PatientGenerator): Patient generator + patient_generator (PatientGenerator): Generator that yields patient data. frame_size (int): Frame size - samples_per_patient (int, optional): # samples per patient. Defaults to 1. - target_rate (int | None, optional): Target sampling rate. Defaults to None. + samples_per_patient (int, optional): Samples per patient. Defaults to 1. + target_rate (int | None, optional): Target rate. Defaults to None. Returns: Generator[npt.NDArray, None, None]: Generator of input data of shape (frame_size, 1) @@ -161,7 +172,7 @@ def signal_generator( if target_rate is None: target_rate = self.sampling_rate - input_size = int(np.round((self.sampling_rate / target_rate) * frame_size)) + input_size = int(np.ceil((self.sampling_rate / target_rate) * frame_size)) for pt in patient_generator: with self.patient_data(pt) as h5: @@ -174,6 +185,7 @@ def signal_generator( x = np.nan_to_num(x).astype(np.float32) if self.sampling_rate != target_rate: x = pk.signal.resample_signal(x, self.sampling_rate, target_rate, axis=0) + x = x[:frame_size] # END IF yield x # END FOR @@ -205,10 +217,10 @@ def download(self, num_workers: int | None = None, force: bool = False): num_workers (int | None, optional): # parallel workers. Defaults to None. force (bool, optional): Force redownload. Defaults to False. """ - os.makedirs(self.ds_path, exist_ok=True) - zip_path = self.ds_path / f"{self.name}.zip" + os.makedirs(self.path, exist_ok=True) + zip_path = self.path / f"{self.name}.zip" - did_download = download_s3_file( + did_download = nse.utils.download_s3_file( key=f"{self.name}/{self.name}.zip", dst=zip_path, bucket="ambiq-ai-datasets", @@ -216,7 +228,7 @@ def download(self, num_workers: int | None = None, force: bool = False): ) if did_download: with zipfile.ZipFile(zip_path, "r") as zf: - zf.extractall(self.ds_path) + zf.extractall(self.path) def download_raw_dataset(self, num_workers: int | None = None, force: bool = False): """Downloads full dataset zipfile and converts into individial patient HDF5 files. @@ -230,14 +242,14 @@ def download_raw_dataset(self, num_workers: int | None = None, force: bool = Fal "https://physionet.org/static/published-projects/ludb/" "lobachevsky-university-electrocardiography-database-1.0.1.zip" ) - ds_zip_path = self.ds_path / "ludb.zip" - os.makedirs(self.ds_path, exist_ok=True) + ds_zip_path = self.path / "ludb.zip" + os.makedirs(self.path, exist_ok=True) if os.path.exists(ds_zip_path) and not force: logger.warning( f"Zip file already exists. Please delete or set `force` flag to redownload. PATH={ds_zip_path}" ) else: - download_file(ds_url, ds_zip_path, progress=True) + nse.utils.download_file(ds_url, ds_zip_path, progress=True) # 2. Extract and convert patient ECG data to H5 files logger.debug("Generating LUDB patient data") @@ -263,18 +275,16 @@ def convert_dataset_zip_to_hdf5( patient_ids = self.patient_ids subdir = "lobachevsky-university-electrocardiography-database-1.0.1" - with Pool(processes=num_workers) as pool, tempfile.TemporaryDirectory() as tmpdir, zipfile.ZipFile( - zip_path, mode="r" - ) as zp: + with tempfile.TemporaryDirectory() as tmpdir, zipfile.ZipFile(zip_path, mode="r") as zp: ludb_dir = Path(tmpdir, "ludb") zp.extractall(ludb_dir) f = functools.partial( self.convert_pt_wfdb_to_hdf5, src_path=ludb_dir / subdir / "data", - dst_path=self.ds_path, + dst_path=self.path, force=force, ) - _ = list(tqdm(pool.imap(f, patient_ids), total=len(patient_ids))) + _ = process_map(f, patient_ids) # END WITH def convert_pt_wfdb_to_hdf5( diff --git a/heartkit/datasets/nstdb.py b/heartkit/datasets/nstdb.py index f63017a6..b9d72f4f 100644 --- a/heartkit/datasets/nstdb.py +++ b/heartkit/datasets/nstdb.py @@ -1,4 +1,3 @@ -import logging import os from pathlib import Path @@ -6,17 +5,22 @@ import numpy as np import numpy.typing as npt import physiokit as pk +import neuralspot_edge as nse -logger = logging.getLogger(__name__) +logger = nse.utils.setup_logger(__name__) class NstdbNoise: - """Noise stress test database (NSTDB) noise generator.""" - def __init__( self, target_rate: int, ): + """Noise stress test database (NSTDB) noise generator. + + Args: + target_rate (int): Target rate in Hz + """ + self.target_rate = target_rate self._noises: dict[str, npt.NDArray] | None = None diff --git a/heartkit/datasets/syntheticppg.py b/heartkit/datasets/ppg_synthetic.py similarity index 63% rename from heartkit/datasets/syntheticppg.py rename to heartkit/datasets/ppg_synthetic.py index 1585e08a..3fec3839 100644 --- a/heartkit/datasets/syntheticppg.py +++ b/heartkit/datasets/ppg_synthetic.py @@ -1,24 +1,25 @@ +import tempfile import contextlib -import io -import logging -import os from typing import Generator +from pathlib import Path import h5py import numpy as np import numpy.typing as npt import physiokit as pk from pydantic import BaseModel, Field +import neuralspot_edge as nse +from tqdm.contrib.concurrent import process_map from .dataset import HKDataset -from .defines import PatientGenerator +from .defines import PatientGenerator, PatientData from .nstdb import NstdbNoise -logger = logging.getLogger(__name__) +logger = nse.utils.setup_logger(__name__) -class SyntheticPpgParams(BaseModel, extra="allow"): - """PPG Synthetic parameters""" +class PpgSyntheticParams(BaseModel, extra="allow"): + """PPG Synthetic signal generator parameters""" sample_rate: float = Field(500, description="Signal sample rate (Hz)") duration: int = Field(10, description="Signal duration in sec") @@ -28,33 +29,62 @@ class SyntheticPpgParams(BaseModel, extra="allow"): noise_multiplier: tuple[float, float] = Field((0, 0), description="Noise multiplier range") -class SyntheticPpgDataset(HKDataset): - """Synthetic PPG dataset""" - +class PpgSyntheticDataset(HKDataset): def __init__( self, - ds_path: os.PathLike, num_pts: int = 250, params: dict | None = None, + path: str = Path(tempfile.gettempdir()) / "ppg-synthetic", + **kwargs, ) -> None: - super().__init__( - ds_path=ds_path, + """PPG synthetic dataset creates 1-lead PPG signal using PhysioKit. + + Args: + num_pts (int, optional): Number of patients. Defaults to 250. + params (dict | None, optional): PPG synthetic parameters (PpgSyntheticParams). Defaults to None. + path (str, optional): Path to dataset. Defaults to Path(tempfile.gettempdir()) / "ppg-synthetic". + + Example: + + ```python + import heartkit as hk + + # Create synthetic PPG dataset: + # - 10 patients + # - 500 Hz sample rate + # - 10 sec duration + # - heart rate between 40 and 120 bpm + # - frequency modulation between 0.2 and 0.4 + # - IBI randomness between 0.05 and 0.15 + # - no noise + ds = hk.datasets.PpgSyntheticDataset( + num_pts=10, + params=dict( + sample_rate=500, + duration=10, + heart_rate=(40, 120), + frequency_modulation=(0.2, 0.4), + ibi_randomness=(0.05, 0.15), + noise_multiplier=(0, 0), + ) ) + + with ds.patient_data[ds.patient_ids[0]] as pt: + ppg = pt["data"][:] + segs = pt["segmentations"][:] + fids = pt["fiducials"][:] + # END WITH + ``` + """ + super().__init__(path=path, **kwargs) self._noise_gen = None self._num_pts = num_pts - self.params = SyntheticPpgParams(**params or {}) - self._cache: dict[str, io.BytesIO] = {} - os.makedirs(self.ds_path, exist_ok=True) + self.params = PpgSyntheticParams(**params or {}) @property def name(self) -> str: """Dataset name""" - return "syntheticppg" - - @property - def cachable(self) -> bool: - """If dataset supports file caching.""" - return True + return "ppg-synthetic" @property def sampling_rate(self) -> int: @@ -102,47 +132,42 @@ def pt_key(self, patient_id: int): """Get patient key""" return f"{patient_id:05d}" + def load_patient_data(self, patient_id: int): + ppg, segs, fids = self._synthesize_signal( + frame_size=int(self.params.duration * self.sampling_rate), target_rate=self.sampling_rate + ) + pt_data = { + "data": ppg, + "segmentations": segs, + "fiducials": fids, + } + return pt_data + + def build_cache(self): + """Build cache""" + logger.info(f"Creating synthetic dataset cache with {self._num_pts} patients") + pts_data = process_map(self.load_patient_data, self.patient_ids) + self._cached_data = {self.pt_key(i): pt_data for i, pt_data in enumerate(pts_data)} + @contextlib.contextmanager - def patient_data(self, patient_id: int) -> Generator[h5py.Group, None, None]: + def patient_data(self, patient_id: int) -> Generator[PatientData, None, None]: """Get patient data Args: patient_id (int): Patient ID Returns: - Generator[h5py.Group, None, None]: Patient data + Generator[PatientData, None, None]: Patient data """ - ppg, segs, fids = self._synthesize_signal( - frame_size=int(self.params.duration * self.sampling_rate), target_rate=self.sampling_rate - ) - fp = io.BytesIO() - with h5py.File(fp, mode="w") as h5: - h5.create_dataset("data", data=ppg) - h5.create_dataset("segmentations", data=segs) - h5.create_dataset("fiducials", data=fids) - # END WITH - fp.seek(0) - with h5py.File(fp, mode="r") as h5: - yield h5 - - # pt_key = self.pt_key(patient_id) - # if pt_key not in self._cache: - # ppg, segs, fids = self._synthesize_signal( - # frame_size=int(self.params.duration * self.sampling_rate), target_rate=self.sampling_rate - # ) - # fp = io.BytesIO() - # with h5py.File(fp, mode="w") as h5: - # h5.create_dataset("data", data=ppg) - # h5.create_dataset("segmentations", data=segs) - # h5.create_dataset("fiducials", data=fids) - # # END WITH - # fp.seek(0) - # self._cache[pt_key] = fp - # # END IF - - # with h5py.File(self._cache[pt_key], mode="r") as h5: - # yield h5 - # # END WITH + pt_key = self.pt_key(patient_id) + if self.cacheable: + if pt_key not in self._cached_data: + self.build_cache() + yield self._cached_data[pt_key] + else: + pt_data = self.load_patient_data(patient_id) + yield pt_data + # END IF def signal_generator( self, @@ -154,8 +179,10 @@ def signal_generator( """Generate frames using patient generator. Args: - patient_generator (PatientGenerator): Generator that yields a tuple of patient id and patient data. - samples_per_patient (int): Samples per patient. + patient_generator (PatientGenerator): Generator that yields patient data. + frame_size (int): Frame size + samples_per_patient (int, optional): Samples per patient. Defaults to 1. + target_rate (int | None, optional): Target rate. Defaults to None. Returns: SampleGenerator: Generator of input data of shape (frame_size, 1) @@ -163,7 +190,7 @@ def signal_generator( if target_rate is None: target_rate = self.sampling_rate - input_size = int(np.round((self.sampling_rate / target_rate) * frame_size)) + input_size = int(np.ceil((self.sampling_rate / target_rate) * frame_size)) for pt in patient_generator: with self.patient_data(pt) as h5: @@ -176,6 +203,7 @@ def signal_generator( x = self.add_noise(x) if self.sampling_rate != target_rate: x = pk.signal.resample_signal(x, self.sampling_rate, target_rate, axis=0) + x = x[:frame_size] # END IF yield x # END FOR @@ -219,6 +247,7 @@ def _synthesize_signal( Args: frame_size (int): Frame size + target_rate (float | None, optional): Target rate. Defaults to None. Returns: tuple[npt.NDArray, npt.NDArray, npt.NDArray]: signal, segments, fiducials @@ -227,7 +256,7 @@ def _synthesize_signal( frequency_modulation = np.random.uniform( self.params.frequency_modulation[0], self.params.frequency_modulation[1] ) - frequency_modulation = min(frequency_modulation, 1 - 0.3 / (60 / heart_rate)) # Must be at least 300 ms IBI + frequency_modulation = min(frequency_modulation, 1 - 0.35 / (60 / heart_rate)) # Must be at least 300 ms IBI ibi_randomness = np.random.uniform(self.params.ibi_randomness[0], self.params.ibi_randomness[1]) ppg, segs, fids = pk.ppg.synthesize( diff --git a/heartkit/datasets/preprocessing.py b/heartkit/datasets/preprocessing.py deleted file mode 100644 index c3b69f2d..00000000 --- a/heartkit/datasets/preprocessing.py +++ /dev/null @@ -1,28 +0,0 @@ -import numpy.typing as npt -import physiokit as pk - -from ..defines import PreprocessParams - - -def preprocess_pipeline(x: npt.NDArray, preprocesses: list[PreprocessParams], sample_rate: float) -> npt.NDArray: - """Apply preprocessing pipeline - - Args: - x (npt.NDArray): Signal - preprocesses (list[PreprocessParams]): Preprocessing pipeline - sample_rate (float): Sampling rate in Hz. - - Returns: - npt.NDArray: Preprocessed signal - """ - for preprocess in preprocesses: - match preprocess.name: - case "filter": - x = pk.signal.filter_signal(x, sample_rate=sample_rate, **preprocess.params) - case "znorm": - x = pk.signal.normalize_signal(x, **preprocess.params) - case _: - raise ValueError(f"Unknown preprocess '{preprocess.name}'") - # END MATCH - # END FOR - return x diff --git a/heartkit/datasets/ptbxl.py b/heartkit/datasets/ptbxl.py index d29f873e..2109e1f4 100644 --- a/heartkit/datasets/ptbxl.py +++ b/heartkit/datasets/ptbxl.py @@ -1,12 +1,10 @@ import contextlib import functools -import logging import os import zipfile import random from collections.abc import Iterable from enum import IntEnum -from multiprocessing import Pool from typing import Generator import h5py @@ -15,13 +13,13 @@ import physiokit as pk import sklearn.model_selection from tqdm import tqdm +from tqdm.contrib.concurrent import process_map +import neuralspot_edge as nse -from ..utils import download_file from .dataset import HKDataset -from .defines import PatientGenerator -from .utils import download_s3_file +from .defines import PatientGenerator, PatientData -logger = logging.getLogger(__name__) +logger = nse.utils.setup_logger(__name__) class PtbxlScpCode(IntEnum): @@ -199,16 +197,14 @@ class PtbxlScpCode(IntEnum): class PtbxlDataset(HKDataset): - """PTBXL dataset""" + def __init__(self, leads: list[int] | None = None, **kwargs) -> None: + """PTBXL dataset consists of 21837 clinical 12-lead ECGs from 18885 patients. - def __init__( - self, - ds_path: os.PathLike, - leads: list[int] | None = None, - ) -> None: - super().__init__( - ds_path=ds_path, - ) + Args: + leads (list[int] | None, optional): Leads to use. Defaults to None. + + """ + super().__init__(**kwargs) self.leads = leads or list(range(12)) self._data_cache: dict[str, np.ndarray] = {} @@ -319,17 +315,40 @@ def label_key(self, label_type: str = "scp") -> str: raise ValueError(f"Invalid label type: {label_type}") @contextlib.contextmanager - def patient_data(self, patient_id: int) -> Generator[h5py.Group, None, None]: + def patient_data(self, patient_id: int) -> Generator[PatientData, None, None]: """Get patient data + !!! note + If cacheable, data is cached in memory and returned as dict + Otherwise, data is provided as HDF5 objects + + Patient Data Format: + - data: ECG data of shape (12, N) + - slabels: SCP labels of shape (N, 2) + - blabels: Beat labels of shape (N, 2) + Args: patient_id (int): Patient ID Returns: - Generator[h5py.Group, None, None]: Patient data + Generator[PatientData, None, None]: Patient data """ - with h5py.File(self.ds_path / f"{self._pt_key(patient_id)}.h5", mode="r") as h5: - yield h5 + pt_path = self.path / f"{self._pt_key(patient_id)}.h5" + if self.cacheable: + if patient_id not in self._cached_data: + pt_data = {} + with h5py.File(pt_path, mode="r") as h5: + pt_data["data"] = h5["data"][:] + pt_data["slabels"] = h5["slabels"][:] + pt_data["blabels"] = h5["blabels"][:] + self._cached_data[patient_id] = pt_data + # END IF + yield self._cached_data[patient_id] + else: + with h5py.File(pt_path, mode="r") as h5: + yield h5 + # END WITH + # END IF def signal_generator( self, @@ -341,9 +360,10 @@ def signal_generator( """Generate random frames. Args: - patient_generator (PatientGenerator): Generator that yields a tuple of patient id and patient data. - Patient data may contain only signals, since labels are not used. - samples_per_patient (int): Samples per patient. + patient_generator (PatientGenerator): Generator that yields patient data. + frame_size (int): Frame size + samples_per_patient (int, optional): Samples per patient. Defaults to 1. + target_rate (int | None, optional): Target rate. Defaults to None. Returns: Generator[npt.NDArray, None, None]: Generator of input data of shape (frame_size, 1) @@ -351,7 +371,7 @@ def signal_generator( if target_rate is None: target_rate = self.sampling_rate - input_size = int(np.round((self.sampling_rate / target_rate) * frame_size)) + input_size = int(np.ceil((self.sampling_rate / target_rate) * frame_size)) for pt in patient_generator: with self.patient_data(pt) as h5: @@ -364,6 +384,7 @@ def signal_generator( x = np.nan_to_num(x).astype(np.float32) if self.sampling_rate != target_rate: x = pk.signal.resample_signal(x, self.sampling_rate, target_rate, axis=0) + x = x[:frame_size] # truncate to frame size # END IF yield x # END FOR @@ -413,7 +434,7 @@ def signal_label_generator( samples_per_tgt = num_classes * [num_per_tgt] # END IF - input_size = int(np.round((self.sampling_rate / target_rate) * frame_size)) + input_size = int(np.ceil((self.sampling_rate / target_rate) * frame_size)) for pt in patient_generator: # 1. Grab patient scp label (fixed for all samples) @@ -470,6 +491,8 @@ def signal_label_generator( # Resample if needed if self.sampling_rate != target_rate: x = pk.signal.resample_signal(x, self.sampling_rate, target_rate, axis=0) + x = x[:frame_size] # truncate to frame size + x = np.reshape(x, (frame_size, 1)) yield x, y # END FOR # END FOR @@ -564,8 +587,7 @@ def get_patients_labels( """ ids = patient_ids.tolist() func = functools.partial(self.get_patient_labels, label_map=label_map, label_type=label_type) - with Pool() as pool: - pts_labels = list(pool.imap(func, ids)) + pts_labels = process_map(func, ids) return pts_labels def get_patient_scp_codes(self, patient_id: int) -> list[int]: @@ -607,10 +629,10 @@ def download(self, num_workers: int | None = None, force: bool = False): num_workers (int | None, optional): # parallel workers. Defaults to None. force (bool, optional): Force redownload. Defaults to False. """ - os.makedirs(self.ds_path, exist_ok=True) - zip_path = self.ds_path / f"{self.name}.zip" + os.makedirs(self.path, exist_ok=True) + zip_path = self.path / f"{self.name}.zip" - did_download = download_s3_file( + did_download = nse.utils.download_s3_file( key=f"{self.name}/{self.name}.zip", dst=zip_path, bucket="ambiq-ai-datasets", @@ -618,7 +640,7 @@ def download(self, num_workers: int | None = None, force: bool = False): ) if did_download: with zipfile.ZipFile(zip_path, "r") as zf: - zf.extractall(self.ds_path) + zf.extractall(self.path) def download_raw_dataset(self, num_workers: int | None = None, force: bool = False): """Downloads full dataset zipfile and converts into individial patient HDF5 files. @@ -632,14 +654,14 @@ def download_raw_dataset(self, num_workers: int | None = None, force: bool = Fal "https://www.physionet.org/static/published-projects/ptb-xl/" "ptb-xl-a-large-publicly-available-electrocardiography-dataset-1.0.2.zip" ) - ds_zip_path = self.ds_path / "ptbxl.zip" - os.makedirs(self.ds_path, exist_ok=True) + ds_zip_path = self.path / "ptbxl.zip" + os.makedirs(self.path, exist_ok=True) if os.path.exists(ds_zip_path) and not force: logger.warning( f"Zip file already exists. Please delete or set `force` flag to redownload. PATH={ds_zip_path}" ) else: - download_file(ds_url, ds_zip_path, progress=True) + nse.utils.download_file(ds_url, ds_zip_path, progress=True) # 2. Extract and convert patient ECG data to H5 files logger.debug("Processing PTB-XL patient data") @@ -683,7 +705,7 @@ def _convert_dataset_zip_to_hdf5( zp_root = "ptb-xl-a-large-publicly-available-electrocardiography-dataset-1.0.2" # scp_df = pd.read_csv(io.BytesIO(zp.read(os.path.join(zp_root, "scp_statements.csv")))) - with open(self.ds_path / "scp_statements.csv", "wb") as fp: + with open(self.path / "scp_statements.csv", "wb") as fp: fp.write(zp.read(os.path.join(zp_root, "scp_statements.csv"))) db_df = pd.read_csv(io.BytesIO(zp.read(os.path.join(zp_root, "ptbxl_database.csv")))) @@ -694,7 +716,7 @@ def _convert_dataset_zip_to_hdf5( for patient in tqdm(patient_ids, desc="Converting"): # logger.debug(f"Processing patient {patient}") pt_id = self._pt_key(patient) - pt_path = self.ds_path / f"{pt_id}.h5" + pt_path = self.path / f"{pt_id}.h5" pt_info = db_df[db_df.ecg_id == patient] if len(pt_info) == 0: diff --git a/heartkit/datasets/qtdb.py b/heartkit/datasets/qtdb.py index bd121cd6..4f26fa19 100644 --- a/heartkit/datasets/qtdb.py +++ b/heartkit/datasets/qtdb.py @@ -1,25 +1,22 @@ import contextlib import functools -import logging import os import random import tempfile import zipfile -from multiprocessing import Pool from typing import Generator import h5py import numpy as np import numpy.typing as npt import physiokit as pk -from tqdm import tqdm +from tqdm.contrib.concurrent import process_map +import neuralspot_edge as nse -from ..utils import download_file from .dataset import HKDataset -from .defines import PatientGenerator -from .utils import download_s3_file +from .defines import PatientGenerator, PatientData -logger = logging.getLogger(__name__) +logger = nse.utils.setup_logger(__name__) QtdbSymbolMap = { "o": 0, # Other @@ -42,9 +39,9 @@ class QtdbDataset(HKDataset): def __init__( self, - ds_path: os.PathLike, + **kwargs, ) -> None: - super().__init__(ds_path=ds_path) + super().__init__(**kwargs) @property def name(self) -> str: @@ -204,17 +201,33 @@ def _pt_key(self, patient_id: int): return f"{patient_id}" @contextlib.contextmanager - def patient_data(self, patient_id: int) -> Generator[h5py.Group, None, None]: + def patient_data(self, patient_id: int) -> Generator[PatientData, None, None]: """Get patient data Args: patient_id (int): Patient ID Returns: - Generator[h5py.Group, None, None]: Patient data + Generator[PatientData, None, None]: Patient data """ - with h5py.File(self.ds_path / f"{self._pt_key(patient_id)}.h5", mode="r") as h5: - yield h5 + pt_key = self._pt_key(patient_id) + pt_path = self.path / f"{pt_key}.h5" + if self.cacheable: + if pt_key not in self._cached_data: + pt_data = {} + with h5py.File(pt_path, mode="r") as h5: + pt_data["data"] = h5["data"][:] + pt_data["segmentations"] = h5["segmentations"][:] + pt_data["fiducials"] = h5["fiducials"][:] + # END WITH + self._cached_data[pt_key] = pt_data + # END IF + yield self._cached_data[pt_key] + else: + with h5py.File(pt_path, mode="r") as h5: + yield h5 + # END WITH + # END IF def signal_generator( self, @@ -226,9 +239,10 @@ def signal_generator( """Generate random frames. Args: - patient_generator (PatientGenerator): Generator that yields a tuple of patient id and patient data. - Patient data may contain only signals, since labels are not used. - samples_per_patient (int): Samples per patient. + patient_generator (PatientGenerator): Generator that yields patient data. + frame_size (int): Frame size + samples_per_patient (int, optional): Samples per patient. Defaults to 1. + target_rate (int | None, optional): Target rate. Defaults to None. Returns: Generator[npt.NDArray, None, None]: Generator of input data of shape (frame_size, 1) @@ -236,7 +250,7 @@ def signal_generator( if target_rate is None: target_rate = self.sampling_rate - input_size = int(np.round((self.sampling_rate / target_rate) * frame_size)) + input_size = int(np.ceil((self.sampling_rate / target_rate) * frame_size)) for pt in patient_generator: with self.patient_data(pt) as h5: @@ -249,6 +263,7 @@ def signal_generator( x = np.nan_to_num(x).astype(np.float32) if self.sampling_rate != target_rate: x = pk.signal.resample_signal(x, self.sampling_rate, target_rate, axis=0) + x = x[:frame_size] # END IF yield x # END FOR @@ -279,10 +294,10 @@ def download(self, num_workers: int | None = None, force: bool = False): num_workers (int | None, optional): # parallel workers. Defaults to None. force (bool, optional): Force redownload. Defaults to False. """ - os.makedirs(self.ds_path, exist_ok=True) - zip_path = self.ds_path / f"{self.name}.zip" + os.makedirs(self.path, exist_ok=True) + zip_path = self.path / f"{self.name}.zip" - did_download = download_s3_file( + did_download = nse.utils.download_s3_file( key=f"{self.name}/{self.name}.zip", dst=zip_path, bucket="ambiq-ai-datasets", @@ -290,7 +305,7 @@ def download(self, num_workers: int | None = None, force: bool = False): ) if did_download: with zipfile.ZipFile(zip_path, "r") as zf: - zf.extractall(self.ds_path) + zf.extractall(self.path) def download_raw_dataset(self, num_workers: int | None = None, force: bool = False): """Downloads full dataset zipfile and converts into individial patient HDF5 files. @@ -301,14 +316,14 @@ def download_raw_dataset(self, num_workers: int | None = None, force: bool = Fal """ logger.debug("Downloading QTDB dataset") ds_url = "https://physionet.org/static/published-projects/qtdb/qt-database-1.0.0.zip" - ds_zip_path = self.ds_path / "qtdb.zip" - os.makedirs(self.ds_path, exist_ok=True) + ds_zip_path = self.path / "qtdb.zip" + os.makedirs(self.path, exist_ok=True) if os.path.exists(ds_zip_path) and not force: logger.warning( f"Zip file already exists. Please delete or set `force` flag to redownload. PATH={ds_zip_path}" ) else: - download_file(ds_url, ds_zip_path, progress=True) + nse.utils.download_file(ds_url, ds_zip_path, progress=True) # 2. Extract and convert patient ECG data to H5 files logger.debug("Generating QT patient data") @@ -396,17 +411,15 @@ def convert_dataset_zip_to_hdf5( patient_ids = self.patient_ids subdir = "qt-database-1.0.0" - with Pool(processes=num_workers) as pool, tempfile.TemporaryDirectory() as tmpdir, zipfile.ZipFile( - zip_path, mode="r" - ) as zp: + with tempfile.TemporaryDirectory() as tmpdir, zipfile.ZipFile(zip_path, mode="r") as zp: qtdb_dir = tmpdir / "qtdb" zp.extractall(qtdb_dir) f = functools.partial( self.convert_pt_wfdb_to_hdf5, src_path=qtdb_dir / subdir, - dst_path=self.ds_path, + dst_path=self.path, force=force, ) - _ = list(tqdm(pool.imap(f, patient_ids), total=len(patient_ids))) + _ = process_map(f, patient_ids) # END WITH diff --git a/heartkit/datasets/utils.py b/heartkit/datasets/utils.py deleted file mode 100644 index de977065..00000000 --- a/heartkit/datasets/utils.py +++ /dev/null @@ -1,374 +0,0 @@ -import functools -import os -import random -from concurrent.futures import ThreadPoolExecutor, as_completed -from pathlib import Path -from typing import Callable, Generator, Iterable, TypeVar - -import boto3 -import numpy as np -import numpy.typing as npt -import tensorflow as tf -from botocore import UNSIGNED -from botocore.client import Config -from tqdm import tqdm - -from ..utils import compute_checksum, setup_logger - -logger = setup_logger(__name__) - - -def create_dataset_from_data(x: npt.NDArray, y: npt.NDArray, spec: tuple[tf.TensorSpec]) -> tf.data.Dataset: - """Helper function to create dataset from static data - - Args: - x (npt.NDArray): Numpy data - y (npt.NDArray): Numpy labels - - Returns: - tf.data.Dataset: Dataset - """ - return tf.data.Dataset.zip((tf.data.Dataset.from_tensor_slices(x), tf.data.Dataset.from_tensor_slices(y))) - - -T = TypeVar("T") -K = TypeVar("K") - - -def buffered_generator(generator: Generator[T, None, None], buffer_size: int) -> Generator[list[T], None, None]: - """Buffer the elements yielded by a generator. New elements replace the oldest elements in the buffer. - - Args: - generator (Generator[T]): Generator object. - buffer_size (int): Number of elements in the buffer. - - Returns: - Generator[list[T], None, None]: Yields a buffer. - """ - buffer = [] - for e in generator: - buffer.append(e) - if len(buffer) == buffer_size: - break - yield buffer - for e in generator: - buffer = buffer[1:] + [e] - yield buffer - - -def uniform_id_generator( - ids: Iterable[T], - repeat: bool = True, - shuffle: bool = True, -) -> Generator[T, None, None]: - """Simple generator that yields ids in a uniform manner. - - Args: - ids (pt.ArrayLike): Array of ids - repeat (bool, optional): Whether to repeat generator. Defaults to True. - shuffle (bool, optional): Whether to shuffle ids.. Defaults to True. - - Returns: - Generator[T, None, None]: Generator - Yields: - T: Id - """ - ids = np.copy(ids) - while True: - if shuffle: - np.random.shuffle(ids) - yield from ids - if not repeat: - break - # END IF - # END WHILE - - -def random_id_generator( - ids: Iterable[T], - weights: list[int] | None = None, -) -> Generator[T, None, None]: - """Simple generator that yields ids in a random manner. - - Args: - ids (pt.ArrayLike): Array of ids - weights (list[int], optional): Weights for each id. Defaults to None. - - Returns: - Generator[T, None, None]: Generator - - Yields: - T: Id - """ - while True: - yield random.choice(ids) - # END WHILE - - -def transform_dataset_pipeline( - ds: tf.data.Dataset, - buffer_size: int | None = None, - batch_size: int | None = None, - prefetch_size: int | None = None, -) -> tf.data.Dataset: - """Transform dataset pipeline - - Args: - ds (tf.data.Dataset): Dataset - buffer_size (int | None, optional): Buffer size. Defaults to None. - batch_size (int | None, optional): Batch size. Defaults to None. - prefetch_size (int | None, optional): Prefetch size. Defaults to None. - - Returns: - tf.data.Dataset: Transformed dataset - """ - if buffer_size is not None: - ds = ds.shuffle( - buffer_size=buffer_size, - reshuffle_each_iteration=True, - ) - if batch_size is not None: - ds = ds.batch( - batch_size=batch_size, - drop_remainder=False, - ) - if prefetch_size is not None: - ds = ds.prefetch(buffer_size=tf.data.AUTOTUNE) - return ds - - -def create_interleaved_dataset_from_generator( - data_generator: Callable[[Generator[T, None, None]], Generator[K, None, None]], - id_generator: Callable[[list[T]], Generator[T, None, None]], - ids: list[T], - spec: tuple[tf.TensorSpec, tf.TensorSpec], - preprocess: Callable[[K], K] | None = None, - num_workers: int = 4, -) -> tf.data.Dataset: - """Create TF dataset pipeline by interleaving multiple workers across ids - - The id_generator is used to generate ids for each worker. - The data_generator is used to generate data for each id. - - Args: - data_generator (Callable[[Generator[T, None, None]], Generator[K, None, None]]): Data generator - id_generator (Callable[[list[T]], Generator[T, None, None]]): Id generator - ids (list[T]): List of ids - spec (tuple[tf.TensorSpec, tf.TensorSpec]): Tensor spec - preprocess (Callable[[K], K] | None, optional): Preprocess function. Defaults to None. - num_workers (int, optional): Number of workers. Defaults to 4. - - Returns: - tf.data.Dataset: Dataset - """ - - def split_generator(split_ids: list[T]) -> tf.data.Dataset: - """Split generator per worker""" - - def ds_gen(): - """Worker generator routine""" - split_id_generator = id_generator(split_ids) - return map(preprocess, data_generator(split_id_generator)) - - return tf.data.Dataset.from_generator( - ds_gen, - output_signature=spec, - ) - - # END IF - - num_workers = min(num_workers, len(ids)) - split = len(ids) // num_workers - logger.debug(f"Splitting {len(ids)} ids into {num_workers} workers with {split} ids each") - ds_splits = [split_generator(ids[i * split : (i + 1) * split]) for i in range(num_workers)] - - # Create TF datasets (interleave workers) - ds = tf.data.Dataset.from_tensor_slices(ds_splits) - - ds = ds.interleave( - lambda x: x, - cycle_length=num_workers, - deterministic=False, - num_parallel_calls=tf.data.AUTOTUNE, - ) - - return ds - - -def _get_s3_client(config: Config | None = None) -> boto3.client: - """Get S3 client - - Args: - config (Config | None, optional): Boto3 config. Defaults to None. - - Returns: - boto3.client: S3 client - """ - session = boto3.Session() - return session.client("s3", config=config) - - -def download_s3_file( - key: str, - dst: Path, - bucket: str, - client: boto3.client = None, - checksum: str = "size", - config: Config | None = Config(signature_version=UNSIGNED), -) -> bool: - """Download a file from S3 - - Args: - key (str): Object key - dst (Path): Destination path - bucket (str): Bucket name - client (boto3.client): S3 client - checksum (str, optional): Checksum type. Defaults to "size". - config (Config, optional): Boto3 config. Defaults to Config(signature_version=UNSIGNED). - - Returns: - bool: True if file was downloaded, False if already exists - """ - - if client is None: - client = _get_s3_client(config) - - if not dst.is_file(): - pass - elif checksum == "size": - obj = client.head_object(Bucket=bucket, Key=key) - if dst.stat().st_size == obj["ContentLength"]: - return False - elif checksum == "md5": - obj = client.head_object(Bucket=bucket, Key=key) - etag = obj["ETag"] - checksum_type = obj.get("ChecksumAlgorithm", ["md5"])[0] - calculated_checksum = compute_checksum(dst, checksum) - if etag == calculated_checksum and checksum_type.lower() == "md5": - return False - # END IF - - client.download_file( - Bucket=bucket, - Key=key, - Filename=str(dst), - ) - - return True - - -def download_s3_object( - item: dict[str, str], - dst: Path, - bucket: str, - client: boto3.client = None, - checksum: str = "size", - config: Config | None = Config(signature_version=UNSIGNED), -) -> bool: - """Download an object from S3 - - Args: - object (dict[str, str]): Object metadata - dst (Path): Destination path - bucket (str): Bucket name - client (boto3.client): S3 client - checksum (str, optional): Checksum type. Defaults to "size". - config (Config, optional): Boto3 config. Defaults to Config(signature_version=UNSIGNED). - - Returns: - bool: True if file was downloaded, False if already exists - """ - - # Is a directory, skip - if item["Key"].endswith("/"): - os.makedirs(dst, exist_ok=True) - return False - - if not dst.is_file(): - pass - elif checksum == "size": - if dst.stat().st_size == item["Size"]: - return False - elif checksum == "md5": - etag = item["ETag"] - checksum_type = item.get("ChecksumAlgorithm", ["md5"])[0] - calculated_checksum = compute_checksum(dst, checksum) - if etag == calculated_checksum and checksum_type.lower() == "md5": - return False - # END IF - - if client is None: - client = _get_s3_client() - - client.download_file( - Bucket=bucket, - Key=item["Key"], - Filename=str(dst), - ) - - return True - - -def download_s3_objects( - bucket: str, - prefix: str, - dst: Path, - checksum: str = "size", - progress: bool = True, - num_workers: int | None = None, - config: Config | None = Config(signature_version=UNSIGNED), -): - """Download all objects in a S3 bucket with a given prefix - - Args: - bucket (str): Bucket name - prefix (str): Prefix to filter objects - dst (Path): Destination directory - checksum (str, optional): Checksum type. Defaults to "size". - progress (bool, optional): Show progress bar. Defaults to True. - num_workers (int | None, optional): Number of workers. Defaults to None. - config (Config | None, optional): Boto3 config. Defaults to Config(signature_version=UNSIGNED). - - """ - - client = _get_s3_client(config) - - # Fetch all objects in the bucket with the given prefix - items = [] - fetching = True - next_token = None - while fetching: - if next_token is None: - response = client.list_objects_v2(Bucket=bucket, Prefix=prefix) - else: - response = client.list_objects_v2(Bucket=bucket, Prefix=prefix, ContinuationToken=next_token) - items.extend(response["Contents"]) - next_token = response.get("NextContinuationToken", None) - fetching = next_token is not None - # END WHILE - - logger.debug(f"Found {len(items)} objects in {bucket}/{prefix}") - - os.makedirs(dst, exist_ok=True) - - func = functools.partial(download_s3_object, bucket=bucket, client=client, checksum=checksum) - - pbar = tqdm(total=len(items), unit="objects") if progress else None - - with ThreadPoolExecutor(max_workers=num_workers) as executor: - futures = ( - executor.submit( - func, - item, - dst / item["Key"], - ) - for item in items - ) - for future in as_completed(futures): - err = future.exception() - if err: - logger.exception("Failed on file") - if pbar: - pbar.update(1) - # END FOR - # END WITH diff --git a/heartkit/defines.py b/heartkit/defines.py index daec6a7c..c5e06429 100644 --- a/heartkit/defines.py +++ b/heartkit/defines.py @@ -20,36 +20,15 @@ class QuantizationParams(BaseModel, extra="allow"): fallback: bool = Field(False, description="Fallback to float32") -class ModelArchitecture(BaseModel, extra="allow"): - """Model architecture parameters""" +class NamedParams(BaseModel, extra="allow"): + """Named parameters is used to store parameters for a specific model, preprocessing, or augmentation. + Typically name refers to class/method name and params is provided as kwargs. + """ name: str params: dict[str, Any] = Field(default_factory=dict, description="Parameters") -class PreprocessParams(BaseModel, extra="allow"): - """Preprocessing parameters""" - - name: str - params: dict[str, Any] - - -class AugmentationParams(BaseModel, extra="allow"): - """Augmentation parameters""" - - name: str - params: dict[str, tuple[float | int, float | int]] - - -class DatasetParams(BaseModel, extra="allow"): - """Dataset parameters""" - - name: str - path: Path = Field(default_factory=Path, description="Dataset path") - params: dict[str, Any] = Field(default_factory=dict, description="Parameters") - weight: float = Field(1, description="Dataset weight") - - class HKMode(StrEnum): """HeartKit Mode""" @@ -67,7 +46,7 @@ class HKDownloadParams(BaseModel, extra="allow"): default_factory=lambda: Path(tempfile.gettempdir()), description="Job output directory", ) - datasets: list[DatasetParams] = Field(default_factory=list, description="Datasets") + datasets: list[NamedParams] = Field(default_factory=list, description="Datasets") progress: bool = Field(True, description="Display progress bar") force: bool = Field(False, description="Force download dataset- overriding existing files") data_parallelism: int = Field( @@ -76,199 +55,106 @@ class HKDownloadParams(BaseModel, extra="allow"): ) -class HKTrainParams(BaseModel, extra="allow"): - """Train command params""" +class HKTaskParams(BaseModel, extra="allow"): + """Task command params""" + # Common arguments name: str = Field("experiment", description="Experiment name") project: str = Field("heartkit", description="Project name") job_dir: Path = Field( default_factory=lambda: Path(tempfile.gettempdir()), description="Job output directory", ) + # Dataset arguments - datasets: list[DatasetParams] = Field(default_factory=list, description="Datasets") + datasets: list[NamedParams] = Field(default_factory=list, description="Datasets") + dataset_weights: list[float] | None = Field(None, description="Dataset weights") + # Signal arguments sampling_rate: int = Field(250, description="Target sampling rate (Hz)") - frame_size: int = Field(1250, description="Frame size") + frame_size: int = Field(1250, description="Frame size in samples") + + # Dataloader arguments + samples_per_patient: int | list[int] = Field(1000, description="# train samples per patient") + val_samples_per_patient: int | list[int] = Field(1000, description="# validation samples per patient") + test_samples_per_patient: int | list[int] = Field(1000, description="# test samples per patient") + + # Preprocessing/Augmentation arguments + preprocesses: list[NamedParams] = Field(default_factory=list, description="Preprocesses") + augmentations: list[NamedParams] = Field(default_factory=list, description="Augmentations") + + # Class arguments num_classes: int = Field(1, description="# of classes") class_map: dict[int, int] = Field(default_factory=lambda: {1: 1}, description="Class/label mapping") class_names: list[str] | None = Field(default=None, description="Class names") - samples_per_patient: int | list[int] = Field(1000, description="# train samples per patient") - val_samples_per_patient: int | list[int] = Field(1000, description="# validation samples per patient") + # Split arguments train_patients: float | None = Field(None, description="# or proportion of patients for training") val_patients: float | None = Field(None, description="# or proportion of patients for validation") + test_patients: float | None = Field(None, description="# or proportion of patients for testing") + val_file: Path | None = Field(None, description="Path to load/store pickled validation file") + test_file: Path | None = Field(None, description="Path to load/store pickled test file") val_size: int | None = Field(None, description="# samples for validation") + test_size: int = Field(10000, description="# samples for testing") # Model arguments resume: bool = Field(False, description="Resume training") - architecture: ModelArchitecture | None = Field(default=None, description="Custom model architecture") - model_file: Path | None = Field(None, description="Path to save model file (.keras)") - threshold: float | None = Field(None, description="Model output threshold") - - weights_file: Path | None = Field(None, description="Path to a checkpoint weights to load") + architecture: NamedParams | None = Field(default=None, description="Custom model architecture") + model_file: Path | None = Field(None, description="Path to load/save model file (.keras)") + use_logits: bool = Field(True, description="Use logits output or softmax") + weights_file: Path | None = Field(None, description="Path to a checkpoint weights to load/save") quantization: QuantizationParams = Field(default_factory=QuantizationParams, description="Quantization parameters") + # Training arguments lr_rate: float = Field(1e-3, description="Learning rate") lr_cycles: int = Field(3, description="Number of learning rate cycles") lr_decay: float = Field(0.9, description="Learning rate decay") - class_weights: Literal["balanced", "fixed"] = Field("fixed", description="Class weights") label_smoothing: float = Field(0, description="Label smoothing") batch_size: int = Field(32, description="Batch size") - buffer_size: int = Field(100, description="Buffer size") + buffer_size: int = Field(100, description="Buffer cache size") epochs: int = Field(50, description="Number of epochs") steps_per_epoch: int = Field(10, description="Number of steps per epoch") + val_steps_per_epoch: int = Field(10, description="Number of validation steps") val_metric: Literal["loss", "acc", "f1"] = Field("loss", description="Performance metric") - # Preprocessing/Augmentation arguments - preprocesses: list[PreprocessParams] = Field(default_factory=list, description="Preprocesses") - augmentations: list[AugmentationParams] = Field(default_factory=list, description="Augmentations") - # Extra arguments - seed: int | None = Field(None, description="Random state seed") - data_parallelism: int = Field( - default_factory=lambda: os.cpu_count() or 1, - description="# of data loaders running in parallel", - ) - model_config = ConfigDict(protected_namespaces=()) - verbose: int = Field(1, ge=0, le=2, description="Verbosity level") - - def model_post_init(self, __context: Any) -> None: - """Post init hook""" - - if self.val_file and len(self.val_file.parts) == 1: - self.val_file = self.job_dir / self.val_file - - if self.model_file and len(self.model_file.parts) == 1: - self.model_file = self.job_dir / self.model_file + class_weights: Literal["balanced", "fixed"] = Field("fixed", description="Class weights") - if self.weights_file and len(self.weights_file.parts) == 1: - self.weights_file = self.job_dir / self.weights_file + # Evaluation arguments + threshold: float | None = Field(None, description="Model output threshold") + val_metric_threshold: float | None = Field(0.98, description="Validation metric threshold") + # Export arguments + tflm_var_name: str = Field("g_model", description="TFLite Micro C variable name") + tflm_file: Path | None = Field(None, description="Path to copy TFLM header file (e.g. ./model_buffer.h)") -class HKTestParams(BaseModel, extra="allow"): - """Test command params""" + # Demo arguments + backend: str = Field("pc", description="Backend") + demo_size: int | None = Field(1000, description="# samples for demo") + display_report: bool = Field(True, description="Display report") - name: str = Field("experiment", description="Experiment name") - project: str = Field("heartkit", description="Project name") - job_dir: Path = Field( - default_factory=lambda: Path(tempfile.gettempdir()), - description="Job output directory", - ) - # Dataset arguments - datasets: list[DatasetParams] = Field(default_factory=list, description="Datasets") - sampling_rate: int = Field(250, description="Target sampling rate (Hz)") - frame_size: int = Field(1250, description="Frame size") - num_classes: int = Field(1, description="# of classes") - class_map: dict[int, int] = Field(default_factory=lambda: {1: 1}, description="Class/label mapping") - class_names: list[str] | None = Field(default=None, description="Class names") - test_samples_per_patient: int | list[int] = Field(1000, description="# test samples per patient") - test_patients: float | None = Field(None, description="# or proportion of patients for testing") - test_size: int = Field(200_000, description="# samples for testing") - test_file: Path | None = Field(None, description="Path to load/store pickled test file") - preprocesses: list[PreprocessParams] = Field(default_factory=list, description="Preprocesses") - augmentations: list[AugmentationParams] = Field(default_factory=list, description="Augmentations") - # Model arguments - model_file: Path | None = Field(None, description="Path to save model file (.keras)") - threshold: float | None = Field(None, description="Model output threshold") # Extra arguments seed: int | None = Field(None, description="Random state seed") data_parallelism: int = Field( default_factory=lambda: os.cpu_count() or 1, description="# of data loaders running in parallel", ) - model_config = ConfigDict(protected_namespaces=()) verbose: int = Field(1, ge=0, le=2, description="Verbosity level") - - def model_post_init(self, __context: Any) -> None: - """Post init hook""" - - if self.test_file and len(self.test_file.parts) == 1: - self.test_file = self.job_dir / self.test_file - - if self.model_file and len(self.model_file.parts) == 1: - self.model_file = self.job_dir / self.model_file - - -class HKExportParams(BaseModel, extra="allow"): - """Export command params""" - - name: str = Field("experiment", description="Experiment name") - project: str = Field("heartkit", description="Project name") - job_dir: Path = Field( - default_factory=lambda: Path(tempfile.gettempdir()), - description="Job output directory", - ) - # Dataset arguments - datasets: list[DatasetParams] = Field(default_factory=list, description="Datasets") - sampling_rate: int = Field(250, description="Target sampling rate (Hz)") - frame_size: int = Field(1250, description="Frame size") - num_classes: int = Field(3, description="# of classes") - class_map: dict[int, int] = Field(default_factory=lambda: {1: 1}, description="Class/label mapping") - class_names: list[str] | None = Field(default=None, description="Class names") - test_samples_per_patient: int | list[int] = Field(100, description="# test samples per patient") - test_patients: float | None = Field(None, description="# or proportion of patients for testing") - test_size: int = Field(100_000, description="# samples for testing") - test_file: Path | None = Field(None, description="Path to load/store pickled test file") - preprocesses: list[PreprocessParams] = Field(default_factory=list, description="Preprocesses") - augmentations: list[AugmentationParams] = Field(default_factory=list, description="Augmentations") - model_file: Path | None = Field(None, description="Path to save model file (.keras)") - threshold: float | None = Field(None, description="Model output threshold") - val_acc_threshold: float | None = Field(0.98, description="Validation accuracy threshold") - use_logits: bool = Field(True, description="Use logits output or softmax") - quantization: QuantizationParams = Field(default_factory=QuantizationParams, description="Quantization parameters") - tflm_var_name: str = Field("g_model", description="TFLite Micro C variable name") - tflm_file: Path | None = Field(None, description="Path to copy TFLM header file (e.g. ./model_buffer.h)") - data_parallelism: int = Field( - default_factory=lambda: os.cpu_count() or 1, - description="# of data loaders running in parallel", - ) model_config = ConfigDict(protected_namespaces=()) - verbose: int = Field(1, ge=0, le=2, description="Verbosity level") def model_post_init(self, __context: Any) -> None: """Post init hook""" + if self.val_file and len(self.val_file.parts) == 1: + self.val_file = self.job_dir / self.val_file + if self.test_file and len(self.test_file.parts) == 1: self.test_file = self.job_dir / self.test_file if self.model_file and len(self.model_file.parts) == 1: self.model_file = self.job_dir / self.model_file + if self.weights_file and len(self.weights_file.parts) == 1: + self.weights_file = self.job_dir / self.weights_file + if self.tflm_file and len(self.tflm_file.parts) == 1: self.tflm_file = self.job_dir / self.tflm_file - - -class HKDemoParams(BaseModel, extra="allow"): - """HK demo command params""" - - name: str = Field("experiment", description="Experiment name") - project: str = Field("heartkit", description="Project name") - job_dir: Path = Field( - default_factory=lambda: Path(tempfile.gettempdir()), - description="Job output directory", - ) - # Dataset arguments - datasets: list[DatasetParams] = Field(default_factory=list, description="Datasets") - sampling_rate: int = Field(250, description="Target sampling rate (Hz)") - frame_size: int = Field(1250, description="Frame size") - num_classes: int = Field(1, description="# of classes") - class_map: dict[int, int] = Field(default_factory=lambda: {1: 1}, description="Class/label mapping") - class_names: list[str] | None = Field(default=None, description="Class names") - preprocesses: list[PreprocessParams] = Field(default_factory=list, description="Preprocesses") - augmentations: list[AugmentationParams] = Field(default_factory=list, description="Augmentations") - # Model arguments - model_file: Path | None = Field(None, description="Path to save model file (.keras)") - backend: str = Field("pc", description="Backend") - # Demo arguments - demo_size: int | None = Field(1000, description="# samples for demo") - display_report: bool = Field(True, description="Display report") - # Extra arguments - seed: int | None = Field(None, description="Random state seed") - model_config = ConfigDict(protected_namespaces=()) - verbose: int = Field(1, ge=0, le=2, description="Verbosity level") - - def model_post_init(self, __context: Any) -> None: - """Post init hook""" - - if self.model_file and len(self.model_file.parts) == 1: - self.model_file = self.job_dir / self.model_file diff --git a/heartkit/metrics.py b/heartkit/metrics.py deleted file mode 100644 index eac95f6f..00000000 --- a/heartkit/metrics.py +++ /dev/null @@ -1,145 +0,0 @@ -import warnings -from typing import Literal - -import numpy as np -import numpy.typing as npt -from sklearn.metrics import f1_score, jaccard_score - - -def compute_iou( - y_true: npt.NDArray, - y_pred: npt.NDArray, - average: Literal["micro", "macro", "weighted"] = "micro", -) -> float: - """Compute IoU - - Args: - y_true (npt.NDArray): Y true - y_pred (npt.NDArray): Y predicted - - Returns: - float: IoU - """ - return jaccard_score(y_true.flatten(), y_pred.flatten(), average=average) - - -def f1( - y_true: npt.NDArray, - y_prob: npt.NDArray, - multiclass: bool = False, - threshold: float = None, -) -> npt.NDArray | float: - """Compute F1 scores - - Args: - y_true ( npt.NDArray): Y true - y_prob ( npt.NDArray): 2D matrix with class probs - multiclass (bool, optional): If multiclass. Defaults to False. - threshold (float, optional): Decision threshold for multiclass. Defaults to None. - - Returns: - npt.NDArray|float: F1 scores - """ - if y_prob.ndim != 2: - raise ValueError("y_prob must be a 2d matrix with class probabilities for each sample") - if y_true.ndim == 1: # we assume that y_true is sparse (consequently, multiclass=False) - if multiclass: - raise ValueError("if y_true cannot be sparse and multiclass at the same time") - depth = y_prob.shape[1] - y_true = _one_hot(y_true, depth) - if multiclass: - if threshold is None: - threshold = 0.5 - y_pred = y_prob >= threshold - else: - y_pred = y_prob >= np.max(y_prob, axis=1)[:, None] - return f1_score(y_true, y_pred, average="macro") - - -def f_max( - y_true: npt.NDArray, - y_prob: npt.NDArray, - thresholds: float | list[float] | None = None, -) -> tuple[float, float]: - """Compute F max - source: https://github.com/helme/ecg_ptbxl_benchmarking - - Args: - y_true (npt.NDArray): Y True - y_prob (npt.NDArray): Y probs - thresholds (float|list[float]|None, optional): Thresholds. Defaults to None. - - Returns: - tuple[float, float]: F1 and thresholds - """ - if thresholds is None: - thresholds = np.linspace(0, 1, 100) - pr, rc = macro_precision_recall(y_true, y_prob, thresholds) - f1s = (2 * pr * rc) / (pr + rc) - i = np.nanargmax(f1s) - return f1s[i], thresholds[i] - - -def macro_precision_recall( - y_true: npt.NDArray, y_prob: npt.NDArray, thresholds: npt.NDArray -) -> tuple[np.float_, np.float_]: - """Compute macro precision and recall - source: https://github.com/helme/ecg_ptbxl_benchmarking - - Args: - y_true (npt.NDArray): True y labels - y_prob (npt.NDArray): Predicted y labels - thresholds (npt.NDArray): Thresholds - - Returns: - tuple[np.float_, np.float_]: Precision and recall - """ - y_true = np.repeat(y_true[None, :, :], len(thresholds), axis=0) - y_prob = np.repeat(y_prob[None, :, :], len(thresholds), axis=0) - y_pred = y_prob >= thresholds[:, None, None] - - # compute true positives - tp = np.sum(np.logical_and(y_true, y_pred), axis=2) - - # compute macro average precision handling all warnings - with np.errstate(divide="ignore", invalid="ignore"): - den = np.sum(y_pred, axis=2) - precision = tp / den - precision[den == 0] = np.nan - with warnings.catch_warnings(): # for nan slices - warnings.simplefilter("ignore", category=RuntimeWarning) - av_precision = np.nanmean(precision, axis=1) - - # compute macro average recall - recall = tp / np.sum(y_true, axis=2) - av_recall = np.mean(recall, axis=1) - - return av_precision, av_recall - - -def _one_hot(x: npt.NDArray, depth: int) -> npt.NDArray: - """Generate one hot encoding - - Args: - x (npt.NDArray): Categories - depth (int): Depth - - Returns: - npt.NDArray: One hot encoded - """ - x_one_hot = np.zeros((x.size, depth)) - x_one_hot[np.arange(x.size), x] = 1 - return x_one_hot - - -def multi_f1(y_true: npt.NDArray, y_prob: npt.NDArray) -> npt.NDArray | float: - """Compute multi-class F1 - - Args: - y_true (npt.NDArray): True y labels - y_prob (npt.NDArray): Predicted y labels - - Returns: - npt.NDArray|float: F1 score - """ - return f1(y_true, y_prob, multiclass=True, threshold=0.5) diff --git a/heartkit/models/__init__.py b/heartkit/models/__init__.py index 0838e508..e8ffb8cd 100644 --- a/heartkit/models/__init__.py +++ b/heartkit/models/__init__.py @@ -1,16 +1,30 @@ +"""ModelFactory is used to store and retrieve model generators. +key (str): Model name slug (e.g. "unet") +value (ModelFactoryItem): Model generator +""" + from typing import Protocol import keras import neuralspot_edge as nse -from ..utils import ItemFactory - class ModelFactoryItem(Protocol): + """ModelFactoryItem is a protocol for model factory items. + + Args: + x (keras.KerasTensor): Input tensor + params (dict): Model parameters + num_classes (int): Number of classes + + Returns: + keras.Model: Model + """ + def __call__(self, x: keras.KerasTensor, params: dict, num_classes: int) -> keras.Model: ... -ModelFactory = ItemFactory[ModelFactoryItem].shared("HKModelFactory") +ModelFactory = nse.utils.ItemFactory[ModelFactoryItem].shared("HKModelFactory") ModelFactory.register("unet", nse.models.unet.unet_from_object) ModelFactory.register("unext", nse.models.unext.unext_from_object) diff --git a/heartkit/rpc/__init__.py b/heartkit/rpc/__init__.py index a3050642..f42d5012 100644 --- a/heartkit/rpc/__init__.py +++ b/heartkit/rpc/__init__.py @@ -1,11 +1,11 @@ +import neuralspot_edge as nse + from . import GenericDataOperations_EvbToPc as evb2pc from . import GenericDataOperations_PcToEvb as pc2evb from . import utils -from .backends import DemoBackend, EvbBackend, PcBackend - -from ..utils import create_factory +from .backends import HKInferenceBackend, EvbBackend, PcBackend -BackendFactory = create_factory("HKDemoBackend", DemoBackend) +BackendFactory = nse.utils.create_factory("HKDemoBackend", HKInferenceBackend) BackendFactory.register("pc", PcBackend) BackendFactory.register("evb", EvbBackend) diff --git a/heartkit/rpc/backends.py b/heartkit/rpc/backends.py index 95772014..07c51ed0 100644 --- a/heartkit/rpc/backends.py +++ b/heartkit/rpc/backends.py @@ -6,13 +6,12 @@ import numpy as np import numpy.typing as npt -from ..defines import HKDemoParams -from ..utils import setup_logger +from ..defines import HKTaskParams from . import GenericDataOperations_PcToEvb as pc2evb from . import erpc from .utils import get_serial_transport -logger = setup_logger(__name__) +logger = nse.utils.setup_logger(__name__) class RpcCommands(IntEnum): @@ -25,10 +24,10 @@ class RpcCommands(IntEnum): PERFORM_INFERENCE = 4 -class DemoBackend(abc.ABC): +class HKInferenceBackend(abc.ABC): """Demo backend base class""" - def __init__(self, params: HKDemoParams) -> None: + def __init__(self, params: HKTaskParams) -> None: self.params = params def open(self): @@ -52,10 +51,10 @@ def get_outputs(self) -> npt.NDArray: raise NotImplementedError -class EvbBackend(DemoBackend): +class EvbBackend(HKInferenceBackend): """Demo backend for EVB""" - def __init__(self, params: HKDemoParams) -> None: + def __init__(self, params: HKTaskParams) -> None: super().__init__(params=params) self._interpreter = None self._transport = None @@ -148,10 +147,10 @@ def get_outputs(self) -> npt.NDArray: return outputs -class PcBackend(DemoBackend): +class PcBackend(HKInferenceBackend): """Demo backend for PC""" - def __init__(self, params: HKDemoParams) -> None: + def __init__(self, params: HKTaskParams) -> None: super().__init__(params=params) self._inputs = None self._outputs = None diff --git a/heartkit/tasks/__init__.py b/heartkit/tasks/__init__.py index d2a1071c..ccc1c26b 100644 --- a/heartkit/tasks/__init__.py +++ b/heartkit/tasks/__init__.py @@ -1,3 +1,7 @@ +import neuralspot_edge as nse + +from . import beat, denoise, diagnostic, foundation, rhythm, segmentation + from .beat import BeatTask, HKBeat from .denoise import DenoiseTask from .diagnostic import DiagnosticTask, HKDiagnostic @@ -6,10 +10,8 @@ from .segmentation import HKSegment, SegmentationTask from .task import HKTask from .translate import HKTranslate, TranslateTask -from .utils import load_datasets -from ..utils import create_factory -TaskFactory = create_factory(factory="HKTaskFactory", type=HKTask) +TaskFactory = nse.utils.create_factory(factory="HKTaskFactory", type=HKTask) TaskFactory.register("rhythm", RhythmTask) TaskFactory.register("beat", BeatTask) diff --git a/heartkit/tasks/beat/__init__.py b/heartkit/tasks/beat/__init__.py index 2069e8cf..0d826815 100644 --- a/heartkit/tasks/beat/__init__.py +++ b/heartkit/tasks/beat/__init__.py @@ -1,4 +1,4 @@ -from ...defines import HKDemoParams, HKExportParams, HKTestParams, HKTrainParams +from ...defines import HKTaskParams from ..task import HKTask from .defines import HKBeat from .demo import demo @@ -11,17 +11,24 @@ class BeatTask(HKTask): """HeartKit Beat Task""" @staticmethod - def train(params: HKTrainParams): + def description() -> str: + return ( + "This task is used to train, evaluate, and export beat models." + "Beat includes normal, pac, pvc, and other beats." + ) + + @staticmethod + def train(params: HKTaskParams): train(params) @staticmethod - def evaluate(params: HKTestParams): + def evaluate(params: HKTaskParams): evaluate(params) @staticmethod - def export(params: HKExportParams): + def export(params: HKTaskParams): export(params) @staticmethod - def demo(params: HKDemoParams): + def demo(params: HKTaskParams): demo(params) diff --git a/heartkit/tasks/beat/dataloaders/__init__.py b/heartkit/tasks/beat/dataloaders/__init__.py index d1c5e93c..a5ee9849 100644 --- a/heartkit/tasks/beat/dataloaders/__init__.py +++ b/heartkit/tasks/beat/dataloaders/__init__.py @@ -1 +1,10 @@ -from .icentia11k import icentia11k_data_generator, icentia11k_label_map +import neuralspot_edge as nse + +from ....datasets import HKDataloader + +from .icentia11k import Icentia11kDataloader +from .icentia_mini import IcentiaMiniDataloader + +BeatTaskFactory = nse.utils.create_factory(factory="HKBeatTaskFactory", type=HKDataloader) +BeatTaskFactory.register("icentia11k", Icentia11kDataloader) +BeatTaskFactory.register("icentia_mini", IcentiaMiniDataloader) diff --git a/heartkit/tasks/beat/dataloaders/icentia11k.py b/heartkit/tasks/beat/dataloaders/icentia11k.py index d36be08a..a349ca19 100644 --- a/heartkit/tasks/beat/dataloaders/icentia11k.py +++ b/heartkit/tasks/beat/dataloaders/icentia11k.py @@ -1,3 +1,4 @@ +import copy import random import functools from typing import Generator, Iterable @@ -5,9 +6,9 @@ import numpy as np import numpy.typing as npt import physiokit as pk +import neuralspot_edge as nse -from ....datasets.defines import PatientGenerator -from ....datasets.icentia11k import IcentiaBeat, IcentiaDataset +from ....datasets import HKDataloader, IcentiaDataset, IcentiaBeat from ..defines import HKBeat IcentiaBeatMap = { @@ -37,73 +38,30 @@ def beat_filter_func(i: int, blabels: npt.NDArray, beat: IcentiaBeat): # END MATCH -# END DEF - - -def icentia11k_label_map( - label_map: dict[int, int] | None = None, -) -> dict[int, int]: - """Get label map - - Args: - label_map (dict[int, int]|None): Label map - - Returns: - dict[int, int]: Label map - """ - return {k: label_map.get(v, -1) for (k, v) in IcentiaBeatMap.items()} - - -def icentia11k_data_generator( - patient_generator: PatientGenerator, - ds: IcentiaDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, - label_map: dict[int, int] | None = None, - label_type: str = "beat", - filter: bool = False, -) -> Generator[tuple[npt.NDArray, int], None, None]: - """Generate frames w/ rhythm labels (e.g. afib) using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: IcentiaDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - label_map (dict[int, int] | None, optional): Label map. Defaults to None. - label_type (str, optional): Label type. Defaults to "beat". - filter (bool, optional): Filter beats. Defaults to False. - Returns: - Generator[tuple[npt.NDArray, int], None, None]: Sample generator - """ - if target_rate is None: - target_rate = ds.sampling_rate - # END IF - - nlabel_threshold = 0.25 - blabel_padding = 20 - - # Target labels and mapping - tgt_labels = sorted(list(set((lbl for lbl in label_map.values() if lbl != -1)))) - label_key = ds.label_key(label_type) - - tgt_map = icentia11k_label_map(label_map=label_map) - num_classes = len(tgt_labels) - - # If samples_per_patient is a list, then it must be the same length as nclasses - if isinstance(samples_per_patient, Iterable): - samples_per_tgt = samples_per_patient - else: - num_per_tgt = int(max(1, samples_per_patient / num_classes)) - samples_per_tgt = num_per_tgt * [num_classes] - - input_size = int(np.round((ds.sampling_rate / target_rate) * frame_size)) - - # For each patient - for pt in patient_generator: - with ds.patient_data(pt) as segments: +class Icentia11kDataloader(HKDataloader): + def __init__(self, ds: IcentiaDataset, **kwargs): + """Icentia11k Dataloader for training beat tasks""" + super().__init__(ds=ds, **kwargs) + + # Update label map + if self.label_map: + self.label_map = {k: self.label_map[v] for (k, v) in IcentiaBeatMap.items() if v in self.label_map} + # END DEF + self.label_type = "beat" + # {PT: [label_idx: [segment, location]]} + self._pts_beat_map: dict[str, list[npt.NDArray]] = {} + + def _create_beat_map(self, patient_id: int, enable_filter: bool = False): + """On initial access, create beat map for patient to improve speed""" + nlabel_threshold = 0.25 + blabel_padding = 20 + + # Target labels and mapping + tgt_labels = sorted(set(self.label_map.values())) + label_key = self.ds.label_key(self.label_type) + num_classes = len(tgt_labels) + + with self.ds.patient_data(patient_id) as segments: # This maps segment index to segment key seg_map: list[str] = list(segments.keys()) @@ -127,14 +85,14 @@ def icentia11k_data_generator( # Capture all beat locations for beat in IcentiaBeat: # Skip if not in class map - beat_class = tgt_map.get(beat, -1) + beat_class = self.label_map.get(beat, -1) if beat_class < 0 or beat_class >= num_classes: continue # Get all beat type indices beat_idxs = np.where(blabels[blabel_padding:-blabel_padding, 1] == beat.value)[0] + blabel_padding - if filter: # Filter indices + if enable_filter: # Filter indices fn = functools.partial(beat_filter_func, blabels=blabels, beat=beat) beat_idxs = filter(fn, beat_idxs) # END IF @@ -142,11 +100,28 @@ def icentia11k_data_generator( # END FOR # END FOR pt_beat_map = [np.array(b) for b in pt_beat_map] + self._pts_beat_map[patient_id] = pt_beat_map + # END WITH + + def patient_data_generator( + self, + patient_id: int, + samples_per_patient: list[int], + ): + """Generate data for given patient id""" + input_size = int(np.ceil((self.ds.sampling_rate / self.sampling_rate) * self.frame_size)) + + with self.ds.patient_data(patient_id) as segments: + # This maps segment index to segment key + seg_map: list[str] = list(segments.keys()) + if patient_id not in self._pts_beat_map: + self._create_beat_map(patient_id) + pt_beat_map = self._pts_beat_map[patient_id] # Randomly select N samples of each target beat pt_segs_beat_idxs: list[tuple[int, int, int]] = [] for tgt_beat_idx, tgt_beats in enumerate(pt_beat_map): - tgt_count = min(samples_per_tgt[tgt_beat_idx], len(tgt_beats)) + tgt_count = min(samples_per_patient[tgt_beat_idx], len(tgt_beats)) tgt_idxs = np.random.choice(np.arange(len(tgt_beats)), size=tgt_count, replace=False) pt_segs_beat_idxs += [(tgt_beats[i][0], tgt_beats[i][1], tgt_beat_idx) for i in tgt_idxs] # END FOR @@ -154,16 +129,47 @@ def icentia11k_data_generator( # Shuffle all random.shuffle(pt_segs_beat_idxs) - # Yield selected samples for patient + # Grab selected samples for patient + samples = [] for seg_idx, beat_idx, beat in pt_segs_beat_idxs: frame_start = max(0, beat_idx - int(random.uniform(0.4722, 0.5278) * input_size)) frame_end = frame_start + input_size data = segments[seg_map[seg_idx]]["data"] x = np.nan_to_num(data[frame_start:frame_end]).astype(np.float32) - if ds.sampling_rate != target_rate: - x = pk.signal.resample_signal(x, ds.sampling_rate, target_rate, axis=0) + if self.ds.sampling_rate != self.sampling_rate: + x = pk.signal.resample_signal(x, self.ds.sampling_rate, self.sampling_rate, axis=0) + x = x[: self.frame_size] # truncate to frame size y = beat - yield x, y + samples.append((x, y)) # END FOR # END WITH - # END FOR + + # Yield samples + for x, y in samples: + yield x, y + # END FOR + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + """Generate data for given patient ids""" + # Target labels and mapping + tgt_labels = sorted(set(self.label_map.values())) + num_classes = len(tgt_labels) + + # If samples_per_patient is a list, then it must be the same length as nclasses + if isinstance(samples_per_patient, Iterable): + samples_per_tgt = samples_per_patient + else: + num_per_tgt = int(max(1, samples_per_patient / num_classes)) + samples_per_tgt = num_per_tgt * [num_classes] + + pt_ids = copy.deepcopy(patient_ids) + for pt_id in nse.utils.uniform_id_generator(pt_ids, repeat=True, shuffle=shuffle): + for x, y in self.patient_data_generator(pt_id, samples_per_tgt): + yield x, y + # END FOR + # END FOR diff --git a/heartkit/tasks/beat/dataloaders/icentia_mini.py b/heartkit/tasks/beat/dataloaders/icentia_mini.py new file mode 100644 index 00000000..9e130da1 --- /dev/null +++ b/heartkit/tasks/beat/dataloaders/icentia_mini.py @@ -0,0 +1,88 @@ +import copy +import random +from typing import Generator, Iterable + +import numpy as np +import numpy.typing as npt + +from ....datasets import HKDataloader, IcentiaMiniDataset, IcentiaMiniBeat +from ..defines import HKBeat + +IcentiaBeatMap = { + IcentiaMiniBeat.normal: HKBeat.normal, + IcentiaMiniBeat.pac: HKBeat.pac, + IcentiaMiniBeat.aberrated: HKBeat.pac, + IcentiaMiniBeat.pvc: HKBeat.pvc, +} + + +class IcentiaMiniDataloader(HKDataloader): + def __init__(self, ds: IcentiaMiniDataset, **kwargs): + """IcentiaMini Dataloader for training beat tasks""" + super().__init__(ds=ds, **kwargs) + # Update label map + if self.label_map: + self.label_map = {k: self.label_map[v] for (k, v) in IcentiaBeatMap.items() if v in self.label_map} + self.label_type = "beat" + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + """Generate data for given patient ids + + Args: + patient_ids (list[int]): Patient IDs + samples_per_patient (int | list[int]): Samples per patient + shuffle (bool, optional): Shuffle data. Defaults to False. + + Yields: + Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Data generator + """ + # Target labels and mapping + tgt_labels = sorted(list(set((lbl for lbl in self.label_map.values() if lbl != -1)))) + label_key = self.ds.label_key(self.label_type) + + num_classes = len(tgt_labels) + + # If samples_per_patient is a list, then it must be the same length as nclasses + if isinstance(samples_per_patient, Iterable): + samples_per_tgt = samples_per_patient + else: + num_per_tgt = int(max(1, samples_per_patient / num_classes)) + samples_per_tgt = num_per_tgt * [num_classes] + + input_size = int(np.ceil((self.ds.sampling_rate / self.sampling_rate) * self.frame_size)) + print(f"Input size: {input_size} {samples_per_tgt}") + + pt_ids = copy.deepcopy(patient_ids) + while True: + for pt_id in pt_ids: + with self.ds.patient_data(pt_id) as pt: + # data = pt["data"][:] # has shape (N, M, 1) + # blabels is a mask with shape (N, M) + blabels = pt[label_key][:] + + # Capture all beat locations + pt_beat_map = {} + for beat in IcentiaMiniBeat: + # Skip if not in class map + beat_class = self.label_map.get(beat, -1) + if beat_class < 0 or beat_class >= num_classes: + continue + # Get all beat type indices + rows, cols = np.where(blabels == beat.value) + # Zip rows and cols to form N, 2 array + pt_beat_map[beat_class] = np.array(list(zip(rows, cols))) + # END FOR + # END WITH + for samples in samples_per_patient: + for i in range(samples): + yield np.random.normal(size=(self.frame_size, 1)), np.random.randint(0, num_classes) + # END FOR + + # END FOR + if shuffle: + random.shuffle(pt_ids) diff --git a/heartkit/tasks/beat/datasets.py b/heartkit/tasks/beat/datasets.py index d553d0e8..84c65da2 100644 --- a/heartkit/tasks/beat/datasets.py +++ b/heartkit/tasks/beat/datasets.py @@ -1,346 +1,197 @@ -import functools -import logging -from pathlib import Path - -import keras import numpy as np -import numpy.typing as npt import tensorflow as tf +import neuralspot_edge as nse from ...datasets import ( HKDataset, - augment_pipeline, - preprocess_pipeline, - uniform_id_generator, -) -from ...datasets.dataloader import test_dataloader, train_val_dataloader -from ...defines import ( - AugmentationParams, - HKExportParams, - HKTestParams, - HKTrainParams, - PreprocessParams, + create_augmentation_pipeline, ) -from ...utils import resolve_template_path -from .dataloaders import icentia11k_data_generator, icentia11k_label_map - -logger = logging.getLogger(__name__) - - -def preprocess(x: npt.NDArray, preprocesses: list[PreprocessParams], sample_rate: float) -> npt.NDArray: - """Preprocess data pipeline - - Args: - x (npt.NDArray): Input data - preprocesses (list[PreprocessParams]): Preprocess parameters - sample_rate (float): Sample rate - - Returns: - tuple[npt.NDArray, npt.NDArray]: Preprocessed data - """ - return preprocess_pipeline(x, preprocesses=preprocesses, sample_rate=sample_rate) - - -def augment(x: npt.NDArray, augmentations: list[AugmentationParams], sample_rate: float) -> npt.NDArray: - """Augment data pipeline - - Args: - x (npt.NDArray): Input data - augmentations (list[AugmentationParams]): Augmentation parameters - sample_rate (float): Sample rate - - Returns: - npt.NDArray: Augmented data - """ - - return augment_pipeline(x=x, augmentations=augmentations, sample_rate=sample_rate) - - -def prepare( - x_y: tuple[npt.NDArray, int], - sample_rate: float, - preprocesses: list[PreprocessParams] | None, - augmentations: list[AugmentationParams] | None, - spec: tuple[tf.TensorSpec, tf.TensorSpec], - num_classes: int, -) -> tuple[npt.NDArray, npt.NDArray]: - """Prepare dataset - - Args: - x_y (tuple[npt.NDArray, int]): Input data and label - sample_rate (float): Sample rate - preprocesses (list[PreprocessParams]|None): Preprocess parameters - augmentations (list[AugmentationParams]|None): Augmentation parameters - spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - num_classes (int): Number of classes - - Returns: - tuple[npt.NDArray, npt.NDArray]: Prepared data - """ - x, y = x_y[0].copy(), x_y[1] +from ...datasets.dataloader import HKDataloader +from ...defines import HKTaskParams, NamedParams - if augmentations: - x = augment(x, augmentations, sample_rate) - # END IF +from .dataloaders import BeatTaskFactory - if preprocesses: - x = preprocess(x, preprocesses, sample_rate) - # END IF +logger = nse.utils.setup_logger(__name__) - x = x.reshape(spec[0].shape) - y = keras.ops.one_hot(y, num_classes) - return x, y - -def get_ds_label_map(ds: HKDataset, label_map: dict[int, int] | None = None) -> dict[int, int]: - """Get label map for dataset - - Args: - ds (HKDataset): Dataset - label_map (dict[int, int]|None): Label map - - Returns: - dict[int, int]: Label map - """ - match ds.name: - case "icentia11k": - return icentia11k_label_map(label_map=label_map) - case _: - raise ValueError(f"Dataset {ds.name} not supported") - # END MATCH - - -def get_data_generator( - ds: HKDataset, - frame_size: int, - samples_per_patient: int, - target_rate: int, - label_map: dict[int, int] | None = None, -): - """Get task data generator for dataset +def create_data_pipeline( + ds: tf.data.Dataset, + sampling_rate: int, + batch_size: int, + buffer_size: int | None = None, + augmentations: list[NamedParams] | None = None, + num_classes: int = 2, +) -> tf.data.Dataset: + """Create a beat task data pipeline for given dataset. Args: - ds (HKDataset): Dataset - frame_size (int): Frame size - samples_per_patient (int): Samples per patient - target_rate (int): Target rate - label_map (dict[int, int] | None, optional): Label map. Defaults to None. + ds (tf.data.Dataset): Input dataset. + sampling_rate (int): Sampling rate of the dataset. + batch_size (int): Batch size. + buffer_size (int, optional): Buffer size for shuffling. Defaults to None. + augmentations (list[NamedParams], optional): List of augmentations. Defaults to None. + num_classes (int, optional): Number of classes. Defaults to 2. Returns: - callable: Data generator + tf.data.Dataset: Data pipeline. """ - match ds.name: - case "icentia11k": - data_generator = icentia11k_data_generator - case _: - raise ValueError(f"Dataset {ds.name} not supported") - # END MATCH - return functools.partial( - data_generator, - ds=ds, - frame_size=frame_size, - samples_per_patient=samples_per_patient, - target_rate=target_rate, - label_map=label_map, + if buffer_size: + ds = ds.shuffle( + buffer_size=buffer_size, + reshuffle_each_iteration=True, + ) + if batch_size: + ds = ds.batch( + batch_size=batch_size, + drop_remainder=True, + num_parallel_calls=tf.data.AUTOTUNE, + ) + augmenter = create_augmentation_pipeline(augmentations, sampling_rate=sampling_rate) + ds = ( + ds.map( + lambda data, labels: { + "data": tf.cast(data, "float32"), + "labels": tf.one_hot(labels, num_classes), + }, + num_parallel_calls=tf.data.AUTOTUNE, + ) + .map( + augmenter, + num_parallel_calls=tf.data.AUTOTUNE, + ) + .map( + lambda data: (data["data"], data["labels"]), + num_parallel_calls=tf.data.AUTOTUNE, + ) ) - -def get_ds_label_type(ds: HKDataset) -> str: - """Get label type for dataset - - Args: - ds (HKDataset): Dataset - - Returns: - str: Label type - """ - return "beat" if ds.name == "icentia11k" else "scp" - - -def resolve_ds_cache_path(fpath: Path | None, ds: HKDataset, task: str, frame_size: int, sample_rate: int): - """Resolve dataset cache path - - Args: - fpath (Path|None): File path - ds (HKDataset): Dataset - task (str): Task - frame_size (int): Frame size - sample_rate (int): Sampling rate - - Returns: - Path|None: Resolved path - """ - if not fpath: - return None - return resolve_template_path( - fpath=fpath, - dataset=ds.name, - task=task, - frame_size=frame_size, - sampling_rate=sample_rate, - ) + return ds.prefetch(tf.data.AUTOTUNE) def load_train_datasets( datasets: list[HKDataset], - params: HKTrainParams, - ds_spec: tuple[tf.TensorSpec, tf.TensorSpec], + params: HKTaskParams, ) -> tuple[tf.data.Dataset, tf.data.Dataset]: - """Load training and validation datasets + """Load training and validation tf.data.Datasets pipeline. + + !!! note + if val_size or val_steps_per_epoch is given, then validation dataset will be + a fixed cached size. Otherwise, it will be a unbounded dataset generator. In + the latter case, a length will need to be passed to functions like `model.fit`. Args: - datasets (list[HKDataset]): Datasets - params (HKTrainParams): Training parameters - ds_spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec + datasets (list[HKDataset]): List of datasets. + params (HKTaskParams): Training parameters. Returns: - tuple[tf.data.Dataset, tf.data.Dataset]: Train and validation datasets + tuple[tf.data.Dataset, tf.data.Dataset]: Training and validation datasets """ - id_generator = functools.partial(uniform_id_generator, repeat=True) - train_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, - ) - - val_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=None, - spec=ds_spec, - num_classes=params.num_classes, - ) - train_datasets = [] val_datasets = [] for ds in datasets: - val_file = resolve_ds_cache_path( - params.val_file, - ds=ds, - task="beat", - frame_size=params.frame_size, - sample_rate=params.sampling_rate, - ) - data_generator = get_data_generator( + dataloader: HKDataloader = BeatTaskFactory.get(ds.name)( ds=ds, frame_size=params.frame_size, - samples_per_patient=params.samples_per_patient, - target_rate=params.sampling_rate, + sampling_rate=params.sampling_rate, label_map=params.class_map, ) - - train_ds, val_ds = train_val_dataloader( - ds=ds, - spec=ds_spec, - data_generator=data_generator, - id_generator=id_generator, + train_patients, val_patients = dataloader.split_train_val_patients( train_patients=params.train_patients, val_patients=params.val_patients, - val_pt_samples=params.val_samples_per_patient, - val_file=val_file, - val_size=params.val_size, - label_map=get_ds_label_map(ds, label_map=params.class_map), - label_type=get_ds_label_type(ds), - preprocess=train_prepare, - val_preprocess=val_prepare, - num_workers=params.data_parallelism, + ) + + train_ds = dataloader.create_dataloader( + patient_ids=train_patients, samples_per_patient=params.samples_per_patient, shuffle=True + ) + + val_ds = dataloader.create_dataloader( + patient_ids=val_patients, samples_per_patient=params.val_samples_per_patient, shuffle=False ) train_datasets.append(train_ds) val_datasets.append(val_ds) # END FOR - ds_weights = np.array([d.weight for d in params.datasets]) - ds_weights = ds_weights / ds_weights.sum() + ds_weights = None + if params.dataset_weights: + ds_weights = np.array(params.dataset_weights) + ds_weights = ds_weights / ds_weights.sum() train_ds = tf.data.Dataset.sample_from_datasets(train_datasets, weights=ds_weights) val_ds = tf.data.Dataset.sample_from_datasets(val_datasets, weights=ds_weights) # Shuffle and batch datasets for training - train_ds = ( - train_ds.shuffle( - buffer_size=params.buffer_size, - reshuffle_each_iteration=True, - ) - .batch( - batch_size=params.batch_size, - drop_remainder=False, - num_parallel_calls=tf.data.AUTOTUNE, - ) - .prefetch(buffer_size=tf.data.AUTOTUNE) + train_ds = create_data_pipeline( + ds=train_ds, + sampling_rate=params.sampling_rate, + batch_size=params.batch_size, + buffer_size=params.buffer_size, + augmentations=params.augmentations + params.preprocesses, + num_classes=params.num_classes, ) - val_ds = val_ds.batch( + + val_ds = create_data_pipeline( + ds=val_ds, + sampling_rate=params.sampling_rate, batch_size=params.batch_size, - drop_remainder=True, - num_parallel_calls=tf.data.AUTOTUNE, + augmentations=params.preprocesses, + num_classes=params.num_classes, ) + + # If given fixed val size or steps, then capture and cache + val_steps_per_epoch = params.val_size // params.batch_size if params.val_size else params.val_steps_per_epoch + if val_steps_per_epoch: + logger.info(f"Validation steps per epoch: {val_steps_per_epoch}") + val_ds = val_ds.take(val_steps_per_epoch).cache() + return train_ds, val_ds def load_test_dataset( datasets: list[HKDataset], - params: HKTestParams | HKExportParams, - ds_spec: tuple[tf.TensorSpec, tf.TensorSpec], + params: HKTaskParams, ) -> tf.data.Dataset: - """Load test dataset + """Load test tf.data.Dataset. Args: - datasets (list[HKDataset]): Datasets - params (HKTestParams|HKExportParams): Test parameters - ds_spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec + datasets (list[HKDataset]): List of datasets. + params (HKTaskParams): Test parameters. Returns: tf.data.Dataset: Test dataset """ - - id_generator = functools.partial(uniform_id_generator, repeat=True) - test_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=None, # params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, - ) test_datasets = [] for ds in datasets: - test_file = resolve_ds_cache_path( - fpath=params.test_file, - ds=ds, - task="beat", - frame_size=params.frame_size, - sample_rate=params.sampling_rate, - ) - data_generator = get_data_generator( + dataloader: HKDataloader = BeatTaskFactory.get(ds.name)( ds=ds, frame_size=params.frame_size, - samples_per_patient=params.test_samples_per_patient, - target_rate=params.sampling_rate, + sampling_rate=params.sampling_rate, label_map=params.class_map, ) - - test_ds = test_dataloader( - ds=ds, - spec=ds_spec, - data_generator=data_generator, - id_generator=id_generator, - test_patients=params.test_patients, - test_file=test_file, - label_map=get_ds_label_map(ds, label_map=params.class_map), - label_type=get_ds_label_type(ds), - preprocess=test_prepare, - num_workers=params.data_parallelism, + test_patients = dataloader.test_patient_ids(params.test_patients) + test_ds = dataloader.create_dataloader( + patient_ids=test_patients, + samples_per_patient=params.test_samples_per_patient, + shuffle=False, ) test_datasets.append(test_ds) # END FOR - ds_weights = np.array([d.weight for d in params.datasets]) - ds_weights = ds_weights / ds_weights.sum() + ds_weights = None + if params.dataset_weights: + ds_weights = np.array(params.dataset_weights) + ds_weights = ds_weights / ds_weights.sum() test_ds = tf.data.Dataset.sample_from_datasets(test_datasets, weights=ds_weights) + test_ds = create_data_pipeline( + ds=test_ds, + sampling_rate=params.sampling_rate, + batch_size=params.batch_size, + augmentations=params.preprocesses, + num_classes=params.num_classes, + ) + + if params.test_size: + batch_size = getattr(params, "batch_size", 1) + test_ds = test_ds.take(params.test_size // batch_size).cache() - # END WITH return test_ds diff --git a/heartkit/tasks/beat/demo.py b/heartkit/tasks/beat/demo.py index 57061723..664ac262 100644 --- a/heartkit/tasks/beat/demo.py +++ b/heartkit/tasks/beat/demo.py @@ -1,23 +1,19 @@ import random +import keras import numpy as np import numpy.typing as npt import physiokit as pk import plotly.graph_objects as go -import tensorflow as tf from plotly.subplots import make_subplots -from rich.console import Console from tqdm import tqdm +import neuralspot_edge as nse -from ...datasets import IcentiaDataset, PtbxlDataset, uniform_id_generator -from ...defines import HKDemoParams +from ...datasets import IcentiaDataset, PtbxlDataset, DatasetFactory, create_augmentation_pipeline +from ...defines import HKTaskParams from ...rpc import BackendFactory -from ...utils import setup_logger -from ..utils import load_datasets -from .datasets import preprocess -console = Console() -logger = setup_logger(__name__) +logger = nse.utils.setup_logger(__name__) def get_ptbxl_patient_data( @@ -38,7 +34,7 @@ def get_ptbxl_patient_data( data = h5["data"][:] blabels = h5[ds.label_key("beat")][:, 0] * 5 # Stored in 100Hz # END WITH - input_size = int(np.round((ds.sampling_rate / target_rate) * frame_size)) + input_size = int(np.ceil((ds.sampling_rate / target_rate) * frame_size)) lead = random.choice(ds.leads) start = np.random.randint(0, data.shape[1] - input_size) x = data[lead, start : start + input_size].squeeze() @@ -47,6 +43,7 @@ def get_ptbxl_patient_data( if ds.sampling_rate != target_rate: ratio = target_rate / ds.sampling_rate x = pk.signal.resample_signal(x, ds.sampling_rate, target_rate, axis=0) + x = x[:frame_size] # truncate to frame size y = (y * ratio).astype(np.int32) # END IF return x, y @@ -70,7 +67,7 @@ def get_icentia11k_patient_data( if target_rate is None: target_rate = ds.sampling_rate - input_size = int(np.round((ds.sampling_rate / target_rate) * frame_size)) + input_size = int(np.ceil((ds.sampling_rate / target_rate) * frame_size)) label_key = ds.label_key("beat") with ds.patient_data(patient_id) as segments: @@ -87,6 +84,7 @@ def get_icentia11k_patient_data( if ds.sampling_rate != target_rate: ratio = target_rate / ds.sampling_rate x = pk.signal.resample_signal(x, ds.sampling_rate, target_rate, axis=0) + x = x[:frame_size] # truncate to frame size y = (y * ratio).astype(np.int32) # END IF @@ -94,11 +92,11 @@ def get_icentia11k_patient_data( return x, y -def demo(params: HKDemoParams): +def demo(params: HKTaskParams): """Run demo on model. Args: - params (HKDemoParams): Demo parameters + params (HKTaskParams): Demo parameters """ bg_color = "rgba(38,42,50,1.0)" @@ -112,21 +110,15 @@ def demo(params: HKDemoParams): params.demo_size = params.demo_size or 20 * params.sampling_rate # Load backend inference engine - runner = BackendFactory.create(params.backend, params=params) + runner = BackendFactory.get(params.backend)(params=params) # Load data - # classes = sorted(list(set(params.class_map.values()))) class_names = params.class_names or [f"Class {i}" for i in range(params.num_classes)] feat_shape = (params.frame_size, 1) - # class_shape = (params.num_classes,) - # ds_spec = ( - # tf.TensorSpec(shape=feat_shape, dtype=tf.float32), - # tf.TensorSpec(shape=class_shape, dtype=tf.int32), - # ) + dsets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - dsets = load_datasets(datasets=params.datasets) ds = random.choice(dsets) if ds.name == "ptbxl": pt_id = random.choice(ds.get_test_patient_ids()) @@ -147,7 +139,7 @@ def demo(params: HKDemoParams): else: # Need to manually locate peaks, compute ds_gen = ds.signal_generator( - patient_generator=uniform_id_generator(ds.get_test_patient_ids(), repeat=False), + patient_generator=nse.utils.uniform_id_generator(ds.get_test_patient_ids(), repeat=False), frame_size=params.demo_size, samples_per_patient=5, target_rate=params.sampling_rate, @@ -159,7 +151,11 @@ def demo(params: HKDemoParams): # END IF rri = pk.ecg.compute_rr_intervals(peaks) - # mask = pk.ecg.filter_rr_intervals(rri, sample_rate=params.sampling_rate) + + augmenter = create_augmentation_pipeline( + params.augmentations + params.preprocesses, + sampling_rate=params.sampling_rate, + ) # Run inference runner.open() @@ -174,16 +170,12 @@ def demo(params: HKDemoParams): y_prob[i] = 0.0 continue xx = x[start:stop] - xx = preprocess( - x[start:stop], - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - ) xx = xx.reshape(feat_shape) + xx = augmenter(xx) runner.set_inputs(xx) runner.perform_inference() yy = runner.get_outputs() - yy = tf.nn.softmax(yy).numpy() + yy = keras.ops.softmax(yy).numpy() y_pred[i] = np.argmax(yy, axis=-1) y_prob[i] = yy[y_pred[i]] if y_prob[i] < params.threshold: diff --git a/heartkit/tasks/beat/evaluate.py b/heartkit/tasks/beat/evaluate.py index 92b41317..a4c8ec7b 100644 --- a/heartkit/tasks/beat/evaluate.py +++ b/heartkit/tasks/beat/evaluate.py @@ -1,51 +1,34 @@ -import logging import os import numpy as np import keras import neuralspot_edge as nse -import tensorflow as tf -from sklearn.metrics import f1_score -from ...defines import HKTestParams -from ...utils import set_random_seed, setup_logger -from ..utils import load_datasets +from ...defines import HKTaskParams +from ...datasets import DatasetFactory from .datasets import load_test_dataset -logger = setup_logger(__name__) - -def evaluate(params: HKTestParams): - """Evaluate model +def evaluate(params: HKTaskParams): + """Evaluate beat task model on given parameters. Args: - params (HKTestParams): Evaluation parameters + params (HKTaskParams): Evaluation parameters """ - params.seed = set_random_seed(params.seed) - logger.debug(f"Random seed {params.seed}") - os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "test.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "test.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) + params.seed = nse.utils.set_random_seed(params.seed) + logger.debug(f"Random seed {params.seed}") - # classes = sorted(list(set(params.class_map.values()))) class_names = params.class_names or [f"Class {i}" for i in range(params.num_classes)] - feat_shape = (params.frame_size, 1) - class_shape = (params.num_classes,) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype="float32"), - tf.TensorSpec(shape=class_shape, dtype="int32"), - ) - - datasets = load_datasets(datasets=params.datasets) - - test_ds = load_test_dataset(datasets=datasets, params=params, ds_spec=ds_spec) - test_x, test_y = next(test_ds.batch(params.test_size).as_numpy_iterator()) + test_ds = load_test_dataset(datasets=datasets, params=params) + test_x = np.concatenate([x for x, _ in test_ds.as_numpy_iterator()]) + test_y = np.concatenate([y for _, y in test_ds.as_numpy_iterator()]) logger.debug("Loading model") model = nse.models.load_model(params.model_file) @@ -61,31 +44,28 @@ def evaluate(params: HKTestParams): # Summarize results logger.debug("Testing Results") - test_acc = np.sum(y_pred == y_true) / len(y_true) - test_f1 = f1_score(y_true, y_pred, average="weighted") - - logger.debug(f"[TEST SET] ACC={test_acc:.2%}, F1={test_f1:.2%}") + rst = model.evaluate(test_x, test_y, verbose=params.verbose, return_dict=True) + logger.info("[TEST SET] " + ", ".join([f"{k.upper()}={v:.2%}" for k, v in rst.items()])) if params.num_classes == 2: roc_path = params.job_dir / "roc_auc_test.png" - nse.plotting.roc.roc_auc_plot(y_true, y_prob[:, 1], labels=class_names, save_path=roc_path) + nse.plotting.roc_auc_plot(y_true, y_prob[:, 1], labels=class_names, save_path=roc_path) # END IF # If threshold given, only count predictions above threshold if params.threshold: prev_numel = len(y_true) - y_prob, y_pred, y_true = nse.metrics.threshold.threshold_predictions(y_prob, y_pred, y_true, params.threshold) - drop_perc = 1 - len(y_true) / prev_numel - test_acc = np.sum(y_pred == y_true) / len(y_true) - test_f1 = f1_score(y_true, y_pred, average="weighted") - logger.debug(f"[TEST SET] THRESH={params.threshold:0.2%}, DROP={drop_perc:.2%}") - logger.debug(f"[TEST SET] ACC={test_acc:.2%}, F1={test_f1:.2%}") + indices = nse.metrics.threshold.get_predicted_threshold_indices(y_prob, y_pred, params.threshold) + test_x, test_y = test_x[indices], test_y[indices] + y_true, y_pred = y_true[indices], y_pred[indices] + rst = model.evaluate(test_x, test_y, verbose=params.verbose, return_dict=True) + logger.info(f"[TEST SET] THRESH={params.threshold:0.2%}, DROP={1 - len(indices) / prev_numel:.2%}") + logger.info("[TEST SET] " + ", ".join([f"{k.upper()}={v:.2%}" for k, v in rst.items()])) # END IF cm_path = params.job_dir / "confusion_matrix_test.png" - - nse.plotting.cm.confusion_matrix_plot(y_true, y_pred, labels=class_names, save_path=cm_path, normalize="true") - nse.plotting.cm.px_plot_confusion_matrix( + nse.plotting.confusion_matrix_plot(y_true, y_pred, labels=class_names, save_path=cm_path, normalize="true") + nse.plotting.px_plot_confusion_matrix( y_true, y_pred, labels=class_names, diff --git a/heartkit/tasks/beat/export.py b/heartkit/tasks/beat/export.py index b72faa9f..76021d5f 100644 --- a/heartkit/tasks/beat/export.py +++ b/heartkit/tasks/beat/export.py @@ -1,70 +1,47 @@ -import logging import os import shutil import keras -import neuralspot_edge as nse import numpy as np -import tensorflow as tf -from sklearn.metrics import f1_score +import neuralspot_edge as nse -from ...defines import HKExportParams -from ...utils import setup_logger -from ..utils import load_datasets +from ...defines import HKTaskParams +from ...datasets import DatasetFactory from .datasets import load_test_dataset -logger = setup_logger(__name__) - -def export(params: HKExportParams): - """Export model +def export(params: HKTaskParams): + """Export beat task model with given parameters. Args: - params (HKExportParams): Deployment parameters + params (HKTaskParams): Deployment parameters """ - os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "export.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "export.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) - tfl_model_path = params.job_dir / "model.tflite" tflm_model_path = params.job_dir / "model_buffer.h" feat_shape = (params.frame_size, 1) - class_shape = (params.num_classes,) - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype="float32"), - tf.TensorSpec(shape=class_shape, dtype="int32"), - ) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - datasets = load_datasets(datasets=params.datasets) + test_ds = load_test_dataset(datasets=datasets, params=params) + test_x = np.concatenate([x for x, _ in test_ds.as_numpy_iterator()]) + test_y = np.concatenate([y for _, y in test_ds.as_numpy_iterator()]) - test_ds = load_test_dataset(datasets=datasets, params=params, ds_spec=ds_spec) - test_x, test_y = next(test_ds.batch(params.test_size).as_numpy_iterator()) - - # Load model and set fixed batch size of 1 + # Load model logger.debug("Loading trained model") model = nse.models.load_model(params.model_file) + # Add softmax layer if required if not params.use_logits and not isinstance(model.layers[-1], keras.layers.Softmax): - last_layer_name = model.layers[-1].name - - def call_function(layer, *args, **kwargs): - out = layer(*args, **kwargs) - if layer.name == last_layer_name: - out = keras.layers.Softmax()(out) - return out - - # END DEF - model_clone = keras.models.clone_model(model, call_function=call_function) - model_clone.set_weights(model.get_weights()) - model = model_clone + model = nse.models.append_layers(model, layers=[keras.layers.Softmax()], copy_weights=True) # END IF - inputs = keras.Input(shape=ds_spec[0].shape, batch_size=1, name="input", dtype=ds_spec[0].dtype.name) + + # Fix batch size to 1 + inputs = keras.Input(shape=feat_shape, batch_size=1, name="input", dtype="float32") model(inputs) flops = nse.metrics.flops.get_flops(model, batch_size=1, fpath=params.job_dir / "model_flops.log") @@ -72,8 +49,9 @@ def call_function(layer, *args, **kwargs): logger.debug(f"Model requires {flops/1e6:0.2f} MFLOPS") logger.debug(f"Converting model to TFLite (quantization={params.quantization.mode})") - tflite = nse.converters.tflite.TfLiteKerasConverter(model=model) - tflite_content = tflite.convert( + converter = nse.converters.tflite.TfLiteKerasConverter(model=model) + + tflite_content = converter.convert( test_x=test_x, quantization=params.quantization.format, io_type=params.quantization.io_type, @@ -82,41 +60,48 @@ def call_function(layer, *args, **kwargs): ) if params.quantization.debug: - quant_df = tflite.debug_quantization() + quant_df = converter.debug_quantization() quant_df.to_csv(params.job_dir / "quant.csv") # Save TFLite model logger.debug(f"Saving TFLite model to {tfl_model_path}") - tflite.export(tfl_model_path) + converter.export(tfl_model_path) # Save TFLM model logger.debug(f"Saving TFL micro model to {tflm_model_path}") - tflite.export_header(tflm_model_path, name=params.tflm_var_name) - tflite.cleanup() + converter.export_header(tflm_model_path, name=params.tflm_var_name) + converter.cleanup() tflite = nse.interpreters.tflite.TfLiteKerasInterpreter(tflite_content) tflite.compile() # Verify TFLite results match TF results on example data + metrics = [ + keras.metrics.CategoricalCrossentropy(name="loss", from_logits=params.use_logits), + keras.metrics.CategoricalAccuracy(name="acc"), + keras.metrics.F1Score(name="f1", average="weighted"), + ] + + if params.val_metric not in [m.name for m in metrics]: + raise ValueError(f"Metric {params.val_metric} not supported") + logger.debug("Validating model results") - y_true = np.argmax(test_y, axis=-1) - y_pred_tf = np.argmax(model.predict(test_x), axis=-1) - y_pred_tfl = np.argmax(tflite.predict(x=test_x), axis=-1) + y_true = test_y + y_pred_tf = model.predict(test_x) + y_pred_tfl = tflite.predict(x=test_x) - tf_acc = np.sum(y_true == y_pred_tf) / y_true.size - tf_f1 = f1_score(y_true, y_pred_tf, average="weighted") - logger.debug(f"[TF SET] ACC={tf_acc:.2%}, F1={tf_f1:.2%}") + tf_rst = nse.metrics.compute_metrics(metrics, y_true, y_pred_tf) + tfl_rst = nse.metrics.compute_metrics(metrics, y_true, y_pred_tfl) + logger.info("[TF METRICS] " + " ".join([f"{k.upper()}={v:.2%}" for k, v in tf_rst.items()])) + logger.info("[TFL METRICS] " + " ".join([f"{k.upper()}={v:.2%}" for k, v in tfl_rst.items()])) - tfl_acc = np.sum(y_true == y_pred_tfl) / y_true.size - tfl_f1 = f1_score(y_true, y_pred_tfl, average="weighted") - logger.debug(f"[TFL SET] ACC={tfl_acc:.2%}, F1={tfl_f1:.2%}") + metric_diff = abs(tf_rst[params.val_metric] - tfl_rst[params.val_metric]) # Check accuracy hit - tfl_acc_drop = max(0, tf_acc - tfl_acc) - if params.val_acc_threshold is not None and (1 - tfl_acc_drop) < params.val_acc_threshold: - logger.warning(f"TFLite accuracy dropped by {tfl_acc_drop:0.2%}") - elif params.val_acc_threshold: - logger.debug(f"Validation passed ({tfl_acc_drop:0.2%})") + if params.val_metric_threshold is not None and metric_diff > params.val_metric_threshold: + logger.warning(f"TFLite accuracy dropped by {metric_diff:0.2%}") + elif params.val_metric_threshold: + logger.info(f"Validation passed ({metric_diff:0.2%})") if params.tflm_file and tflm_model_path != params.tflm_file: logger.debug(f"Copying TFLM header to {params.tflm_file}") diff --git a/heartkit/tasks/beat/train.py b/heartkit/tasks/beat/train.py index 6bf09f7b..89c708ad 100644 --- a/heartkit/tasks/beat/train.py +++ b/heartkit/tasks/beat/train.py @@ -1,122 +1,82 @@ -import logging import os import keras import neuralspot_edge as nse import numpy as np import sklearn.utils -import tensorflow as tf import wandb -from rich.console import Console -from sklearn.metrics import f1_score from wandb.keras import WandbMetricsLogger, WandbModelCheckpoint -from ...defines import HKTrainParams -from ...utils import env_flag, set_random_seed, setup_logger -from ..utils import load_datasets +from ...defines import HKTaskParams +from ...datasets import DatasetFactory +from ...models import ModelFactory from .datasets import load_train_datasets -from .utils import create_model -console = Console() -logger = setup_logger(__name__) - -def train(params: HKTrainParams): - """Train model +def train(params: HKTaskParams): + """Train beat task model with given parameters. Args: - params (HKTrainParams): Training parameters + params (HKTaskParams): Training parameters """ - params.seed = set_random_seed(params.seed) - logger.debug(f"Random seed {params.seed}") - os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "train.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "train.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) + params.seed = nse.utils.set_random_seed(params.seed) + logger.debug(f"Random seed {params.seed}") with open(params.job_dir / "train_config.json", "w", encoding="utf-8") as fp: fp.write(params.model_dump_json(indent=2)) - if env_flag("WANDB"): - wandb.init( - project=params.project, - entity="ambiq", - dir=params.job_dir, - ) + if nse.utils.env_flag("WANDB"): + wandb.init(project=params.project, entity="ambiq", dir=params.job_dir) wandb.config.update(params.model_dump()) # END IF - classes = sorted(list(set(params.class_map.values()))) + classes = sorted(set(params.class_map.values())) class_names = params.class_names or [f"Class {i}" for i in range(params.num_classes)] feat_shape = (params.frame_size, 1) - class_shape = (params.num_classes,) - - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype="float32"), - tf.TensorSpec(shape=class_shape, dtype="int32"), - ) - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - train_ds, val_ds = load_train_datasets( - datasets=datasets, - params=params, - ds_spec=ds_spec, - ) + train_ds, val_ds = load_train_datasets(datasets=datasets, params=params) - test_labels = [label.numpy() for _, label in val_ds] - y_true = np.argmax(np.concatenate(test_labels), axis=-1).flatten() + y_true = np.concatenate([y for _, y in val_ds.as_numpy_iterator()]) + y_true = np.argmax(y_true, axis=-1) class_weights = 0.25 if params.class_weights == "balanced": class_weights = sklearn.utils.compute_class_weight("balanced", classes=np.array(classes), y=y_true) class_weights = (class_weights + class_weights.mean()) / 2 # Smooth out + class_weights = class_weights.tolist() # END IF logger.debug(f"Class weights: {class_weights}") - inputs = keras.Input( - shape=ds_spec[0].shape, - batch_size=None, - name="input", - dtype=ds_spec[0].dtype.name, - ) + inputs = keras.Input(shape=feat_shape, name="input", dtype="float32") + if params.resume and params.model_file: logger.debug(f"Loading model from file {params.model_file}") - model = keras.models.load_model(params.model_file) params.model_file = None else: logger.debug("Creating model from scratch") - model = create_model( - inputs, + model = ModelFactory.get(params.architecture.name)( + x=inputs, + params=params.architecture.params, num_classes=params.num_classes, - architecture=params.architecture, ) # END IF - if params.lr_cycles > 1: - scheduler = keras.optimizers.schedules.CosineDecayRestarts( - initial_learning_rate=params.lr_rate, - first_decay_steps=int(0.1 * params.steps_per_epoch * params.epochs), - t_mul=1.65 / (0.1 * params.lr_cycles * (params.lr_cycles - 1)), - m_mul=0.4, - ) - else: - scheduler = keras.optimizers.schedules.CosineDecay( - initial_learning_rate=params.lr_rate, - decay_steps=params.steps_per_epoch * params.epochs, - ) - # END IF - optimizer = keras.optimizers.Adam(scheduler) - loss = keras.losses.CategoricalFocalCrossentropy(from_logits=True, alpha=class_weights) - metrics = [ - keras.metrics.CategoricalAccuracy(name="acc"), - # tfa.MultiF1Score(name="f1", average="weighted"), - ] + t_mul = 1 + first_steps = (params.steps_per_epoch * params.epochs) / (np.power(params.lr_cycles, t_mul) - t_mul + 1) + scheduler = keras.optimizers.schedules.CosineDecayRestarts( + initial_learning_rate=params.lr_rate, + first_decay_steps=np.ceil(first_steps), + t_mul=t_mul, + m_mul=0.5, + ) if params.resume and params.weights_file: logger.debug(f"Hydrating model weights from file {params.weights_file}") @@ -125,14 +85,20 @@ def train(params: HKTrainParams): if params.model_file is None: params.model_file = params.job_dir / "model.keras" + optimizer = keras.optimizers.Adam(scheduler) + loss = keras.losses.CategoricalFocalCrossentropy(from_logits=True, alpha=class_weights) + metrics = [ + keras.metrics.CategoricalAccuracy(name="acc"), + keras.metrics.F1Score(name="f1", average="weighted"), + ] + model.compile(optimizer=optimizer, loss=loss, metrics=metrics) - model(inputs) flops = nse.metrics.flops.get_flops(model, batch_size=1, fpath=params.job_dir / "model_flops.log") model.summary(print_fn=logger.info) logger.debug(f"Model requires {flops/1e6:0.2f} MFLOPS") ModelCheckpoint = keras.callbacks.ModelCheckpoint - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): ModelCheckpoint = WandbModelCheckpoint model_callbacks = [ keras.callbacks.EarlyStopping( @@ -140,31 +106,32 @@ def train(params: HKTrainParams): patience=max(int(0.25 * params.epochs), 1), mode="max" if params.val_metric == "f1" else "auto", restore_best_weights=True, + verbose=params.verbose - 1, ), ModelCheckpoint( filepath=str(params.model_file), monitor=f"val_{params.val_metric}", save_best_only=True, mode="max" if params.val_metric == "f1" else "auto", - verbose=1, + verbose=params.verbose - 1, ), keras.callbacks.CSVLogger(params.job_dir / "history.csv"), ] - if env_flag("TENSORBOARD"): + if nse.utils.env_flag("TENSORBOARD"): model_callbacks.append( keras.callbacks.TensorBoard( log_dir=params.job_dir, write_steps_per_second=True, ) ) - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): model_callbacks.append(WandbMetricsLogger()) try: model.fit( train_ds, steps_per_epoch=params.steps_per_epoch, - verbose=2, + verbose=params.verbose, epochs=params.epochs, validation_data=val_ds, callbacks=model_callbacks, @@ -175,18 +142,19 @@ def train(params: HKTrainParams): logger.debug(f"Model saved to {params.model_file}") # Get full validation results - model = keras.models.load_model(params.model_file) logger.debug("Performing full validation") - y_pred = np.argmax(model.predict(val_ds), axis=-1).flatten() + y_pred = np.argmax(model.predict(val_ds, verbose=params.verbose), axis=-1) cm_path = params.job_dir / "confusion_matrix.png" - nse.plotting.cm.confusion_matrix_plot(y_true, y_pred, labels=class_names, save_path=cm_path, normalize="true") - if env_flag("WANDB"): - conf_mat = wandb.plot.confusion_matrix(preds=y_pred, y_true=y_true, class_names=class_names) - wandb.log({"conf_mat": conf_mat}) - # END IF + nse.plotting.confusion_matrix_plot(y_true, y_pred, labels=class_names, save_path=cm_path, normalize="true") + nse.plotting.px_plot_confusion_matrix( + y_true, + y_pred, + labels=class_names, + save_path=cm_path.with_suffix(".html"), + normalize="true", + ) # Summarize results - test_acc = np.sum(y_pred == y_true) / len(y_true) - test_f1 = f1_score(y_true, y_pred, average="weighted") - logger.debug(f"[VAL SET] ACC={test_acc:.2%}, F1={test_f1:.2%}") + rst = model.evaluate(val_ds, verbose=params.verbose, return_dict=True) + logger.info("[VAL SET] " + ", ".join([f"{k}={v:0.4f}" for k, v in rst.items()])) diff --git a/heartkit/tasks/beat/utils.py b/heartkit/tasks/beat/utils.py deleted file mode 100644 index 73133a45..00000000 --- a/heartkit/tasks/beat/utils.py +++ /dev/null @@ -1,96 +0,0 @@ -import keras -from neuralspot_edge.models.efficientnet import ( - EfficientNetParams, - EfficientNetV2, - MBConvParams, -) -from rich.console import Console - -from ...defines import ModelArchitecture -from ...models import ModelFactory - -console = Console() - - -def create_model(inputs: keras.KerasTensor, num_classes: int, architecture: ModelArchitecture | None) -> keras.Model: - """Generate model or use default - - Args: - inputs (keras.KerasTensor): Model inputs - num_classes (int): Number of classes - architecture (ModelArchitecture|None): Model - - Returns: - keras.Model: Model - """ - if architecture: - return ModelFactory.get(architecture.name)( - x=inputs, - params=architecture.params, - num_classes=num_classes, - ) - - return default_model(inputs=inputs, num_classes=num_classes) - - -def default_model( - inputs: keras.KerasTensor, - num_classes: int, -) -> keras.Model: - """Reference beat model - - Args: - inputs (keras.KerasTensor): Model inputs - num_classes (int): Number of classes - - Returns: - keras.Model: Model - """ - blocks = [ - MBConvParams( - filters=32, - depth=2, - ex_ratio=1, - kernel_size=(1, 3), - strides=(1, 1), - se_ratio=2, - ), - MBConvParams( - filters=48, - depth=2, - ex_ratio=1, - kernel_size=(1, 3), - strides=(1, 2), - se_ratio=2, - ), - MBConvParams( - filters=64, - depth=3, - ex_ratio=1, - kernel_size=(1, 3), - strides=(1, 2), - se_ratio=4, - ), - MBConvParams( - filters=96, - depth=3, - ex_ratio=1, - kernel_size=(1, 3), - strides=(1, 2), - se_ratio=4, - ), - ] - return EfficientNetV2( - inputs, - params=EfficientNetParams( - input_filters=24, - input_strides=(1, 2), - input_kernel_size=(1, 5), - output_filters=0, - blocks=blocks, - include_top=True, - dropout=0.0, - drop_connect_rate=0.0, - ), - num_classes=num_classes, - ) diff --git a/heartkit/tasks/denoise/__init__.py b/heartkit/tasks/denoise/__init__.py index a6716dca..a1ea5d58 100644 --- a/heartkit/tasks/denoise/__init__.py +++ b/heartkit/tasks/denoise/__init__.py @@ -1,26 +1,27 @@ -from ...defines import HKDemoParams, HKExportParams, HKTestParams, HKTrainParams +from ...defines import HKTaskParams from ..task import HKTask from .demo import demo from .evaluate import evaluate from .export import export from .train import train +from .dataloader import DenoiseDataloader class DenoiseTask(HKTask): """HeartKit Denoise Task""" @staticmethod - def train(params: HKTrainParams): + def train(params: HKTaskParams): train(params) @staticmethod - def evaluate(params: HKTestParams): + def evaluate(params: HKTaskParams): evaluate(params) @staticmethod - def export(params: HKExportParams): + def export(params: HKTaskParams): export(params) @staticmethod - def demo(params: HKDemoParams): + def demo(params: HKTaskParams): demo(params) diff --git a/heartkit/tasks/denoise/dataloader.py b/heartkit/tasks/denoise/dataloader.py new file mode 100644 index 00000000..af567f4d --- /dev/null +++ b/heartkit/tasks/denoise/dataloader.py @@ -0,0 +1,34 @@ +from typing import Generator + +import numpy as np +import numpy.typing as npt +import neuralspot_edge as nse + + +from ...datasets import HKDataloader + + +class DenoiseDataloader(HKDataloader): + def __init__(self, **kwargs): + """Generic Dataloader for denoising task.""" + super().__init__(**kwargs) + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[npt.NDArray, None, None]: + """Generate data for given patient ids. + Leveraging the signal_generator method from the dataset class to generate data. + """ + gen = self.ds.signal_generator( + patient_generator=nse.utils.uniform_id_generator(patient_ids, repeat=True, shuffle=shuffle), + frame_size=self.frame_size, + samples_per_patient=samples_per_patient, + target_rate=self.sampling_rate, + ) + for x in gen: + x = np.nan_to_num(x, neginf=0, posinf=0).astype(np.float32) + x = np.reshape(x, (-1, 1)) + yield x diff --git a/heartkit/tasks/denoise/dataloaders/__init__.py b/heartkit/tasks/denoise/dataloaders/__init__.py deleted file mode 100644 index 3b0a30c0..00000000 --- a/heartkit/tasks/denoise/dataloaders/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .lsad import lsad_data_generator -from .ptbxl import ptbxl_data_generator -from .synthetic import synthetic_data_generator -from .syntheticppg import synthetic_ppg_data_generator diff --git a/heartkit/tasks/denoise/dataloaders/lsad.py b/heartkit/tasks/denoise/dataloaders/lsad.py deleted file mode 100644 index 167ad9ee..00000000 --- a/heartkit/tasks/denoise/dataloaders/lsad.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import Generator, Iterable - -import numpy.typing as npt - -from ....datasets import LsadDataset, PatientGenerator - - -def lsad_data_generator( - patient_generator: PatientGenerator, - ds: LsadDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, -) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: - """Generate frames using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: LsadDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - - Returns: - Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Sample generator - - """ - if isinstance(samples_per_patient, Iterable): - samples_per_patient = samples_per_patient[0] - - gen = ds.signal_generator( - patient_generator=patient_generator, - frame_size=frame_size, - samples_per_patient=samples_per_patient, - target_rate=target_rate, - ) - for x in gen: - y = x.copy() - yield x, y - # END FOR diff --git a/heartkit/tasks/denoise/dataloaders/ptbxl.py b/heartkit/tasks/denoise/dataloaders/ptbxl.py deleted file mode 100644 index 75fca173..00000000 --- a/heartkit/tasks/denoise/dataloaders/ptbxl.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import Generator, Iterable - -import numpy.typing as npt - -from ....datasets import PatientGenerator, PtbxlDataset - - -def ptbxl_data_generator( - patient_generator: PatientGenerator, - ds: PtbxlDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, -) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: - """Generate frames using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: PtbxlDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - - Returns: - Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Sample generator - - """ - if isinstance(samples_per_patient, Iterable): - samples_per_patient = samples_per_patient[0] - - gen = ds.signal_generator( - patient_generator=patient_generator, - frame_size=frame_size, - samples_per_patient=samples_per_patient, - target_rate=target_rate, - ) - for x in gen: - y = x.copy() - yield x, y - # END FOR diff --git a/heartkit/tasks/denoise/dataloaders/synthetic.py b/heartkit/tasks/denoise/dataloaders/synthetic.py deleted file mode 100644 index 68098e6f..00000000 --- a/heartkit/tasks/denoise/dataloaders/synthetic.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import Generator, Iterable - -import numpy.typing as npt - -from ....datasets import PatientGenerator, SyntheticDataset - - -def synthetic_data_generator( - patient_generator: PatientGenerator, - ds: SyntheticDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, -) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: - """Generate frames using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: SyntheticDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - - Returns: - Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Sample generator - - """ - if isinstance(samples_per_patient, Iterable): - samples_per_patient = samples_per_patient[0] - - gen = ds.signal_generator( - patient_generator=patient_generator, - frame_size=frame_size, - samples_per_patient=samples_per_patient, - target_rate=target_rate, - ) - for x in gen: - y = x.copy() - yield x, y - # END FOR diff --git a/heartkit/tasks/denoise/dataloaders/syntheticppg.py b/heartkit/tasks/denoise/dataloaders/syntheticppg.py deleted file mode 100644 index 07897c22..00000000 --- a/heartkit/tasks/denoise/dataloaders/syntheticppg.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import Generator, Iterable - -import numpy.typing as npt - -from ....datasets import PatientGenerator, SyntheticPpgDataset - - -def synthetic_ppg_data_generator( - patient_generator: PatientGenerator, - ds: SyntheticPpgDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, -) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: - """Generate frames using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: SyntheticPpgDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - - Returns: - Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Sample generator - - """ - if isinstance(samples_per_patient, Iterable): - samples_per_patient = samples_per_patient[0] - - gen = ds.signal_generator( - patient_generator=patient_generator, - frame_size=frame_size, - samples_per_patient=samples_per_patient, - target_rate=target_rate, - ) - for x in gen: - y = x.copy() - yield x, y - # END FOR diff --git a/heartkit/tasks/denoise/datasets.py b/heartkit/tasks/denoise/datasets.py index 55d6597c..6f09c601 100644 --- a/heartkit/tasks/denoise/datasets.py +++ b/heartkit/tasks/denoise/datasets.py @@ -1,323 +1,172 @@ -import functools -import logging -from pathlib import Path - import numpy as np -import numpy.typing as npt import tensorflow as tf +import neuralspot_edge as nse -from ...datasets import ( - HKDataset, - augment_pipeline, - preprocess_pipeline, - uniform_id_generator, -) -from ...datasets.dataloader import test_dataloader, train_val_dataloader -from ...defines import ( - AugmentationParams, - HKExportParams, - HKTestParams, - HKTrainParams, - PreprocessParams, -) -from ...utils import resolve_template_path -from .dataloaders import ( - lsad_data_generator, - ptbxl_data_generator, - synthetic_data_generator, - synthetic_ppg_data_generator, -) - -logger = logging.getLogger(__name__) - - -def preprocess(x: npt.NDArray, preprocesses: list[PreprocessParams], sample_rate: float) -> npt.NDArray: - """Preprocess data pipeline - - Args: - x (npt.NDArray): Input data - preprocesses (list[PreprocessParams]): Preprocess parameters - sample_rate (float): Sample rate - - Returns: - npt.NDArray: Preprocessed data - """ - return preprocess_pipeline(x, preprocesses=preprocesses, sample_rate=sample_rate) - - -def augment(x: npt.NDArray, augmentations: list[AugmentationParams], sample_rate: float) -> npt.NDArray: - """Augment data pipeline - - Args: - x (npt.NDArray): Input data - augmentations (list[AugmentationParams]): Augmentation parameters - sample_rate (float): Sample rate - - Returns: - npt.NDArray: Augmented data - """ - - return augment_pipeline(x=x, augmentations=augmentations, sample_rate=sample_rate) - - -def prepare( - x_y: tuple[npt.NDArray, npt.NDArray], - sample_rate: float, - preprocesses: list[PreprocessParams], - augmentations: list[AugmentationParams], - spec: tuple[tf.TensorSpec, tf.TensorSpec], - num_classes: int, -) -> tuple[npt.NDArray, npt.NDArray]: - """Prepare dataset - - Args: - x_y (tuple[npt.NDArray, npt.NDArray]): Input data - sample_rate (float): Sample rate - preprocesses (list[PreprocessParams]|None): Preprocess parameters - augmentations (list[AugmentationParams]|None): Augmentation parameters - spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - num_classes (int): Number of classes - - Returns: - tuple[npt.NDArray, npt.NDArray]: Prepared data - """ - - x, y = x_y[0].copy(), x_y[1].copy() - - if augmentations: - x = augment(x, augmentations, sample_rate) - # END IF - - if preprocesses: - x = preprocess(x, preprocesses, sample_rate) - y = preprocess(y, preprocesses, sample_rate) - # END IF - - x = x.reshape(spec[0].shape) - y = y.reshape(spec[1].shape) - - return x, y +from ...datasets import HKDataset, create_augmentation_pipeline +from ...defines import HKTaskParams, NamedParams +from .dataloader import DenoiseDataloader - -def get_ds_label_map(ds: HKDataset, label_map: dict[int, int] | None = None) -> dict[int, int]: - """Get label map for dataset - - Args: - ds (HKDataset): Dataset - label_map (dict[int, int]|None): Label map - - Returns: - dict[int, int]: Label map - """ - return label_map +logger = nse.utils.setup_logger(__name__) -def get_data_generator(ds: HKDataset, frame_size: int, samples_per_patient: int, target_rate: int): - """Get task data generator for dataset - - Args: - ds (HKDataset): Dataset - frame_size (int): Frame size - samples_per_patient (int): Samples per patient - target_rate (int): Target rate - - Returns: - callable: Data generator - """ - match ds.name: - case "lsad": - data_generator = lsad_data_generator - case "ptbxl": - data_generator = ptbxl_data_generator - case "synthetic": - data_generator = synthetic_data_generator - case "syntheticppg": - data_generator = synthetic_ppg_data_generator - case _: - raise ValueError(f"Dataset {ds.name} not supported") - # END MATCH - return functools.partial( - data_generator, - ds=ds, - frame_size=frame_size, - samples_per_patient=samples_per_patient, - target_rate=target_rate, - ) - - -def resolve_ds_cache_path(fpath: Path | None, ds: HKDataset, task: str, frame_size: int, sample_rate: int): - """Resolve dataset cache path +def create_data_pipeline( + ds: tf.data.Dataset, + sampling_rate: int, + batch_size: int, + buffer_size: int | None = None, + preprocesses: list[NamedParams] | None = None, + augmentations: list[NamedParams] | None = None, +) -> tf.data.Dataset: + """ "Create 'tf.data.Dataset' pipeline. Args: - fpath (Path|None): File path - ds (HKDataset): Dataset - task (str): Task - frame_size (int): Frame size - sample_rate (int): Sampling rate + ds (tf.data.Dataset): Input dataset + sampling_rate (int): Sampling rate + batch_size (int): Batch size + buffer_size (int | None, optional): Buffer size. Defaults to None. + preprocesses (list[NamedParams] | None, optional): Preprocessing pipeline. Defaults to None. + augmentations (list[NamedParams] | None, optional): Augmentation pipeline. Defaults to None. Returns: - Path|None: Resolved path + tf.data.Dataset: Augmented dataset """ - if not fpath: - return None - return resolve_template_path( - fpath=fpath, - dataset=ds.name, - task=task, - frame_size=frame_size, - sampling_rate=sample_rate, - ) + preprocessor = create_augmentation_pipeline(preprocesses, sampling_rate) + augmenter = create_augmentation_pipeline(augmentations, sampling_rate) + if buffer_size: + ds = ds.shuffle( + buffer_size=buffer_size, + reshuffle_each_iteration=True, + ) + if batch_size: + ds = ds.batch( + batch_size=batch_size, + drop_remainder=True, + num_parallel_calls=tf.data.AUTOTUNE, + ) + ds = ds.map(lambda x: preprocessor(x), num_parallel_calls=tf.data.AUTOTUNE) + ds = ds.map(lambda x: (augmenter(x), x), num_parallel_calls=tf.data.AUTOTUNE) + return ds.prefetch(tf.data.AUTOTUNE) def load_train_datasets( datasets: list[HKDataset], - params: HKTrainParams, - ds_spec: tuple[tf.TensorSpec, tf.TensorSpec], + params: HKTaskParams, ) -> tuple[tf.data.Dataset, tf.data.Dataset]: - """Load training and validation datasets + """Load training and validation dataset pipelines Args: - datasets (list[HKDataset]): Datasets - params (HKTrainParams): Training parameters - ds_spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - - Returns: - tuple[tf.data.Dataset, tf.data.Dataset]: Train and validation datasets + datasets (list[HKDataset]): List of datasets + params (HKTaskParams): Training parameters """ - id_generator = functools.partial(uniform_id_generator, repeat=True) - train_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, - ) - train_datasets = [] val_datasets = [] for ds in datasets: - val_file = resolve_ds_cache_path( - params.val_file, - ds=ds, - task="denoise", - frame_size=params.frame_size, - sample_rate=params.sampling_rate, - ) - data_generator = get_data_generator( + dataloader = DenoiseDataloader( ds=ds, frame_size=params.frame_size, - samples_per_patient=params.samples_per_patient, - target_rate=params.sampling_rate, + sampling_rate=params.sampling_rate, + label_map=params.class_map, ) - - train_ds, val_ds = train_val_dataloader( - ds=ds, - spec=ds_spec, - data_generator=data_generator, - id_generator=id_generator, + train_patients, val_patients = dataloader.split_train_val_patients( train_patients=params.train_patients, val_patients=params.val_patients, - val_pt_samples=params.val_samples_per_patient, - val_file=val_file, - val_size=params.val_size, - label_map=None, - label_type=None, - preprocess=train_prepare, - num_workers=params.data_parallelism, + ) + + train_ds = dataloader.create_dataloader( + patient_ids=train_patients, samples_per_patient=params.samples_per_patient, shuffle=True + ) + + val_ds = dataloader.create_dataloader( + patient_ids=val_patients, samples_per_patient=params.val_samples_per_patient, shuffle=False ) train_datasets.append(train_ds) val_datasets.append(val_ds) # END FOR - ds_weights = np.array([d.weight for d in params.datasets]) - ds_weights = ds_weights / ds_weights.sum() + ds_weights = None + if params.dataset_weights: + ds_weights = np.array(params.dataset_weights) + ds_weights = ds_weights / ds_weights.sum() train_ds = tf.data.Dataset.sample_from_datasets(train_datasets, weights=ds_weights) val_ds = tf.data.Dataset.sample_from_datasets(val_datasets, weights=ds_weights) # Shuffle and batch datasets for training - train_ds = ( - train_ds.shuffle( - buffer_size=params.buffer_size, - reshuffle_each_iteration=True, - ) - .batch( - batch_size=params.batch_size, - drop_remainder=False, - num_parallel_calls=tf.data.AUTOTUNE, - ) - .prefetch(buffer_size=tf.data.AUTOTUNE) + train_ds = create_data_pipeline( + ds=train_ds, + sampling_rate=params.sampling_rate, + batch_size=params.batch_size, + buffer_size=params.buffer_size, + preprocesses=params.preprocesses, + augmentations=params.augmentations, ) - val_ds = val_ds.batch( + + val_ds = create_data_pipeline( + ds=val_ds, + sampling_rate=params.sampling_rate, batch_size=params.batch_size, - drop_remainder=True, - num_parallel_calls=tf.data.AUTOTUNE, + buffer_size=None, + preprocesses=params.preprocesses, + augmentations=params.augmentations, ) + + # If given fixed val size or steps, then capture and cache + val_steps_per_epoch = params.val_size // params.batch_size if params.val_size else params.val_steps_per_epoch + if val_steps_per_epoch: + logger.info(f"Validation steps per epoch: {val_steps_per_epoch}") + val_ds = val_ds.take(val_steps_per_epoch).cache() + return train_ds, val_ds def load_test_dataset( datasets: list[HKDataset], - params: HKTestParams | HKExportParams, - ds_spec: tuple[tf.TensorSpec, tf.TensorSpec], + params: HKTaskParams, ) -> tf.data.Dataset: - """Load test dataset + """Load test dataset pipeline Args: - datasets (list[HKDataset]): Datasets - params (HKTestParams|HKExportParams): Test parameters - ds_spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec + datasets (list[HKDataset]): List of datasets + params (HKTaskParams): Test or export parameters Returns: - tf.data.Dataset: Test dataset + tf.data.Dataset: Test dataset pipeline """ - - id_generator = functools.partial(uniform_id_generator, repeat=True) - test_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, - ) - test_datasets = [] for ds in datasets: - test_file = resolve_ds_cache_path( - fpath=params.test_file, + dataloader = DenoiseDataloader( ds=ds, - task="denoise", frame_size=params.frame_size, - sample_rate=params.sampling_rate, + sampling_rate=params.sampling_rate, + label_map=params.class_map, ) - data_generator = get_data_generator( - ds=ds, - frame_size=params.frame_size, + test_patients = dataloader.test_patient_ids(params.test_patients) + test_ds = dataloader.create_dataloader( + patient_ids=test_patients, samples_per_patient=params.test_samples_per_patient, - target_rate=params.sampling_rate, - ) - - test_ds = test_dataloader( - ds=ds, - spec=ds_spec, - data_generator=data_generator, - id_generator=id_generator, - test_patients=params.test_patients, - test_file=test_file, - label_map=None, - label_type=None, - preprocess=test_prepare, - num_workers=params.data_parallelism, + shuffle=False, ) test_datasets.append(test_ds) # END FOR - ds_weights = np.array([d.weight for d in params.datasets]) - ds_weights = ds_weights / ds_weights.sum() + ds_weights = None + if params.dataset_weights: + ds_weights = np.array(params.dataset_weights) + ds_weights = ds_weights / ds_weights.sum() test_ds = tf.data.Dataset.sample_from_datasets(test_datasets, weights=ds_weights) - # END WITH + test_ds = create_data_pipeline( + ds=test_ds, + sampling_rate=params.sampling_rate, + batch_size=params.batch_size, + buffer_size=None, + preprocesses=params.preprocesses, + augmentations=params.augmentations, + ) + + if params.test_size: + batch_size = getattr(params, "batch_size", 1) + test_ds = test_ds.take(params.test_size // batch_size).cache() + return test_ds diff --git a/heartkit/tasks/denoise/demo.py b/heartkit/tasks/denoise/demo.py index f4bd6b75..13b203e7 100644 --- a/heartkit/tasks/denoise/demo.py +++ b/heartkit/tasks/denoise/demo.py @@ -2,25 +2,22 @@ import numpy as np import plotly.graph_objects as go -import tensorflow as tf from plotly.subplots import make_subplots from tqdm import tqdm +import neuralspot_edge as nse -from ...datasets.utils import uniform_id_generator -from ...defines import HKDemoParams +from ...defines import HKTaskParams from ...rpc import BackendFactory -from ...utils import setup_logger -from ..utils import load_datasets -from .datasets import prepare +from ...datasets import DatasetFactory, create_augmentation_pipeline -def demo(params: HKDemoParams): +def demo(params: HKTaskParams): """Run segmentation demo. Args: - params (HKDemoParams): Demo parameters + params (HKTaskParams): Demo parameters """ - logger = setup_logger(__name__, level=params.verbose) + logger = nse.utils.setup_logger(__name__, level=params.verbose) bg_color = "rgba(38,42,50,1.0)" primary_color = "#11acd5" @@ -32,36 +29,35 @@ def demo(params: HKDemoParams): params.demo_size = params.demo_size or 10 * params.sampling_rate # Load backend inference engine - runner = BackendFactory.create(params.backend, params=params) - - feat_shape = (params.demo_size, 1) - class_shape = (params.demo_size, 1) - - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype="float32"), - tf.TensorSpec(shape=class_shape, dtype="float32"), - ) + runner = BackendFactory.get(params.backend)(params=params) # Load data - dsets = load_datasets(datasets=params.datasets) - ds = random.choice(dsets) + datasets = [DatasetFactory.get(ds.name)(cacheable=False, **ds.params) for ds in params.datasets] + ds = random.choice(datasets) ds_gen = ds.signal_generator( - patient_generator=uniform_id_generator(ds.get_test_patient_ids(), repeat=False), + patient_generator=nse.utils.uniform_id_generator(ds.get_test_patient_ids(), repeat=False), frame_size=params.demo_size, samples_per_patient=5, target_rate=params.sampling_rate, ) x = next(ds_gen) + x = np.nan_to_num(x, neginf=0, posinf=0).astype(np.float32) + x = np.reshape(x, (-1, 1)) + y_act = x.copy() - x, y_act = prepare( - (x, x), - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, + preprocessor = create_augmentation_pipeline( + params.preprocesses, + sampling_rate=params.sampling_rate, ) + augmenter = create_augmentation_pipeline( + params.augmentations, + sampling_rate=params.sampling_rate, + ) + + x = preprocessor(augmenter(x)).numpy() + y_act = preprocessor(y_act).numpy() + x = x.flatten() y_act = y_act.flatten() diff --git a/heartkit/tasks/denoise/evaluate.py b/heartkit/tasks/denoise/evaluate.py index 860311d0..55ae9388 100644 --- a/heartkit/tasks/denoise/evaluate.py +++ b/heartkit/tasks/denoise/evaluate.py @@ -1,47 +1,28 @@ -import logging import os -import keras -import numpy as np -import tensorflow as tf - import neuralspot_edge as nse -from ...defines import HKTestParams -from ...utils import set_random_seed, setup_logger -from ..utils import load_datasets + +from ...defines import HKTaskParams +from ...datasets import DatasetFactory from .datasets import load_test_dataset -def evaluate(params: HKTestParams): - """Evaluate model +def evaluate(params: HKTaskParams): + """Evaluate model for denoise task with given parameters. Args: - params (HKTestParams): Evaluation parameters + params (HKTaskParams): Evaluation parameters """ - logger = setup_logger(__name__, level=params.verbose) - - params.seed = set_random_seed(params.seed) - logger.debug(f"Random seed {params.seed}") - os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "test.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "test.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) - - feat_shape = (params.frame_size, 1) - class_shape = (params.frame_size, 1) - - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype="float32"), - tf.TensorSpec(shape=class_shape, dtype="float32"), - ) + params.seed = nse.utils.set_random_seed(params.seed) + logger.debug(f"Random seed {params.seed}") - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - test_ds = load_test_dataset(datasets=datasets, params=params, ds_spec=ds_spec) - test_x, test_y = next(test_ds.batch(params.test_size).as_numpy_iterator()) + test_ds = load_test_dataset(datasets=datasets, params=params) logger.debug("Loading model") model = nse.models.load_model(params.model_file) @@ -50,21 +31,7 @@ def evaluate(params: HKTestParams): model.summary(print_fn=logger.debug) logger.debug(f"Model requires {flops/1e6:0.2f} MFLOPS") - logger.debug("Performing inference") - y_true = test_y.squeeze() - y_prob = model.predict(test_x) - y_pred = y_prob.squeeze() - # Summarize results - cossim = keras.metrics.CosineSimilarity() - cossim.update_state(y_true, y_pred) # pylint: disable=E1102 - test_cossim = cossim.result().numpy() # pylint: disable=E1102 - logger.debug("Testing Results") - mae = keras.metrics.MeanAbsoluteError() - mae.update_state(y_true, y_pred) # pylint: disable=E1102 - test_mae = mae.result().numpy() # pylint: disable=E1102 - mse = keras.metrics.MeanSquaredError() - mse.update_state(y_true, y_pred) # pylint: disable=E1102 - test_mse = mse.result().numpy() # pylint: disable=E1102 - np.sqrt(np.mean(np.square(y_true - y_pred))) - logger.info(f"[TEST SET] MAE={test_mae:.2%}, MSE={test_mse:.2%}, COSSIM={test_cossim:.2%}") + logger.debug("Performing inference") + rst = model.evaluate(test_ds, verbose=params.verbose, return_dict=True) + logger.info("[TEST SET] " + ", ".join([f"{k.upper()}={v:.2%}" for k, v in rst.items()])) diff --git a/heartkit/tasks/denoise/export.py b/heartkit/tasks/denoise/export.py index 83d8eb73..611581b8 100644 --- a/heartkit/tasks/denoise/export.py +++ b/heartkit/tasks/denoise/export.py @@ -1,55 +1,41 @@ -import logging import os import shutil import keras import numpy as np -import tensorflow as tf - import neuralspot_edge as nse -from ...defines import HKExportParams -from ...utils import setup_logger -from ..utils import load_datasets + +from ...defines import HKTaskParams +from ...datasets import DatasetFactory from .datasets import load_test_dataset -def export(params: HKExportParams): +def export(params: HKTaskParams): """Export model Args: - params (HKExportParams): Deployment parameters + params (HKTaskParams): Deployment parameters """ - logger = setup_logger(__name__, level=params.verbose) - os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "export.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "export.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) - tfl_model_path = params.job_dir / "model.tflite" tflm_model_path = params.job_dir / "model_buffer.h" feat_shape = (params.frame_size, 1) - class_shape = (params.frame_size, 1) - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype="float32"), - tf.TensorSpec(shape=class_shape, dtype="float32"), - ) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - datasets = load_datasets(datasets=params.datasets) - - test_ds = load_test_dataset(datasets=datasets, params=params, ds_spec=ds_spec) - test_x, test_y = next(test_ds.batch(params.test_size).as_numpy_iterator()) + test_ds = load_test_dataset(datasets=datasets, params=params) + test_x = np.concatenate([x for x, _ in test_ds.as_numpy_iterator()]) + test_y = np.concatenate([y for _, y in test_ds.as_numpy_iterator()]) # Load model and set fixed batch size of 1 logger.debug("Loading trained model") model = nse.models.load_model(params.model_file) - - inputs = keras.Input(shape=ds_spec[0].shape, batch_size=1, name="input", dtype=ds_spec[0].dtype) - model(inputs) # Build model with fixed batch size of 1 + inputs = keras.Input(shape=feat_shape, batch_size=1, name="input", dtype="float32") + model(inputs) flops = nse.metrics.flops.get_flops(model, batch_size=1, fpath=params.job_dir / "model_flops.log") model.summary(print_fn=logger.debug) @@ -83,25 +69,33 @@ def export(params: HKExportParams): tflite.compile() # Verify TFLite results match TF results on example data + metrics = [ + keras.metrics.MeanAbsoluteError(name="mae"), + keras.metrics.MeanSquaredError(name="mse"), + keras.metrics.RootMeanSquaredError(name="rmse"), + keras.metrics.CosineSimilarity(name="cosine"), + ] + + if params.val_metric not in [m.name for m in metrics]: + raise ValueError(f"Metric {params.val_metric} not supported") + logger.info("Validating model results") y_true = test_y y_pred_tf = model.predict(test_x) y_pred_tfl = tflite.predict(x=test_x) - tf_mae = np.mean(np.abs(y_true - y_pred_tf)) - tf_rmse = np.sqrt(np.mean((y_true - y_pred_tf) ** 2)) - logger.info(f"[TF SET] MAE={tf_mae:.2%}, RMSE={tf_rmse:.2%}") + tf_rst = nse.metrics.compute_metrics(metrics, y_true, y_pred_tf) + tfl_rst = nse.metrics.compute_metrics(metrics, y_true, y_pred_tfl) + logger.info("[TF METRICS] " + " ".join([f"{k.upper()}={v:.2%}" for k, v in tf_rst.items()])) + logger.info("[TFL METRICS] " + " ".join([f"{k.upper()}={v:.2%}" for k, v in tfl_rst.items()])) - tfl_mae = np.mean(np.abs(y_true - y_pred_tfl)) - tfl_rmse = np.sqrt(np.mean((y_true - y_pred_tfl) ** 2)) - logger.info(f"[TFL SET] MAE={tfl_mae:.2%}, RMSE={tfl_rmse:.2%}") + metric_diff = abs(tf_rst[params.val_metric] - tfl_rst[params.val_metric]) # Check accuracy hit - tfl_acc_drop = max(0, tf_mae - tfl_mae) - if params.val_acc_threshold is not None and (1 - tfl_acc_drop) < params.val_acc_threshold: - logger.warning(f"TFLite accuracy dropped by {tfl_acc_drop:0.2%}") - elif params.val_acc_threshold: - logger.info(f"Validation passed ({tfl_acc_drop:0.2%})") + if params.val_metric_threshold is not None and metric_diff > params.val_metric_threshold: + logger.warning(f"TFLite accuracy dropped by {metric_diff:0.2%}") + elif params.val_metric_threshold: + logger.info(f"Validation passed ({metric_diff:0.2%})") if params.tflm_file and tflm_model_path != params.tflm_file: logger.debug(f"Copying TFLM header to {params.tflm_file}") diff --git a/heartkit/tasks/denoise/metrics.py b/heartkit/tasks/denoise/metrics.py deleted file mode 100644 index d33065f9..00000000 --- a/heartkit/tasks/denoise/metrics.py +++ /dev/null @@ -1,96 +0,0 @@ -import numpy as np -import numpy.typing as npt - - -def cossim(y_true: npt.NDArray, y_pred: npt.NDArray, axis: int = -1) -> npt.NDArray: - """Cosine similarity averaged over the batch - - Args: - y_true (npt.NDArray): True values - y_pred (npt.NDArray): Predicted values - axis (int, optional): Axis to sum. Defaults to 1. - - Returns: - npt.NDArray: Cosine similarity - """ - return np.mean( - np.sum(y_true * y_pred, axis=axis) / (np.linalg.norm(y_true, axis=axis) * np.linalg.norm(y_pred, axis=axis)) - ) - - -def ssd(y_true: npt.NDArray, y_pred: npt.NDArray, axis: int = 1) -> npt.NDArray: - """Sum of squared distance - - Args: - y_true (npt.NDArray): True values - y_pred (npt.NDArray): Predicted values - axis (int, optional): Axis to sum. Defaults to 1. - - Returns: - npt.NDArray: Sum of squared distance - """ - return np.sum(np.square(y_true - y_pred), axis=axis) - - -def mad(y_true: npt.NDArray, y_pred: npt.NDArray, axis: int = 1) -> npt.NDArray: - """Absolute max difference - - Args: - y_true (npt.NDArray): True values - y_pred (npt.NDArray): Predicted values - axis (int, optional): Axis to sum. Defaults to 1. - - Returns: - npt.NDArray: Absolute max difference - """ - return np.max(np.abs(y_true - y_pred), axis=axis) - - -def prd(y_true: npt.NDArray, y_pred: npt.NDArray, axis: int = 1) -> npt.NDArray: - """Percentage root mean square difference - - Args: - y_true (npt.NDArray): True values - y_pred (npt.NDArray): Predicted values - axis (int, optional): Axis to sum. Defaults to 1. - - Returns: - npt.NDArray: Percentage root mean square difference - """ - N = np.sum(np.square(y_pred - y_true), axis=axis) - D = np.sum(np.square(y_pred - np.mean(y_true)), axis=axis) - PRD = np.sqrt(N / D) * 100 - - return PRD - - -def snr(y1: npt.NDArray, y2: npt.NDArray) -> npt.NDArray: - """Compute signal to noise ratio - - Args: - y1 (npt.NDArray): True values - y2 (npt.NDArray): Predicted values - - Returns: - npt.NDArray: Signal to noise ratio - """ - N = np.sum(np.square(y1), axis=1) - D = np.sum(np.square(y2 - y1), axis=1) - - SNR = 10 * np.log10(N / D) - - return SNR - - -def snr_improvement(y_in: npt.NDArray, y_out: npt.NDArray, y_clean: npt.NDArray) -> npt.NDArray: - """Compute signal to noise ratio improvement - - Args: - y_in (npt.NDArray): Input signal - y_out (npt.NDArray): Output signal - y_clean (npt.NDArray): Clean signal - - Returns: - npt.NDArray: Signal to noise ratio improvement - """ - return snr(y_clean, y_out) - snr(y_clean, y_in) diff --git a/heartkit/tasks/denoise/train.py b/heartkit/tasks/denoise/train.py index 147d224a..bcc781bc 100644 --- a/heartkit/tasks/denoise/train.py +++ b/heartkit/tasks/denoise/train.py @@ -1,45 +1,35 @@ -import logging import os +import numpy as np import keras -import tensorflow as tf import wandb from wandb.keras import WandbMetricsLogger, WandbModelCheckpoint import neuralspot_edge as nse -from ...defines import HKTrainParams -from ...utils import env_flag, set_random_seed, setup_logger -from ..utils import load_datasets +from ...defines import HKTaskParams +from ...datasets import DatasetFactory from .datasets import load_train_datasets -from .utils import create_model +from ...models import ModelFactory -def train(params: HKTrainParams): - """Train model +def train(params: HKTaskParams): + """Train model for denoise task with given parameters. Args: - params (HKTrainParams): Training parameters + params (HKTaskParams): Training parameters """ - logger = setup_logger(__name__, level=params.verbose) - - params.seed = set_random_seed(params.seed) - logger.debug(f"Random seed {params.seed}") - os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "train.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "train.log", mode="w") - handler.setLevel(logger.level) - logger.addHandler(handler) + + params.seed = nse.utils.set_random_seed(params.seed) + logger.debug(f"Random seed {params.seed}") with open(params.job_dir / "train_config.json", "w", encoding="utf-8") as fp: fp.write(params.model_dump_json(indent=2)) - if env_flag("WANDB"): - wandb.init( - project=params.project, - entity="ambiq", - dir=params.job_dir, - ) + if nse.utils.env_flag("WANDB"): + wandb.init(project=params.project, entity="ambiq", dir=params.job_dir) wandb.config.update(params.model_dump()) # END IF @@ -48,27 +38,12 @@ def train(params: HKTrainParams): params.class_names = ["CLEAN"] feat_shape = (params.frame_size, 1) - class_shape = (params.frame_size, 1) - - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype="float32"), - tf.TensorSpec(shape=class_shape, dtype="float32"), - ) - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - train_ds, val_ds = load_train_datasets( - datasets=datasets, - params=params, - ds_spec=ds_spec, - ) + train_ds, val_ds = load_train_datasets(datasets=datasets, params=params) - inputs = keras.Input( - shape=ds_spec[0].shape, - batch_size=None, - name="input", - dtype=ds_spec[0].dtype.name, - ) + inputs = keras.Input(shape=feat_shape, name="input", dtype="float32") # Load existing model if params.resume and params.model_file: @@ -77,57 +52,54 @@ def train(params: HKTrainParams): params.model_file = None else: logger.debug("Creating model from scratch") - model = create_model( - inputs, + model = ModelFactory.get(params.architecture.name)( + x=inputs, + params=params.architecture.params, num_classes=params.num_classes, - architecture=params.architecture, ) # END IF - if params.lr_cycles > 1: - scheduler = keras.optimizers.schedules.CosineDecayRestarts( - initial_learning_rate=params.lr_rate, - first_decay_steps=int(0.1 * params.steps_per_epoch * params.epochs), - t_mul=1.65 / (0.1 * params.lr_cycles * (params.lr_cycles - 1)), - m_mul=0.4, - ) - else: - scheduler = keras.optimizers.schedules.CosineDecay( - initial_learning_rate=params.lr_rate, - decay_steps=params.steps_per_epoch * params.epochs, - ) - # END IF + t_mul = 1 + first_steps = (params.steps_per_epoch * params.epochs) / (np.power(params.lr_cycles, t_mul) - t_mul + 1) + scheduler = keras.optimizers.schedules.CosineDecayRestarts( + initial_learning_rate=params.lr_rate, + first_decay_steps=np.ceil(first_steps), + t_mul=t_mul, + m_mul=0.5, + ) + + if params.resume and params.weights_file and params.weights_file.exists(): + logger.debug(f"Hydrating model weights from file {params.weights_file}") + model.load_weights(params.weights_file) + + if params.model_file is None: + params.model_file = params.job_dir / "model.keras" optimizer = keras.optimizers.Adam(scheduler) loss = keras.losses.MeanSquaredError() + # loss = keras.losses.Huber() metrics = [ keras.metrics.MeanAbsoluteError(name="mae"), keras.metrics.MeanSquaredError(name="mse"), - keras.metrics.CosineSimilarity(name="cosine"), + keras.metrics.CosineSimilarity(name="cos"), + nse.metrics.Snr(name="snr"), ] - if params.resume and params.weights_file: - logger.debug(f"Hydrating model weights from file {params.weights_file}") - model.load_weights(params.weights_file) - - if params.model_file is None: - params.model_file = params.job_dir / "model.keras" - model.compile(optimizer=optimizer, loss=loss, metrics=metrics) flops = nse.metrics.flops.get_flops(model, batch_size=1, fpath=params.job_dir / "model_flops.log") - model(inputs) model.summary(print_fn=logger.debug) logger.debug(f"Model requires {flops/1e6:0.2f} MFLOPS") + val_mode = "max" if params.val_metric in ("f1", "cos") else "auto" ModelCheckpoint = keras.callbacks.ModelCheckpoint - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): ModelCheckpoint = WandbModelCheckpoint model_callbacks = [ keras.callbacks.EarlyStopping( monitor=f"val_{params.val_metric}", patience=max(int(0.25 * params.epochs), 1), - mode="max" if params.val_metric == "f1" else "auto", + mode=val_mode, restore_best_weights=True, verbose=min(params.verbose - 1, 1), ), @@ -136,19 +108,19 @@ def train(params: HKTrainParams): monitor=f"val_{params.val_metric}", save_best_only=True, save_weights_only=False, - mode="max" if params.val_metric == "f1" else "auto", + mode=val_mode, verbose=min(params.verbose - 1, 1), ), keras.callbacks.CSVLogger(params.job_dir / "history.csv"), ] - if env_flag("TENSORBOARD"): + if nse.utils.env_flag("TENSORBOARD"): model_callbacks.append( keras.callbacks.TensorBoard( log_dir=params.job_dir, write_steps_per_second=True, ) ) - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): model_callbacks.append(WandbMetricsLogger()) try: @@ -166,5 +138,8 @@ def train(params: HKTrainParams): logger.debug(f"Model saved to {params.model_file}") # Get full validation results - keras.models.load_model(params.model_file) logger.debug("Performing full validation") + + # Summarize results + rst = model.evaluate(val_ds, return_dict=True) + logger.info("[VAL SET]" + ", ".join([f"{k.upper()}={v:.2%}" for k, v in rst.items()])) diff --git a/heartkit/tasks/denoise/utils.py b/heartkit/tasks/denoise/utils.py deleted file mode 100644 index dbf46a25..00000000 --- a/heartkit/tasks/denoise/utils.py +++ /dev/null @@ -1,107 +0,0 @@ -import keras -from neuralspot_edge.models.tcn import Tcn, TcnBlockParams, TcnParams -from rich.console import Console - -from ...defines import ModelArchitecture -from ...models import ModelFactory - -console = Console() - - -def create_model(inputs: keras.KerasTensor, num_classes: int, architecture: ModelArchitecture | None) -> keras.Model: - """Generate model or use default - - Args: - inputs (keras.KerasTensor): Model inputs - num_classes (int): Number of classes - architecture (ModelArchitecture|None): Model - - Returns: - keras.Model: Model - """ - if architecture: - return ModelFactory.get(name=architecture.name)( - x=inputs, - params=architecture.params, - num_classes=num_classes, - ) - - return _default_model(inputs=inputs, num_classes=num_classes) - - -def _default_model( - inputs: keras.KerasTensor, - num_classes: int, -) -> keras.Model: - """Reference model - - Args: - inputs (keras.KerasTensor): Model inputs - num_classes (int): Number of classes - - Returns: - keras.Model: Model - """ - # Default model - - blocks = [ - TcnBlockParams( - filters=8, - kernel=(1, 7), - dilation=(1, 1), - dropout=0.1, - ex_ratio=1, - se_ratio=0, - norm="batch", - ), - TcnBlockParams( - filters=12, - kernel=(1, 7), - dilation=(1, 1), - dropout=0.1, - ex_ratio=1, - se_ratio=2, - norm="batch", - ), - TcnBlockParams( - filters=16, - kernel=(1, 7), - dilation=(1, 2), - dropout=0.1, - ex_ratio=1, - se_ratio=2, - norm="batch", - ), - TcnBlockParams( - filters=24, - kernel=(1, 7), - dilation=(1, 4), - dropout=0.1, - ex_ratio=1, - se_ratio=2, - norm="batch", - ), - TcnBlockParams( - filters=32, - kernel=(1, 7), - dilation=(1, 8), - dropout=0.1, - ex_ratio=1, - se_ratio=2, - norm="batch", - ), - ] - - return Tcn( - x=inputs, - params=TcnParams( - input_kernel=(1, 7), - input_norm="batch", - blocks=blocks, - output_kernel=(1, 7), - include_top=True, - use_logits=True, - model_name="tcn", - ), - num_classes=num_classes, - ) diff --git a/heartkit/tasks/diagnostic/__init__.py b/heartkit/tasks/diagnostic/__init__.py index 99cac1de..fb747bf8 100644 --- a/heartkit/tasks/diagnostic/__init__.py +++ b/heartkit/tasks/diagnostic/__init__.py @@ -1,4 +1,4 @@ -from ...defines import HKDemoParams, HKExportParams, HKTestParams, HKTrainParams +from ...defines import HKTaskParams from ..task import HKTask from .defines import HKDiagnostic from .demo import demo @@ -11,17 +11,17 @@ class DiagnosticTask(HKTask): """HeartKit Diagnostic Task""" @staticmethod - def train(params: HKTrainParams): + def train(params: HKTaskParams): train(params) @staticmethod - def evaluate(params: HKTestParams): + def evaluate(params: HKTaskParams): evaluate(params) @staticmethod - def export(params: HKExportParams): + def export(params: HKTaskParams): export(params) @staticmethod - def demo(params: HKDemoParams): + def demo(params: HKTaskParams): demo(params) diff --git a/heartkit/tasks/diagnostic/dataloaders/__init__.py b/heartkit/tasks/diagnostic/dataloaders/__init__.py index cfa9101e..7f8196bb 100644 --- a/heartkit/tasks/diagnostic/dataloaders/__init__.py +++ b/heartkit/tasks/diagnostic/dataloaders/__init__.py @@ -1,2 +1,10 @@ -from .lsad import lsad_data_generator, lsad_label_map -from .ptbxl import ptbxl_data_generator, ptbxl_label_map +import neuralspot_edge as nse + +from ....datasets import HKDataloader + +from .ptbxl import PtbxlDataloader +from .lsad import LsadDataloader + +DiagnosticDataloaderFactory = nse.utils.create_factory(factory="HKDiagnosticDataloaderFactory", type=HKDataloader) +DiagnosticDataloaderFactory.register("ptbxl", PtbxlDataloader) +DiagnosticDataloaderFactory.register("lsad", LsadDataloader) diff --git a/heartkit/tasks/diagnostic/dataloaders/lsad.py b/heartkit/tasks/diagnostic/dataloaders/lsad.py index 088ac70a..b258d1f5 100644 --- a/heartkit/tasks/diagnostic/dataloaders/lsad.py +++ b/heartkit/tasks/diagnostic/dataloaders/lsad.py @@ -1,9 +1,9 @@ from typing import Generator import numpy.typing as npt +import neuralspot_edge as nse -from ....datasets.defines import PatientGenerator -from ....datasets.lsad import LsadDataset, LsadScpCode +from ....datasets import LsadDataset, LsadScpCode, HKDataloader from ..defines import HKDiagnostic LsadDiagnosticMap = { @@ -48,50 +48,26 @@ } -def lsad_label_map( - label_map: dict[int, int] | None = None, -) -> dict[int, int]: - """Get label map +class LsadDataloader(HKDataloader): + def __init__(self, ds: LsadDataset, **kwargs): + super().__init__(ds=ds, **kwargs) + if self.label_map: + self.label_map = {k: self.label_map[v] for (k, v) in LsadDiagnosticMap.items() if v in self.label_map} - Args: - label_map (dict[int, int]|None): Label map + self.label_type = "scp" - Returns: - dict[int, int]: Label map - """ - return {k: label_map.get(v, -1) for (k, v) in LsadDiagnosticMap.items()} - - -def lsad_data_generator( - patient_generator: PatientGenerator, - ds: LsadDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, - label_map: dict[int, int] | None = None, -) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: - """Generate frames w/ diagnostic labels using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: LsadDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - label_map (dict[int, int] | None, optional): Label map. Defaults to None. - - Returns: - Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Sample generator - - """ - tgt_map = lsad_label_map(label_map=label_map) - - return ds.signal_label_generator( - patient_generator=patient_generator, - frame_size=frame_size, - samples_per_patient=samples_per_patient, - target_rate=target_rate, - label_map=tgt_map, - label_type="scp", - label_format="multi_hot", - ) + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + return self.ds.signal_label_generator( + patient_generator=nse.utils.uniform_id_generator(patient_ids, repeat=True, shuffle=shuffle), + frame_size=self.frame_size, + samples_per_patient=samples_per_patient, + target_rate=self.sampling_rate, + label_map=self.label_map, + label_type=self.label_type, + label_format="multi_hot", + ) diff --git a/heartkit/tasks/diagnostic/dataloaders/ptbxl.py b/heartkit/tasks/diagnostic/dataloaders/ptbxl.py index 30caff39..961addf3 100644 --- a/heartkit/tasks/diagnostic/dataloaders/ptbxl.py +++ b/heartkit/tasks/diagnostic/dataloaders/ptbxl.py @@ -1,9 +1,9 @@ from typing import Generator import numpy.typing as npt +import neuralspot_edge as nse -from ....datasets.defines import PatientGenerator -from ....datasets.ptbxl import PtbxlDataset, PtbxlScpCode +from ....datasets import PtbxlDataset, PtbxlScpCode, HKDataloader from ..defines import HKDiagnostic PtbxlDiagnosticMap = { @@ -59,50 +59,26 @@ } -def ptbxl_label_map( - label_map: dict[int, int] | None = None, -) -> dict[int, int]: - """Get label map +class PtbxlDataloader(HKDataloader): + def __init__(self, ds: PtbxlDataset, **kwargs): + super().__init__(ds=ds, **kwargs) + if self.label_map: + self.label_map = {k: self.label_map[v] for (k, v) in PtbxlDiagnosticMap.items() if v in self.label_map} - Args: - label_map (dict[int, int]|None): Label map + self.label_type = "scp" - Returns: - dict[int, int]: Label map - """ - return {k: label_map.get(v, -1) for (k, v) in PtbxlDiagnosticMap.items()} - - -def ptbxl_data_generator( - patient_generator: PatientGenerator, - ds: PtbxlDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, - label_map: dict[int, int] | None = None, -) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: - """Generate frames w/ diagnostic labels using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: PtbxlDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - label_map (dict[int, int] | None, optional): Label map. Defaults to None. - - Returns: - Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Sample generator - - """ - tgt_map = ptbxl_label_map(label_map=label_map) - - return ds.signal_label_generator( - patient_generator=patient_generator, - frame_size=frame_size, - samples_per_patient=samples_per_patient, - target_rate=target_rate, - label_map=tgt_map, - label_type="scp", - label_format="multi_hot", - ) + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + return self.ds.signal_label_generator( + patient_generator=nse.utils.uniform_id_generator(patient_ids, repeat=True, shuffle=shuffle), + frame_size=self.frame_size, + samples_per_patient=samples_per_patient, + target_rate=self.sampling_rate, + label_map=self.label_map, + label_type=self.label_type, + label_format="multi_hot", + ) diff --git a/heartkit/tasks/diagnostic/datasets.py b/heartkit/tasks/diagnostic/datasets.py index 853cced1..c9bf4f7a 100644 --- a/heartkit/tasks/diagnostic/datasets.py +++ b/heartkit/tasks/diagnostic/datasets.py @@ -1,351 +1,159 @@ -import functools -import logging -from pathlib import Path - import numpy as np -import numpy.typing as npt import tensorflow as tf +import neuralspot_edge as nse from ...datasets import ( HKDataset, - augment_pipeline, - preprocess_pipeline, - uniform_id_generator, -) -from ...datasets.dataloader import test_dataloader, train_val_dataloader -from ...defines import ( - AugmentationParams, - HKExportParams, - HKTestParams, - HKTrainParams, - PreprocessParams, -) -from ...utils import resolve_template_path -from .dataloaders import ( - lsad_data_generator, - lsad_label_map, - ptbxl_data_generator, - ptbxl_label_map, + create_augmentation_pipeline, ) +from ...datasets.dataloader import HKDataloader +from ...defines import HKTaskParams, NamedParams -logger = logging.getLogger(__name__) - - -def preprocess(x: npt.NDArray, preprocesses: list[PreprocessParams], sample_rate: float) -> npt.NDArray: - """Preprocess data pipeline - - Args: - x (npt.NDArray): Input data - preprocesses (list[PreprocessParams]): Preprocess parameters - sample_rate (float): Sample rate - - Returns: - npt.NDArray: Preprocessed data - """ - return preprocess_pipeline(x, preprocesses=preprocesses, sample_rate=sample_rate) - - -def augment(x: npt.NDArray, augmentations: list[AugmentationParams], sample_rate: float) -> npt.NDArray: - """Augment data pipeline - - Args: - x (npt.NDArray): Input data - augmentations (list[AugmentationParams]): Augmentation parameters - sample_rate (float): Sample rate - - Returns: - npt.NDArray: Augmented data - """ - - return augment_pipeline(x=x, augmentations=augmentations, sample_rate=sample_rate) - - -def prepare( - x_y: tuple[npt.NDArray, npt.NDArray], - sample_rate: float, - preprocesses: list[PreprocessParams], - augmentations: list[AugmentationParams], - spec: tuple[tf.TensorSpec, tf.TensorSpec], - num_classes: int, -) -> tuple[npt.NDArray, npt.NDArray]: - """Prepare dataset - - Args: - x_y (tuple[npt.NDArray, int]): Data and label - sample_rate (float): Sample rate - preprocesses (list[PreprocessParams]|None): Preprocess parameters - augmentations (list[AugmentationParams]|None): Augmentation parameters - spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - num_classes (int): Number of classes - - Returns: - tuple[npt.NDArray, npt.NDArray]: Data and label - """ - - x, y = x_y[0].copy(), x_y[1] - - if augmentations: - x = augment(x, augmentations, sample_rate) - # END IF - - if preprocesses: - x = preprocess(x, preprocesses, sample_rate) - # END IF - - x = x.reshape(spec[0].shape) - # y is already multi-hot encoded +from .dataloaders import DiagnosticDataloaderFactory - return x, y +logger = nse.utils.setup_logger(__name__) -def get_ds_label_map(ds: HKDataset, label_map: dict[int, int] | None = None) -> dict[int, int]: - """Get label map for dataset - - Args: - ds (HKDataset): Dataset - label_map (dict[int, int]|None): Label map - - Returns: - dict[int, int]: Label map - """ - match ds.name: - case "lsad": - return lsad_label_map(label_map=label_map) - case "ptbxl": - return ptbxl_label_map(label_map=label_map) - case _: - raise ValueError(f"Dataset {ds.name} not supported") - # END MATCH - - -def get_ds_generator( - ds: HKDataset, - frame_size: int, - samples_per_patient: int, - target_rate: int, - label_map: dict[int, int] | None = None, +def create_data_pipeline( + ds: tf.data.Dataset, + sampling_rate: int, + batch_size: int, + buffer_size: int | None = None, + augmentations: list[NamedParams] | None = None, ): - """Get task data generator for dataset - - Args: - ds (HKDataset): Dataset - frame_size (int): Frame size - samples_per_patient (int): Samples per patient - target_rate (int): Target rate - - Returns: - callable: Data generator - """ - match ds.name: - case "lsad": - data_generator = lsad_data_generator - case "ptbxl": - data_generator = ptbxl_data_generator - case _: - raise ValueError(f"Dataset {ds.name} not supported") - # END MATCH - return functools.partial( - data_generator, - ds=ds, - frame_size=frame_size, - samples_per_patient=samples_per_patient, - target_rate=target_rate, - label_map=label_map, + if buffer_size: + ds = ds.shuffle( + buffer_size=buffer_size, + reshuffle_each_iteration=True, + ) + if batch_size: + ds = ds.batch( + batch_size=batch_size, + drop_remainder=True, + num_parallel_calls=tf.data.AUTOTUNE, + ) + augmenter = create_augmentation_pipeline(augmentations, sampling_rate=sampling_rate) + ds = ( + ds.map( + lambda data, labels: { + "data": tf.cast(data, "float32"), + "labels": labels, # Already multi-hot encoded + }, + num_parallel_calls=tf.data.AUTOTUNE, + ) + .map( + augmenter, + num_parallel_calls=tf.data.AUTOTUNE, + ) + .map( + lambda data: (data["data"], data["labels"]), + num_parallel_calls=tf.data.AUTOTUNE, + ) ) - -def get_ds_label_type(ds: HKDataset) -> str: - """Get label type for dataset - - Args: - ds (HKDataset): Dataset - - Returns: - str: Label type - """ - return "scp" - - -def resolve_ds_cache_path(fpath: Path | None, ds: HKDataset, task: str, frame_size: int, sample_rate: int): - """Resolve dataset cache path - - Args: - fpath (Path|None): File path - ds (HKDataset): Dataset - task (str): Task - frame_size (int): Frame size - sample_rate (int): Sampling rate - - Returns: - Path|None: Resolved path - """ - if not fpath: - return None - return resolve_template_path( - fpath=fpath, - dataset=ds.name, - task=task, - frame_size=frame_size, - sampling_rate=sample_rate, - ) + return ds.prefetch(tf.data.AUTOTUNE) def load_train_datasets( datasets: list[HKDataset], - params: HKTrainParams, - ds_spec: tuple[tf.TensorSpec, tf.TensorSpec], + params: HKTaskParams, ) -> tuple[tf.data.Dataset, tf.data.Dataset]: - """Load training and validation datasets - - Args: - datasets (list[HKDataset]): Datasets - params (HKTrainParams): Training parameters - ds_spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - - Returns: - tuple[tf.data.Dataset, tf.data.Dataset]: Train and validation datasets - """ - id_generator = functools.partial(uniform_id_generator, repeat=True) - train_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, - ) - val_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=None, - spec=ds_spec, - num_classes=params.num_classes, - ) - train_datasets = [] val_datasets = [] for ds in datasets: - val_file = resolve_ds_cache_path( - params.val_file, + dataloader: HKDataloader = DiagnosticDataloaderFactory.get(ds.name)( ds=ds, - task="diagnostic", frame_size=params.frame_size, - sample_rate=params.sampling_rate, - ) - data_generator = get_ds_generator( - ds=ds, - frame_size=params.frame_size, - samples_per_patient=params.samples_per_patient, - target_rate=params.sampling_rate, + sampling_rate=params.sampling_rate, label_map=params.class_map, ) - train_ds, val_ds = train_val_dataloader( - ds=ds, - spec=ds_spec, - data_generator=data_generator, - id_generator=id_generator, + train_patients, val_patients = dataloader.split_train_val_patients( train_patients=params.train_patients, val_patients=params.val_patients, - val_pt_samples=params.val_samples_per_patient, - val_file=val_file, - val_size=params.val_size, - label_map=get_ds_label_map(ds, params.class_map), - label_type=get_ds_label_type(ds), - preprocess=train_prepare, - val_preprocess=val_prepare, - num_workers=params.data_parallelism, + ) + + train_ds = dataloader.create_dataloader( + patient_ids=train_patients, samples_per_patient=params.samples_per_patient, shuffle=True + ) + + val_ds = dataloader.create_dataloader( + patient_ids=val_patients, samples_per_patient=params.val_samples_per_patient, shuffle=False ) train_datasets.append(train_ds) val_datasets.append(val_ds) # END FOR - ds_weights = np.array([d.weight for d in params.datasets]) - ds_weights = ds_weights / ds_weights.sum() + ds_weights = None + if params.dataset_weights: + ds_weights = np.array(params.dataset_weights) + ds_weights = ds_weights / ds_weights.sum() train_ds = tf.data.Dataset.sample_from_datasets(train_datasets, weights=ds_weights) val_ds = tf.data.Dataset.sample_from_datasets(val_datasets, weights=ds_weights) # Shuffle and batch datasets for training - train_ds = ( - train_ds.shuffle( - buffer_size=params.buffer_size, - reshuffle_each_iteration=True, - ) - .batch( - batch_size=params.batch_size, - drop_remainder=False, - num_parallel_calls=tf.data.AUTOTUNE, - ) - .prefetch(buffer_size=tf.data.AUTOTUNE) + train_ds = create_data_pipeline( + ds=train_ds, + sampling_rate=params.sampling_rate, + batch_size=params.batch_size, + buffer_size=params.buffer_size, + augmentations=params.augmentations + params.preprocesses, ) - val_ds = val_ds.batch( + + val_ds = create_data_pipeline( + ds=val_ds, + sampling_rate=params.sampling_rate, batch_size=params.batch_size, - drop_remainder=True, - num_parallel_calls=tf.data.AUTOTUNE, + buffer_size=params.buffer_size, + augmentations=params.preprocesses, ) + + # If given fixed val size or steps, then capture and cache + val_steps_per_epoch = params.val_size // params.batch_size if params.val_size else params.val_steps_per_epoch + if val_steps_per_epoch: + logger.info(f"Validation steps per epoch: {val_steps_per_epoch}") + val_ds = val_ds.take(val_steps_per_epoch).cache() + return train_ds, val_ds def load_test_dataset( datasets: list[HKDataset], - params: HKTestParams | HKExportParams, - ds_spec: tuple[tf.TensorSpec, tf.TensorSpec], + params: HKTaskParams, ) -> tf.data.Dataset: - """Load test dataset - - Args: - datasets (list[HKDataset]): Datasets - params (HKTestParams|HKExportParams): Test parameters - ds_spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - - Returns: - tf.data.Dataset: Test dataset - """ - - id_generator = functools.partial(uniform_id_generator, repeat=True) - test_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=None, # params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, - ) test_datasets = [] for ds in datasets: - test_file = resolve_ds_cache_path( - fpath=params.test_file, + dataloader: HKDataloader = DiagnosticDataloaderFactory.get(ds.name)( ds=ds, - task="diagnostic", frame_size=params.frame_size, - sample_rate=params.sampling_rate, + sampling_rate=params.sampling_rate, + label_map=params.class_map, ) - data_generator = get_ds_generator( - ds=ds, - frame_size=params.frame_size, + test_patients = dataloader.test_patient_ids(params.test_patients) + test_ds = dataloader.create_dataloader( + patient_ids=test_patients, samples_per_patient=params.test_samples_per_patient, - target_rate=params.sampling_rate, - ) - test_ds = test_dataloader( - ds=ds, - spec=ds_spec, - data_generator=data_generator, - id_generator=id_generator, - test_patients=params.test_patients, - test_file=test_file, - label_map=get_ds_label_map(ds, params.class_map), - label_type=get_ds_label_type(ds), - preprocess=test_prepare, - num_workers=params.data_parallelism, + shuffle=False, ) test_datasets.append(test_ds) # END FOR - ds_weights = np.array([d.weight for d in params.datasets]) - ds_weights = ds_weights / ds_weights.sum() + ds_weights = None + if params.dataset_weights: + ds_weights = np.array(params.dataset_weights) + ds_weights = ds_weights / ds_weights.sum() test_ds = tf.data.Dataset.sample_from_datasets(test_datasets, weights=ds_weights) - # END WITH + test_ds = create_data_pipeline( + ds=test_ds, + sampling_rate=params.sampling_rate, + batch_size=params.batch_size, + augmentations=params.preprocesses, + ) + + if params.test_size: + batch_size = getattr(params, "batch_size", 1) + test_ds = test_ds.take(params.test_size // batch_size).cache() + return test_ds diff --git a/heartkit/tasks/diagnostic/demo.py b/heartkit/tasks/diagnostic/demo.py index c3219c29..491ff7cb 100644 --- a/heartkit/tasks/diagnostic/demo.py +++ b/heartkit/tasks/diagnostic/demo.py @@ -5,22 +5,21 @@ import plotly.graph_objects as go from plotly.subplots import make_subplots from tqdm import tqdm +import neuralspot_edge as nse -from ...datasets.utils import uniform_id_generator -from ...defines import HKDemoParams +from ...defines import HKTaskParams from ...rpc import BackendFactory -from ...utils import setup_logger -from ..utils import load_datasets -from .datasets import preprocess +from ...datasets import DatasetFactory -logger = setup_logger(__name__) +logger = nse.utils.setup_logger(__name__) -def demo(params: HKDemoParams): + +def demo(params: HKTaskParams): """Run demo for model Args: - params (HKDemoParams): Demo parameters + params (HKTaskParams): Demo parameters """ bg_color = "rgba(38,42,50,1.0)" @@ -31,7 +30,7 @@ def demo(params: HKDemoParams): params.demo_size = params.demo_size or 2 * params.frame_size # Load backend inference engine - runner = BackendFactory.create(params.backend, params=params) + runner = BackendFactory.get(params.backend)(params=params) # classes = sorted(list(set(params.class_map.values()))) class_names = params.class_names or [f"Class {i}" for i in range(params.num_classes)] @@ -45,11 +44,11 @@ def demo(params: HKDemoParams): # ) # Load data - dsets = load_datasets(datasets=params.datasets) - ds = random.choice(dsets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] + ds = random.choice(datasets) ds_gen = ds.signal_generator( - patient_generator=uniform_id_generator(ds.get_test_patient_ids(), repeat=False), + patient_generator=nse.utils.uniform_id_generator(ds.get_test_patient_ids(), repeat=False), frame_size=params.demo_size, samples_per_patient=5, target_rate=params.sampling_rate, @@ -65,7 +64,8 @@ def demo(params: HKDemoParams): start, stop = x.shape[0] - params.frame_size, x.shape[0] else: start, stop = i, i + params.frame_size - xx = preprocess(x[start:stop], sample_rate=params.sampling_rate, preprocesses=params.preprocesses) + # xx = preprocess(x[start:stop], sample_rate=params.sampling_rate, preprocesses=params.preprocesses) + xx = x[start:stop] xx = xx.reshape(feat_shape) runner.set_inputs(xx) runner.perform_inference() diff --git a/heartkit/tasks/diagnostic/evaluate.py b/heartkit/tasks/diagnostic/evaluate.py index f1f52972..94b5593f 100644 --- a/heartkit/tasks/diagnostic/evaluate.py +++ b/heartkit/tasks/diagnostic/evaluate.py @@ -1,53 +1,37 @@ -import logging import os import numpy as np import pandas as pd -import tensorflow as tf from sklearn.metrics import classification_report, f1_score - import neuralspot_edge as nse -from ...defines import HKTestParams -from ...utils import set_random_seed, setup_logger -from ..utils import load_datasets -from .datasets import load_test_dataset -logger = setup_logger(__name__) +from ...defines import HKTaskParams +from ...datasets import DatasetFactory +from .datasets import load_test_dataset -def evaluate(params: HKTestParams): +def evaluate(params: HKTaskParams): """Evaluate model Args: - params (HKTestParams): Evaluation parameters + params (HKTaskParams): Evaluation parameters """ - params.threshold = params.threshold or 0.5 - - params.seed = set_random_seed(params.seed) - logger.debug(f"Random seed {params.seed}") - os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "test.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "test.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) - - # classes = sorted(list(set(params.class_map.values()))) - class_names = params.class_names or [f"Class {i}" for i in range(params.num_classes)] + params.threshold = params.threshold or 0.5 - feat_shape = (params.frame_size, 1) - class_shape = (params.num_classes,) + params.seed = nse.utils.set_random_seed(params.seed) + logger.debug(f"Random seed {params.seed}") - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype=tf.float32), - tf.TensorSpec(shape=class_shape, dtype=tf.int32), - ) + class_names = params.class_names or [f"Class {i}" for i in range(params.num_classes)] - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - test_ds = load_test_dataset(datasets=datasets, params=params, ds_spec=ds_spec) - test_x, test_y = next(test_ds.batch(params.test_size).as_numpy_iterator()) + test_ds = load_test_dataset(datasets=datasets, params=params) + test_x = np.concatenate([x for x, _ in test_ds.as_numpy_iterator()]) + test_y = np.concatenate([y for _, y in test_ds.as_numpy_iterator()]) logger.debug("Loading model") model = nse.models.load_model(params.model_file) @@ -62,7 +46,7 @@ def evaluate(params: HKTestParams): y_pred = y_prob >= params.threshold cm_path = params.job_dir / "confusion_matrix_test.png" - nse.plotting.cm.multilabel_confusion_matrix_plot( + nse.plotting.multilabel_confusion_matrix_plot( y_true=y_true, y_pred=y_pred, labels=class_names, diff --git a/heartkit/tasks/diagnostic/export.py b/heartkit/tasks/diagnostic/export.py index 28a0a032..3e202cc0 100644 --- a/heartkit/tasks/diagnostic/export.py +++ b/heartkit/tasks/diagnostic/export.py @@ -1,59 +1,43 @@ -import logging import os import shutil -import keras import numpy as np -import tensorflow as tf -from sklearn.metrics import f1_score - +import keras import neuralspot_edge as nse -from ...defines import HKExportParams -from ...utils import setup_logger -from ..utils import load_datasets -from .datasets import load_test_dataset -logger = setup_logger(__name__) +from ...defines import HKTaskParams +from ...datasets import DatasetFactory +from .datasets import load_test_dataset -def export(params: HKExportParams): +def export(params: HKTaskParams): """Export model Args: - params (HKExportParams): Deployment parameters + params (HKTaskParams): Deployment parameters """ - params.threshold = params.threshold or 0.5 - os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "export.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "export.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) + params.threshold = params.threshold or 0.5 tfl_model_path = params.job_dir / "model.tflite" tflm_model_path = params.job_dir / "model_buffer.h" - # classes = sorted(list(set(params.class_map.values()))) - # class_names = params.class_names or [f"Class {i}" for i in range(params.num_classes)] - feat_shape = (params.frame_size, 1) - class_shape = (params.num_classes,) - - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype="float32"), - tf.TensorSpec(shape=class_shape, dtype="int32"), - ) - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - test_ds = load_test_dataset(datasets=datasets, params=params, ds_spec=ds_spec) - test_x, test_y = next(test_ds.batch(params.test_size).as_numpy_iterator()) + test_ds = load_test_dataset(datasets=datasets, params=params) + test_x = np.concatenate([x for x, _ in test_ds.as_numpy_iterator()]) + test_y = np.concatenate([y for _, y in test_ds.as_numpy_iterator()]) # Load model and set fixed batch size of 1 + logger.debug("Loading trained model") model = nse.models.load_model(params.model_file) - inputs = keras.Input(shape=ds_spec[0].shape, batch_size=1, name="input", dtype=ds_spec[0].dtype) + inputs = keras.Input(shape=feat_shape, batch_size=1, name="input", dtype="float32") model(inputs) flops = nse.metrics.flops.get_flops(model, batch_size=1, fpath=params.job_dir / "model_flops.log") @@ -62,6 +46,7 @@ def export(params: HKExportParams): logger.debug(f"Converting model to TFLite (quantization={params.quantization.mode})") converter = nse.converters.tflite.TfLiteKerasConverter(model=model) + tflite_content = converter.convert( test_x=test_x, quantization=params.quantization.format, @@ -87,25 +72,28 @@ def export(params: HKExportParams): tflite.compile() # Verify TFLite results match TF results - logger.debug("Validating model results") + metrics = [keras.metrics.CategoricalAccuracy(name="acc"), keras.metrics.F1Score(name="f1", average="weighted")] + + if params.val_metric not in [m.name for m in metrics]: + raise ValueError(f"Metric {params.val_metric} not supported") + + logger.info("Validating model results") y_true = test_y - y_pred_tf = model.predict(test_x) >= params.threshold - y_pred_tfl = tflite.predict(x=test_x) >= params.threshold + y_pred_tf = model.predict(test_x) + y_pred_tfl = tflite.predict(x=test_x) - tf_acc = np.sum(y_true == y_pred_tf) / y_true.size - tf_f1 = f1_score(y_true, y_pred_tf, average="weighted") - logger.info(f"[TF SET] ACC={tf_acc:.2%}, F1={tf_f1:.2%}") + tf_rst = nse.metrics.compute_metrics(metrics, y_true, y_pred_tf) + tfl_rst = nse.metrics.compute_metrics(metrics, y_true, y_pred_tfl) + logger.info("[TF METRICS] " + " ".join([f"{k.upper()}={v:.2%}" for k, v in tf_rst.items()])) + logger.info("[TFL METRICS] " + " ".join([f"{k.upper()}={v:.2%}" for k, v in tfl_rst.items()])) - tfl_acc = np.sum(y_true == y_pred_tfl) / y_true.size - tfl_f1 = f1_score(y_true, y_pred_tfl, average="weighted") - logger.info(f"[TFL SET] ACC={tfl_acc:.2%}, F1={tfl_f1:.2%}") + metric_diff = abs(tf_rst[params.val_metric] - tfl_rst[params.val_metric]) # Check accuracy hit - tfl_acc_drop = max(0, tf_acc - tfl_acc) - if params.val_acc_threshold is not None and (1 - tfl_acc_drop) < params.val_acc_threshold: - logger.warning(f"TFLite accuracy dropped by {tfl_acc_drop:0.2%}") - elif params.val_acc_threshold: - logger.info(f"Validation passed ({tfl_acc_drop:0.2%})") + if params.val_metric_threshold is not None and metric_diff > params.val_metric_threshold: + logger.warning(f"TFLite accuracy dropped by {metric_diff:0.2%}") + elif params.val_metric_threshold: + logger.info(f"Validation passed ({metric_diff:0.2%})") if params.tflm_file and tflm_model_path != params.tflm_file: logger.debug(f"Copying TFLM header to {params.tflm_file}") diff --git a/heartkit/tasks/diagnostic/train.py b/heartkit/tasks/diagnostic/train.py index db894684..0dd73f6c 100644 --- a/heartkit/tasks/diagnostic/train.py +++ b/heartkit/tasks/diagnostic/train.py @@ -1,90 +1,66 @@ -import logging import os import keras import numpy as np import pandas as pd -import tensorflow as tf import wandb from sklearn.metrics import classification_report, f1_score from wandb.keras import WandbMetricsLogger, WandbModelCheckpoint import neuralspot_edge as nse -from ...defines import HKTrainParams -from ...utils import env_flag, set_random_seed, setup_logger -from ..utils import load_datasets -from .datasets import load_train_datasets -from .utils import create_model -logger = setup_logger(__name__) +from ...defines import HKTaskParams +from ...datasets import DatasetFactory +from .datasets import load_train_datasets +from ...models import ModelFactory -def train(params: HKTrainParams): +def train(params: HKTaskParams): """Train model Args: - params (HKTrainParams): Training parameters + params (HKTaskParams): Training parameters """ - params.threshold = params.threshold or 0.5 - - params.seed = set_random_seed(params.seed) - logger.debug(f"Random seed {params.seed}") - os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "train.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "train.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) + params.threshold = params.threshold or 0.5 + + params.seed = nse.utils.set_random_seed(params.seed) + logger.debug(f"Random seed {params.seed}") with open(params.job_dir / "train_config.json", "w", encoding="utf-8") as fp: fp.write(params.model_dump_json(indent=2)) - if env_flag("WANDB"): - wandb.init( - project=params.project, - entity="ambiq", - dir=params.job_dir, - ) + if nse.utils.env_flag("WANDB"): + wandb.init(project=params.project, entity="ambiq", dir=params.job_dir) wandb.config.update(params.model_dump()) # END IF - # classes = sorted(list(set(params.class_map.values()))) class_names = params.class_names or [f"Class {i}" for i in range(params.num_classes)] feat_shape = (params.frame_size, 1) - class_shape = (params.num_classes,) - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype="float32"), - tf.TensorSpec(shape=class_shape, dtype="int32"), - ) - - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] train_ds, val_ds = load_train_datasets( datasets=datasets, params=params, - ds_spec=ds_spec, ) - test_labels = np.array([label.numpy() for _, label in val_ds]) - y_true = np.concatenate(test_labels) + y_true = np.concatenate([y for _, y in val_ds.as_numpy_iterator()]) class_weights = 0.25 if params.class_weights == "balanced": n_samples = np.sum(y_true) class_weights = n_samples / (params.num_classes * np.sum(y_true, axis=0)) class_weights = (class_weights + class_weights.mean()) / 2 # Smooth out + class_weights = class_weights.tolist() # END IF logger.debug(f"Class weights: {class_weights}") - inputs = keras.Input( - shape=ds_spec[0].shape, - batch_size=None, - name="input", - dtype=ds_spec[0].dtype.name, - ) + inputs = keras.Input(shape=feat_shape, name="input", dtype="float32") if params.resume and params.model_file: logger.debug(f"Loading model from file {params.model_file}") @@ -92,39 +68,31 @@ def train(params: HKTrainParams): params.model_file = None else: logger.debug("Creating model from scratch") - model = create_model( - inputs, + if params.architecture is None: + raise ValueError("Model architecture must be specified") + model = ModelFactory.get(params.architecture.name)( + x=inputs, + params=params.architecture.params, num_classes=params.num_classes, - architecture=params.architecture, ) # END IF flops = nse.metrics.flops.get_flops(model, batch_size=1, fpath=params.job_dir / "model_flops.log") - if params.lr_cycles > 1: - scheduler = keras.optimizers.schedules.CosineDecayRestarts( - initial_learning_rate=params.lr_rate, - first_decay_steps=int(0.1 * params.steps_per_epoch * params.epochs), - t_mul=1.65 / (0.1 * params.lr_cycles * (params.lr_cycles - 1)), - m_mul=0.4, - ) - else: - scheduler = keras.optimizers.schedules.CosineDecay( - initial_learning_rate=params.lr_rate, - decay_steps=params.steps_per_epoch * params.epochs, - ) - # END IF + t_mul = 1 + first_steps = (params.steps_per_epoch * params.epochs) / (np.power(params.lr_cycles, t_mul) - t_mul + 1) + scheduler = keras.optimizers.schedules.CosineDecayRestarts( + initial_learning_rate=params.lr_rate, + first_decay_steps=np.ceil(first_steps), + t_mul=t_mul, + m_mul=0.5, + ) optimizer = keras.optimizers.Adam(scheduler) loss = keras.losses.BinaryCrossentropy(from_logits=True, label_smoothing=params.label_smoothing) - # loss = keras.losses.BinaryFocalCrossentropy( - # apply_class_balancing=False, - # alpha=class_weights, - # from_logits=True, - # label_smoothing=params.label_smoothing, - # ) + metrics = [ keras.metrics.BinaryAccuracy(name="acc"), - # tfa.MultiF1Score(name="f1", average="weighted"), + keras.metrics.F1Score(name="f1", average="weighted"), ] if params.resume and params.weights_file: @@ -135,12 +103,11 @@ def train(params: HKTrainParams): params.model_file = params.job_dir / "model.keras" model.compile(optimizer=optimizer, loss=loss, metrics=metrics) - model(inputs) model.summary(print_fn=logger.info) logger.debug(f"Model requires {flops/1e6:0.2f} MFLOPS") ModelCheckpoint = keras.callbacks.ModelCheckpoint - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): ModelCheckpoint = WandbModelCheckpoint model_callbacks = [ keras.callbacks.EarlyStopping( @@ -148,31 +115,32 @@ def train(params: HKTrainParams): patience=max(int(0.25 * params.epochs), 1), mode="max" if params.val_metric == "f1" else "auto", restore_best_weights=True, + verbose=params.verbose - 1, ), ModelCheckpoint( filepath=str(params.model_file), monitor=f"val_{params.val_metric}", save_best_only=True, mode="max" if params.val_metric == "f1" else "auto", - verbose=1, + verbose=params.verbose - 1, ), keras.callbacks.CSVLogger(params.job_dir / "history.csv"), ] - if env_flag("TENSORBOARD"): + if nse.utils.env_flag("TENSORBOARD"): model_callbacks.append( keras.callbacks.TensorBoard( log_dir=params.job_dir, write_steps_per_second=True, ) ) - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): model_callbacks.append(WandbMetricsLogger()) try: model.fit( train_ds, steps_per_epoch=params.steps_per_epoch, - verbose=2, + verbose=params.verbose, epochs=params.epochs, validation_data=val_ds, callbacks=model_callbacks, @@ -183,13 +151,12 @@ def train(params: HKTrainParams): logger.debug(f"Model saved to {params.model_file}") # Get full validation results - keras.models.load_model(params.model_file) logger.debug("Performing full validation") y_pred = model.predict(val_ds) - y_pred = y_pred >= params.threshold - cm_path = params.job_dir / "confusion_matrix.png" + # y_pred = y_pred >= params.threshold - nse.plotting.cm.multilabel_confusion_matrix_plot( + cm_path = params.job_dir / "confusion_matrix.png" + nse.plotting.multilabel_confusion_matrix_plot( y_true=y_true, y_pred=y_pred, labels=class_names, diff --git a/heartkit/tasks/diagnostic/utils.py b/heartkit/tasks/diagnostic/utils.py deleted file mode 100644 index a4554bd9..00000000 --- a/heartkit/tasks/diagnostic/utils.py +++ /dev/null @@ -1,97 +0,0 @@ -import keras -from neuralspot_edge.models.efficientnet import ( - EfficientNetV2, - EfficientNetParams, - MBConvParams, -) -from rich.console import Console - -from ...defines import ModelArchitecture -from ...models import ModelFactory - -console = Console() - - -def create_model(inputs: keras.KerasTensor, num_classes: int, architecture: ModelArchitecture | None) -> keras.Model: - """Generate model or use default - - Args: - inputs (keras.KerasTensor): Model inputs - num_classes (int): Number of classes - architecture (ModelArchitecture|None): Model - - Returns: - keras.Model: Model - """ - if architecture: - return ModelFactory.get(architecture.name)( - x=inputs, - params=architecture.params, - num_classes=num_classes, - ) - - return default_model(inputs=inputs, num_classes=num_classes) - - -def default_model( - inputs: keras.KerasTensor, - num_classes: int, -) -> keras.Model: - """Reference model - - Args: - inputs (keras.KerasTensor): Model inputs - num_classes (int): Number of classes - - Returns: - keras.Model: Model - """ - - blocks = [ - MBConvParams( - filters=32, - depth=2, - ex_ratio=1, - kernel_size=(1, 3), - strides=(1, 2), - se_ratio=2, - ), - MBConvParams( - filters=48, - depth=1, - ex_ratio=1, - kernel_size=(1, 3), - strides=(1, 2), - se_ratio=4, - ), - MBConvParams( - filters=64, - depth=2, - ex_ratio=1, - kernel_size=(1, 3), - strides=(1, 2), - se_ratio=4, - ), - MBConvParams( - filters=80, - depth=1, - ex_ratio=1, - kernel_size=(1, 3), - strides=(1, 2), - se_ratio=4, - ), - ] - return EfficientNetV2( - inputs, - params=EfficientNetParams( - input_filters=24, - input_kernel_size=(1, 3), - input_strides=(1, 2), - blocks=blocks, - output_filters=0, - include_top=True, - dropout=0.0, - drop_connect_rate=0.0, - ), - num_classes=num_classes, - ) diff --git a/heartkit/tasks/foundation/__init__.py b/heartkit/tasks/foundation/__init__.py index 83d16b59..233e1b4b 100644 --- a/heartkit/tasks/foundation/__init__.py +++ b/heartkit/tasks/foundation/__init__.py @@ -1,6 +1,7 @@ -from ...defines import HKDemoParams, HKExportParams, HKTestParams, HKTrainParams +from ...defines import HKTaskParams from ..task import HKTask from . import datasets +from .datasets import FoundationTaskFactory from .demo import demo from .evaluate import evaluate from .export import export @@ -11,17 +12,17 @@ class FoundationTask(HKTask): """HeartKit Foundation Task""" @staticmethod - def train(params: HKTrainParams): + def train(params: HKTaskParams): train(params) @staticmethod - def evaluate(params: HKTestParams): + def evaluate(params: HKTaskParams): evaluate(params) @staticmethod - def export(params: HKExportParams): + def export(params: HKTaskParams): export(params) @staticmethod - def demo(params: HKDemoParams): + def demo(params: HKTaskParams): demo(params) diff --git a/heartkit/tasks/foundation/dataloaders/__init__.py b/heartkit/tasks/foundation/dataloaders/__init__.py index 80b29d60..1f10c830 100644 --- a/heartkit/tasks/foundation/dataloaders/__init__.py +++ b/heartkit/tasks/foundation/dataloaders/__init__.py @@ -1,2 +1,10 @@ -from .lsad import lsad_data_generator -from .ptbxl import ptbxl_data_generator +import neuralspot_edge as nse + +from ....datasets import HKDataloader + +from .lsad import LsadDataloader +from .ptbxl import PtbxlDataloader + +FoundationTaskFactory = nse.utils.create_factory(factory="FoundationTaskFactory", type=HKDataloader) +FoundationTaskFactory.register("lsad", LsadDataloader) +FoundationTaskFactory.register("ptbxl", PtbxlDataloader) diff --git a/heartkit/tasks/foundation/dataloaders/lsad.py b/heartkit/tasks/foundation/dataloaders/lsad.py index 5f5ec85d..0822ecbb 100644 --- a/heartkit/tasks/foundation/dataloaders/lsad.py +++ b/heartkit/tasks/foundation/dataloaders/lsad.py @@ -4,55 +4,60 @@ import numpy as np import numpy.typing as npt import physiokit as pk - -from ....datasets import LsadDataset, PatientGenerator - - -def lsad_data_generator( - patient_generator: PatientGenerator, - ds: LsadDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, -) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: - """Generate frames using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: LsadDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - - Returns: - Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Sample generator - - """ - input_size = int(np.round((ds.sampling_rate / target_rate) * frame_size)) - data_cache = {} - for pt in patient_generator: - if pt not in data_cache: - with ds.patient_data(pt) as h5: - data_cache[pt] = h5["data"][:] - data = data_cache[pt] - # with ds.patient_data(pt) as h5: - # data = h5["data"][:] - - for _ in range(samples_per_patient): - leads = random.sample(ds.leads, k=2) - lead_p1 = leads[0] - lead_p2 = leads[1] - start_p1 = np.random.randint(0, data.shape[1] - input_size) - start_p2 = np.random.randint(0, data.shape[1] - input_size) - # start_p2 = start_p1 - - x1 = np.nan_to_num(data[lead_p1, start_p1 : start_p1 + input_size].squeeze()).astype(np.float32) - x2 = np.nan_to_num(data[lead_p2, start_p2 : start_p2 + input_size].squeeze()).astype(np.float32) - - if ds.sampling_rate != target_rate: - x1 = pk.signal.resample_signal(x1, ds.sampling_rate, target_rate, axis=0) - x2 = pk.signal.resample_signal(x2, ds.sampling_rate, target_rate, axis=0) - # END IF - yield x1, x2 +import neuralspot_edge as nse + +from ....datasets import HKDataloader, LsadDataset + + +class LsadDataloader(HKDataloader): + def __init__(self, ds: LsadDataset, **kwargs): + """Lsad Dataloader for training foundation tasks + + Args: + ds (LsadDataset): LsadDataset + """ + super().__init__(ds=ds, **kwargs) + + def patient_data_generator( + self, + patient_id: int, + samples_per_patient: list[int], + ): + input_size = int(np.ceil((self.ds.sampling_rate / self.sampling_rate) * self.frame_size)) + + with self.ds.patient_data(patient_id) as pt: + data = pt["data"][:] + + for _ in range(samples_per_patient): + leads = random.sample(self.ds.leads, k=2) + lead_p1 = leads[0] + lead_p2 = leads[1] + start_p1 = np.random.randint(0, data.shape[1] - input_size) + start_p2 = np.random.randint(0, data.shape[1] - input_size) + # start_p2 = start_p1 + + x1 = np.nan_to_num(data[lead_p1, start_p1 : start_p1 + input_size].squeeze()).astype(np.float32) + x2 = np.nan_to_num(data[lead_p2, start_p2 : start_p2 + input_size].squeeze()).astype(np.float32) + + if self.ds.sampling_rate != self.sampling_rate: + x1 = pk.signal.resample_signal(x1, self.ds.sampling_rate, self.sampling_rate, axis=0) + x2 = pk.signal.resample_signal(x2, self.ds.sampling_rate, self.sampling_rate, axis=0) + x1 = x1[: self.frame_size] + x2 = x2[: self.frame_size] + # END IF + x1 = np.reshape(x1, (-1, 1)) + x2 = np.reshape(x2, (-1, 1)) + yield x1, x2 + # END FOR + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + for pt_id in nse.utils.uniform_id_generator(patient_ids, shuffle=shuffle): + for x1, x2 in self.patient_data_generator(pt_id, samples_per_patient): + yield x1, x2 + # END FOR # END FOR - # END FOR diff --git a/heartkit/tasks/foundation/dataloaders/ptbxl.py b/heartkit/tasks/foundation/dataloaders/ptbxl.py index 35a93c9d..9a2b3beb 100644 --- a/heartkit/tasks/foundation/dataloaders/ptbxl.py +++ b/heartkit/tasks/foundation/dataloaders/ptbxl.py @@ -4,55 +4,55 @@ import numpy as np import numpy.typing as npt import physiokit as pk - -from ....datasets import PatientGenerator, PtbxlDataset - - -def ptbxl_data_generator( - patient_generator: PatientGenerator, - ds: PtbxlDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, -) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: - """Generate frames using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: PtbxlDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - - Returns: - Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Sample generator - - """ - input_size = int(np.round((ds.sampling_rate / target_rate) * frame_size)) - data_cache = {} - for pt in patient_generator: - if pt not in data_cache: - with ds.patient_data(pt) as h5: - data_cache[pt] = h5["data"][:] - data = data_cache[pt] - # with ds.patient_data(pt) as h5: - # data = h5["data"][:] - - for _ in range(samples_per_patient): - leads = random.sample(ds.leads, k=2) - lead_p1 = leads[0] - lead_p2 = leads[1] - start_p1 = np.random.randint(0, data.shape[1] - input_size) - start_p2 = np.random.randint(0, data.shape[1] - input_size) - # start_p2 = start_p1 - - x1 = np.nan_to_num(data[lead_p1, start_p1 : start_p1 + input_size].squeeze()).astype(np.float32) - x2 = np.nan_to_num(data[lead_p2, start_p2 : start_p2 + input_size].squeeze()).astype(np.float32) - - if ds.sampling_rate != target_rate: - x1 = pk.signal.resample_signal(x1, ds.sampling_rate, target_rate, axis=0) - x2 = pk.signal.resample_signal(x2, ds.sampling_rate, target_rate, axis=0) - # END IF - yield x1, x2 +import neuralspot_edge as nse + +from ....datasets import HKDataloader, PtbxlDataset + + +class PtbxlDataloader(HKDataloader): + def __init__(self, ds: PtbxlDataset, **kwargs): + super().__init__(ds=ds, **kwargs) + + def patient_data_generator( + self, + patient_id: int, + samples_per_patient: list[int], + ): + input_size = int(np.ceil((self.ds.sampling_rate / self.sampling_rate) * self.frame_size)) + + with self.ds.patient_data(patient_id) as pt: + data = pt["data"][:] + + for _ in range(samples_per_patient): + leads = random.sample(self.ds.leads, k=2) + lead_p1 = leads[0] + lead_p2 = leads[1] + start_p1 = np.random.randint(0, data.shape[1] - input_size) + start_p2 = np.random.randint(0, data.shape[1] - input_size) + # start_p2 = start_p1 + + x1 = np.nan_to_num(data[lead_p1, start_p1 : start_p1 + input_size].squeeze()).astype(np.float32) + x2 = np.nan_to_num(data[lead_p2, start_p2 : start_p2 + input_size].squeeze()).astype(np.float32) + + if self.ds.sampling_rate != self.sampling_rate: + x1 = pk.signal.resample_signal(x1, self.ds.sampling_rate, self.sampling_rate, axis=0) + x2 = pk.signal.resample_signal(x2, self.ds.sampling_rate, self.sampling_rate, axis=0) + x1 = x1[: self.frame_size] + x2 = x2[: self.frame_size] + # END IF + x1 = np.reshape(x1, (-1, 1)) + x2 = np.reshape(x2, (-1, 1)) + yield x1, x2 + # END FOR + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + for pt_id in nse.utils.uniform_id_generator(patient_ids, shuffle=shuffle): + for x1, x2 in self.patient_data_generator(pt_id, samples_per_patient): + yield x1, x2 + # END FOR # END FOR - # END FOR diff --git a/heartkit/tasks/foundation/datasets.py b/heartkit/tasks/foundation/datasets.py index a7655eb0..260fc8fe 100644 --- a/heartkit/tasks/foundation/datasets.py +++ b/heartkit/tasks/foundation/datasets.py @@ -1,301 +1,149 @@ -import functools -import logging -from pathlib import Path - import numpy as np -import numpy.typing as npt import tensorflow as tf +import neuralspot_edge as nse -from ...datasets import ( - HKDataset, - augment_pipeline, - preprocess_pipeline, - uniform_id_generator, -) -from ...datasets.dataloader import test_dataloader, train_val_dataloader -from ...defines import ( - AugmentationParams, - HKExportParams, - HKTestParams, - HKTrainParams, - PreprocessParams, -) -from ...utils import resolve_template_path -from .dataloaders import lsad_data_generator, ptbxl_data_generator - -logger = logging.getLogger(__name__) - - -def preprocess(x: npt.NDArray, preprocesses: list[PreprocessParams], sample_rate: float) -> npt.NDArray: - """Preprocess data pipeline - - Args: - x (npt.NDArray): Input data - preprocesses (list[PreprocessParams]): Preprocess parameters - sample_rate (float): Sample rate - - Returns: - npt.NDArray: Preprocessed data - """ - return preprocess_pipeline(x, preprocesses=preprocesses, sample_rate=sample_rate) - - -def augment(x: npt.NDArray, augmentations: list[AugmentationParams], sample_rate: float) -> npt.NDArray: - """Augment data pipeline - - Args: - x (npt.NDArray): Input data - augmentations (list[AugmentationParams]): Augmentation parameters - sample_rate (float): Sample rate - - Returns: - npt.NDArray: Augmented data - """ - - return augment_pipeline(x=x, augmentations=augmentations, sample_rate=sample_rate) - - -def prepare( - x_y: tuple[npt.NDArray, npt.NDArray], - sample_rate: float, - preprocesses: list[PreprocessParams], - augmentations: list[AugmentationParams], - spec: tuple[tf.TensorSpec, tf.TensorSpec], - num_classes: int, -) -> tuple[npt.NDArray, npt.NDArray]: - """Prepare dataset - - Args: - x_y (tuple[npt.NDArray, npt.NDArray]): Input data - sample_rate (float): Sampling rate - preprocesses (list[PreprocessParams]): Preprocessing pipeline - augmentations (list[AugmentationParams]): Augmentation pipeline - spec (tuple[tf.TensorSpec, tf.TensorSpec]): Spec - num_classes (int): Number of classes - - Returns: - tuple[npt.NDArray, npt.NDArray]: Prepared data - """ - x, y = x_y[0].copy(), x_y[1].copy() - - if augmentations: - x = augment(x, augmentations, sample_rate) - y = augment(y, augmentations, sample_rate) - # END IF - - if preprocesses: - x = preprocess(x, preprocesses, sample_rate) - y = preprocess(y, preprocesses, sample_rate) - # END IF - - x = x.reshape(spec[0].shape) - y = y.reshape(spec[0].shape) - - return x, y +from ...datasets import HKDataset, create_augmentation_pipeline +from ...datasets.dataloader import HKDataloader +from ...defines import HKTaskParams, NamedParams +from .dataloaders import FoundationTaskFactory -def get_data_generator(ds: HKDataset, frame_size: int, samples_per_patient: int, target_rate: int): - """Get task data generator for dataset +logger = nse.utils.setup_logger(__name__) - Args: - ds (HKDataset): Dataset - frame_size (int): Frame size - samples_per_patient (int): Samples per patient - target_rate (int): Target rate - Returns: - callable: Data generator - """ - match ds.name: - case "ptbxl": - data_generator = ptbxl_data_generator - case "lsad": - data_generator = lsad_data_generator - case _: - raise ValueError(f"Dataset {ds.name} not supported") - # END MATCH - return functools.partial( - data_generator, - ds=ds, - frame_size=frame_size, - samples_per_patient=samples_per_patient, - target_rate=target_rate, - ) - - -def resolve_ds_cache_path(fpath: Path | None, ds: HKDataset, task: str, frame_size: int, sample_rate: int): - """Resolve dataset cache path - - Args: - fpath (Path|None): File path - ds (HKDataset): Dataset - task (str): Task - frame_size (int): Frame size - sample_rate (int): Sampling rate - - Returns: - Path|None: Resolved path - """ - if not fpath: - return None - return resolve_template_path( - fpath=fpath, - dataset=ds.name, - task=task, - frame_size=frame_size, - sampling_rate=sample_rate, +def create_data_pipeline( + ds: tf.data.Dataset, + sampling_rate: int, + batch_size: int, + buffer_size: int | None = None, + preprocesses: list[NamedParams] | None = None, + augmentations: list[NamedParams] | None = None, +): + augmenter = create_augmentation_pipeline(augmentations + preprocesses, sampling_rate) + if buffer_size: + ds = ds.shuffle( + buffer_size=buffer_size, + reshuffle_each_iteration=True, + ) + if batch_size: + ds = ds.batch( + batch_size=batch_size, + drop_remainder=True, + num_parallel_calls=tf.data.AUTOTUNE, + ) + ds = ds.map( + lambda x1, x2: { + nse.trainers.SimCLRTrainer.SAMPLES: x1, + nse.trainers.SimCLRTrainer.AUG_SAMPLES_0: augmenter(x1), + nse.trainers.SimCLRTrainer.AUG_SAMPLES_1: augmenter(x2), + }, + num_parallel_calls=tf.data.AUTOTUNE, ) + return ds.prefetch(tf.data.AUTOTUNE) def load_train_datasets( datasets: list[HKDataset], - params: HKTrainParams, - ds_spec: tuple[tf.TensorSpec, tf.TensorSpec], + params: HKTaskParams, ) -> tuple[tf.data.Dataset, tf.data.Dataset]: - """Load training and validation datasets - - Args: - datasets (list[HKDataset]): Datasets - params (HKTrainParams): Training parameters - ds_spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - - Returns: - tuple[tf.data.Dataset, tf.data.Dataset]: Train and validation datasets - """ - - id_generator = functools.partial(uniform_id_generator, repeat=True) - train_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, - ) - train_datasets = [] val_datasets = [] for ds in datasets: - val_file = resolve_ds_cache_path( - params.val_file, - ds=ds, - task="foundation", - frame_size=params.frame_size, - sample_rate=params.sampling_rate, - ) - data_generator = get_data_generator( + dataloader: HKDataloader = FoundationTaskFactory.get(ds.name)( ds=ds, frame_size=params.frame_size, - samples_per_patient=params.samples_per_patient, - target_rate=params.sampling_rate, + sampling_rate=params.sampling_rate, + label_map=params.class_map, ) - - train_ds, val_ds = train_val_dataloader( - ds=ds, - spec=ds_spec, - data_generator=data_generator, - id_generator=id_generator, + train_patients, val_patients = dataloader.split_train_val_patients( train_patients=params.train_patients, val_patients=params.val_patients, - val_pt_samples=params.val_samples_per_patient, - val_file=val_file, - val_size=params.val_size, - label_map=None, - label_type=None, - preprocess=train_prepare, - num_workers=params.data_parallelism, + ) + + train_ds = dataloader.create_dataloader( + patient_ids=train_patients, samples_per_patient=params.samples_per_patient, shuffle=True + ) + + val_ds = dataloader.create_dataloader( + patient_ids=val_patients, samples_per_patient=params.val_samples_per_patient, shuffle=False ) train_datasets.append(train_ds) val_datasets.append(val_ds) # END FOR - ds_weights = np.array([d.weight for d in params.datasets]) - ds_weights = ds_weights / ds_weights.sum() + ds_weights = None + if params.dataset_weights: + ds_weights = np.array(params.dataset_weights) + ds_weights = ds_weights / ds_weights.sum() train_ds = tf.data.Dataset.sample_from_datasets(train_datasets, weights=ds_weights) val_ds = tf.data.Dataset.sample_from_datasets(val_datasets, weights=ds_weights) # Shuffle and batch datasets for training - train_ds = ( - train_ds.shuffle( - buffer_size=params.buffer_size, - reshuffle_each_iteration=True, - ) - .batch( - batch_size=params.batch_size, - drop_remainder=False, - num_parallel_calls=tf.data.AUTOTUNE, - ) - .prefetch(buffer_size=tf.data.AUTOTUNE) + train_ds = create_data_pipeline( + ds=train_ds, + sampling_rate=params.sampling_rate, + batch_size=params.batch_size, + buffer_size=params.buffer_size, + preprocesses=params.preprocesses, + augmentations=params.augmentations, ) - val_ds = val_ds.batch( + + val_ds = create_data_pipeline( + ds=val_ds, + sampling_rate=params.sampling_rate, batch_size=params.batch_size, - drop_remainder=True, - num_parallel_calls=tf.data.AUTOTUNE, + preprocesses=params.preprocesses, + augmentations=params.augmentations, ) + + # If given fixed val size or steps, then capture and cache + val_steps_per_epoch = params.val_size // params.batch_size if params.val_size else params.val_steps_per_epoch + if val_steps_per_epoch: + logger.info(f"Validation steps per epoch: {val_steps_per_epoch}") + val_ds = val_ds.take(val_steps_per_epoch).cache() + return train_ds, val_ds def load_test_dataset( datasets: list[HKDataset], - params: HKTestParams | HKExportParams, - ds_spec: tuple[tf.TensorSpec, tf.TensorSpec], + params: HKTaskParams, ) -> tf.data.Dataset: - """Load test dataset - - Args: - datasets (list[HKDataset]): Datasets - params (HKTestParams|HKExportParams): Test parameters - ds_spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - - Returns: - tf.data.Dataset: Test dataset - """ - - id_generator = functools.partial(uniform_id_generator, repeat=True) - test_prepare = functools.partial( - prepare, - preprocesses=params.preprocesses, - augmentations=params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, - ) - test_datasets = [] for ds in datasets: - test_file = resolve_ds_cache_path( - fpath=params.test_file, + dataloader: HKDataloader = FoundationTaskFactory.get(ds.name)( ds=ds, - task="foundation", frame_size=params.frame_size, - sample_rate=params.sampling_rate, + sampling_rate=params.sampling_rate, + label_map=params.class_map, ) - data_generator = get_data_generator( - ds=ds, - frame_size=params.frame_size, + test_patients = dataloader.test_patient_ids(params.test_patients) + test_ds = dataloader.create_dataloader( + patient_ids=test_patients, samples_per_patient=params.test_samples_per_patient, - target_rate=params.sampling_rate, - ) - - test_ds = test_dataloader( - ds=ds, - spec=ds_spec, - data_generator=data_generator, - id_generator=id_generator, - test_patients=params.test_patients, - test_file=test_file, - label_map=None, - label_type=None, - preprocess=test_prepare, - num_workers=params.data_parallelism, + shuffle=False, ) test_datasets.append(test_ds) # END FOR - ds_weights = np.array([d.weight for d in params.datasets]) - ds_weights = ds_weights / ds_weights.sum() + ds_weights = None + if params.dataset_weights: + ds_weights = np.array(params.dataset_weights) + ds_weights = ds_weights / ds_weights.sum() test_ds = tf.data.Dataset.sample_from_datasets(test_datasets, weights=ds_weights) - # END WITH + test_ds = create_data_pipeline( + ds=test_ds, + sampling_rate=params.sampling_rate, + batch_size=params.batch_size, + preprocesses=params.preprocesses, + augmentations=params.augmentations, + ) + + if params.test_size: + batch_size = getattr(params, "batch_size", 1) + test_ds = test_ds.take(params.test_size // batch_size).cache() + return test_ds diff --git a/heartkit/tasks/foundation/demo.py b/heartkit/tasks/foundation/demo.py index 3272cab8..cc9f509a 100644 --- a/heartkit/tasks/foundation/demo.py +++ b/heartkit/tasks/foundation/demo.py @@ -7,24 +7,22 @@ from plotly.subplots import make_subplots from sklearn.manifold import TSNE from tqdm import tqdm +import neuralspot_edge as nse -from ...datasets.utils import uniform_id_generator -from ...defines import HKDemoParams +from ...defines import HKTaskParams from ...rpc import BackendFactory -from ...utils import setup_logger -from ..utils import load_datasets -from .datasets import preprocess +from ...datasets import DatasetFactory -logger = setup_logger(__name__) - -def demo(params: HKDemoParams): +def demo(params: HKTaskParams): """Run demo for model Args: - params (HKDemoParams): Demo parameters + params (HKTaskParams): Demo parameters """ + logger = nse.utils.setup_logger(__name__, level=params.verbose) + bg_color = "rgba(38,42,50,1.0)" # primary_color = "#11acd5" # secondary_color = "#ce6cff" @@ -35,10 +33,10 @@ def demo(params: HKDemoParams): TGT_LEN = 20 # Load backend inference engine - runner = BackendFactory.create(params.backend, params=params) + runner = BackendFactory.get(params.backend)(params=params) # load datasets and randomly select one - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] ds = random.choice(datasets) patients: npt.NDArray = ds.get_test_patient_ids() @@ -49,7 +47,7 @@ def demo(params: HKDemoParams): # For each patient, generate TGT_LEN samples for i, patient in enumerate(patients): ds_gen = ds.signal_generator( - patient_generator=uniform_id_generator([patient], repeat=False), + patient_generator=nse.utils.uniform_id_generator([patient], repeat=False), frame_size=params.frame_size, samples_per_patient=TGT_LEN, target_rate=params.sampling_rate, @@ -65,7 +63,7 @@ def demo(params: HKDemoParams): logger.debug("Running inference") x_p = [] for i in tqdm(range(0, len(x)), desc="Inference"): - x[i] = preprocess(x[i], sample_rate=params.sampling_rate, preprocesses=params.preprocesses) + # x[i] = preprocess(x[i], sample_rate=params.sampling_rate, preprocesses=params.preprocesses) xx = x[i].copy() xx = xx.reshape(feat_shape) runner.set_inputs(xx) diff --git a/heartkit/tasks/foundation/evaluate.py b/heartkit/tasks/foundation/evaluate.py index 67dbab41..177eb7cc 100644 --- a/heartkit/tasks/foundation/evaluate.py +++ b/heartkit/tasks/foundation/evaluate.py @@ -1,15 +1,71 @@ -from ...defines import HKTestParams -from ...utils import setup_logger +import os -logger = setup_logger(__name__) +import keras +import numpy as np +import matplotlib.pyplot as plt +import neuralspot_edge as nse +from sklearn.manifold import TSNE +from ...defines import HKTaskParams +from ...datasets import DatasetFactory +from .datasets import load_test_dataset +from ...utils import setup_plotting -def evaluate(params: HKTestParams): + +def evaluate(params: HKTaskParams): """Evaluate model Args: - params (HKTestParams): Evaluation parameters + params (HKTaskParams): Evaluation parameters """ - # Would need encoder along with either projector or classifier to evaluate + os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "test.log") + logger.debug(f"Creating working directory in {params.job_dir}") + + params.seed = nse.utils.set_random_seed(params.seed) + logger.debug(f"Random seed {params.seed}") + + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] + + # Grab sets of augmented samples + test_ds = load_test_dataset(datasets=datasets, params=params) + test_x1, test_x2 = [], [] + for inputs in test_ds.as_numpy_iterator(): + test_x1.append(inputs[nse.trainers.SimCLRTrainer.AUG_SAMPLES_0]) + test_x2.append(inputs[nse.trainers.SimCLRTrainer.AUG_SAMPLES_1]) + test_x1 = np.concatenate(test_x1) + test_x2 = np.concatenate(test_x2) + + logger.debug("Loading model") + model = nse.models.load_model(params.model_file) + flops = nse.metrics.flops.get_flops(model, batch_size=1, fpath=params.job_dir / "model_flops.log") + + model.summary(print_fn=logger.debug) + logger.debug(f"Model requires {flops/1e6:0.2f} MFLOPS") + + logger.debug("Performing inference") + test_y1 = model.predict(test_x1) + test_y2 = model.predict(test_x2) + + metrics = [ + keras.metrics.CosineSimilarity(name="cos"), + keras.metrics.MeanSquaredError(name="mse"), + ] + + setup_plotting() + + tf_rst = nse.metrics.compute_metrics(metrics, test_y1, test_y2) + logger.info("[TEST SET] " + ", ".join([f"{k.upper()}={v:.2%}" for k, v in tf_rst.items()])) + + # Compute t-SNE + logger.debug("Computing t-SNE") + tsne = TSNE(n_components=2, random_state=0, n_iter=1000, perplexity=75) + x_tsne = tsne.fit_transform(test_y1) - return + # Plot t-SNE in matplotlib + fig, ax = plt.subplots(1, 1, figsize=(9, 9)) + ax.scatter(x_tsne[:, 0], x_tsne[:, 1], c=x_tsne[:, 0] - x_tsne[:, 1], cmap="viridis") + fig.suptitle("HK Foundation: t-SNE") + ax.set_xlabel("Component 1") + ax.set_ylabel("Component 2") + fig.savefig(params.job_dir / "tsne.png") diff --git a/heartkit/tasks/foundation/export.py b/heartkit/tasks/foundation/export.py index ad58cb46..2c0640a2 100644 --- a/heartkit/tasks/foundation/export.py +++ b/heartkit/tasks/foundation/export.py @@ -1,53 +1,39 @@ -import logging import os import keras import numpy as np -import tensorflow as tf - import neuralspot_edge as nse -from ...defines import HKExportParams -from ...utils import setup_logger -from ..utils import load_datasets -from .datasets import load_test_dataset -logger = setup_logger(__name__) +from ...defines import HKTaskParams +from ...datasets import DatasetFactory +from .datasets import load_test_dataset -def export(params: HKExportParams): +def export(params: HKTaskParams): """Export model Args: - params (HKExportParams): Deployment parameters + params (HKTaskParams): Deployment parameters """ - os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "export.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "export.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) - - feat_shape = (params.frame_size, 1) - tfl_model_path = params.job_dir / "model.tflite" tflm_model_path = params.job_dir / "model_buffer.h" - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype="float32"), - tf.TensorSpec(shape=feat_shape, dtype="float32"), - ) + feat_shape = (params.frame_size, 1) - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - test_ds = load_test_dataset(datasets=datasets, params=params, ds_spec=ds_spec) - test_x, _ = next(test_ds.batch(params.test_size).as_numpy_iterator()) + test_ds = load_test_dataset(datasets=datasets, params=params) + test_x = np.concatenate([x[nse.trainers.SimCLRTrainer.SAMPLES] for x in test_ds.as_numpy_iterator()]) # Load model and set fixed batch size of 1 logger.debug("Loading trained model") model = nse.models.load_model(params.model_file) - inputs = keras.Input(shape=ds_spec[0].shape, batch_size=1, dtype=ds_spec[0].dtype) + inputs = keras.Input(shape=feat_shape, batch_size=1, dtype="float32") model(inputs) flops = nse.metrics.flops.get_flops(model, batch_size=1, fpath=params.job_dir / "model_flops.log") @@ -56,6 +42,7 @@ def export(params: HKExportParams): logger.debug(f"Converting model to TFLite (quantization={params.quantization.mode})") converter = nse.converters.tflite.TfLiteKerasConverter(model=model) + tflite_content = converter.convert( test_x=test_x, quantization=params.quantization.format, diff --git a/heartkit/tasks/foundation/train.py b/heartkit/tasks/foundation/train.py index 50c169ba..205af0d5 100644 --- a/heartkit/tasks/foundation/train.py +++ b/heartkit/tasks/foundation/train.py @@ -1,124 +1,97 @@ -import logging import os import keras -import tensorflow as tf import wandb +import numpy as np from wandb.keras import WandbMetricsLogger, WandbModelCheckpoint - import neuralspot_edge as nse -from ...defines import HKTrainParams + +from ...defines import HKTaskParams from ...models import ModelFactory -from ...utils import env_flag, set_random_seed, setup_logger -from ..utils import load_datasets +from ...datasets import DatasetFactory from .datasets import load_train_datasets - -logger = setup_logger(__name__) +from ...utils import setup_plotting, dark_theme -def train(params: HKTrainParams): +def train(params: HKTaskParams): """Train model Args: - params (HKTrainParams): Training parameters + params (HKTaskParams): Training parameters """ + os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "train.log") + logger.debug(f"Creating working directory in {params.job_dir}") params.temperature = float(getattr(params, "temperature", 0.1)) - params.seed = set_random_seed(params.seed) + params.seed = nse.utils.set_random_seed(params.seed) logger.debug(f"Random seed {params.seed}") - os.makedirs(params.job_dir, exist_ok=True) - logger.debug(f"Creating working directory in {params.job_dir}") - - handler = logging.FileHandler(params.job_dir / "train.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) - with open(params.job_dir / "train_config.json", "w", encoding="utf-8") as fp: fp.write(params.model_dump_json(indent=2)) - if env_flag("WANDB"): - wandb.init( - project=params.project, - entity="ambiq", - dir=params.job_dir, - ) + if nse.utils.env_flag("WANDB"): + wandb.init(project=params.project, entity="ambiq", dir=params.job_dir) wandb.config.update(params.model_dump()) # END IF - # Currently we return positive pairs w/o labels feat_shape = (params.frame_size, 1) - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype="float32"), - tf.TensorSpec(shape=feat_shape, dtype="float32"), - ) - - datasets = load_datasets(datasets=params.datasets) - train_ds, val_ds = load_train_datasets( - datasets=datasets, - params=params, - ds_spec=ds_spec, - ) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - projection_width = params.num_classes + train_ds, val_ds = load_train_datasets(datasets=datasets, params=params) + # Create encoder encoder_input = keras.Input(shape=feat_shape, dtype="float32") - - # Encoder encoder = ModelFactory.get(params.architecture.name)( x=encoder_input, params=params.architecture.params, num_classes=None, ) - encoder_output = encoder(encoder_input) flops = nse.metrics.flops.get_flops(encoder, batch_size=1, fpath=params.job_dir / "encoder_flops.log") encoder.summary(print_fn=logger.info) logger.debug(f"Encoder requires {flops/1e6:0.2f} MFLOPS") - # Projector - projector_input = encoder_output - projector_output = keras.layers.Dense(projection_width, activation="relu6")(projector_input) - projector_output = keras.layers.Dense(projection_width)(projector_output) - projector = keras.Model(inputs=projector_input, outputs=projector_output, name="projector") - flops = nse.metrics.flops.get_flops(projector, batch_size=1, fpath=params.job_dir / "projector_flops.log") - projector.summary(print_fn=logger.info) - logger.debug(f"Projector requires {flops/1e6:0.2f} MFLOPS") + # Create projector + # encoder_output = encoder(encoder_input) + # projection_width = params.num_classes + # projector_input = encoder_output + # projector_output = keras.layers.Dense(projection_width, activation="relu6")(projector_input) + # projector_output = keras.layers.Dense(projection_width)(projector_output) + # projector = keras.Model(inputs=projector_input, outputs=projector_output, name="projector") + # flops = nse.metrics.flops.get_flops(projector, batch_size=1, fpath=params.job_dir / "projector_flops.log") + # projector.summary(print_fn=logger.info) + # logger.debug(f"Projector requires {flops/1e6:0.2f} MFLOPS") if params.model_file is None: params.model_file = params.job_dir / "model.keras" - model = nse.models.opimizers.simclr.SimCLR( - contrastive_augmenter=lambda x: x, + model = nse.trainers.SimCLRTrainer( encoder=encoder, - projector=projector, - # momentum_coeff=0.999, - temperature=params.temperature, - # queue_size=65536, + projector=None, ) def get_scheduler(): - if params.lr_cycles > 1: - return keras.optimizers.schedules.CosineDecayRestarts( - initial_learning_rate=params.lr_rate, - first_decay_steps=int(0.1 * params.steps_per_epoch * params.epochs), - t_mul=1.65 / (0.1 * params.lr_cycles * (params.lr_cycles - 1)), - m_mul=0.4, - ) - return keras.optimizers.schedules.CosineDecay( + t_mul = 1 + first_steps = (params.steps_per_epoch * params.epochs) / (np.power(params.lr_cycles, t_mul) - t_mul + 1) + scheduler = keras.optimizers.schedules.CosineDecayRestarts( initial_learning_rate=params.lr_rate, - decay_steps=params.steps_per_epoch * params.epochs, + first_decay_steps=np.ceil(first_steps), + t_mul=t_mul, + m_mul=0.5, ) + return scheduler model.compile( - contrastive_optimizer=keras.optimizers.Adam(get_scheduler()), - probe_optimizer=keras.optimizers.Adam(get_scheduler()), + encoder_optimizer=keras.optimizers.Adam(get_scheduler()), + encoder_loss=nse.losses.simclr.SimCLRLoss(temperature=params.temperature), + encoder_metrics=[keras.metrics.MeanSquaredError(name="mse"), keras.metrics.CosineSimilarity(name="cos")], ) ModelCheckpoint = keras.callbacks.ModelCheckpoint - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): ModelCheckpoint = WandbModelCheckpoint model_callbacks = [ keras.callbacks.EarlyStopping( @@ -126,28 +99,29 @@ def get_scheduler(): patience=max(int(0.25 * params.epochs), 1), mode="max" if params.val_metric == "f1" else "auto", restore_best_weights=True, + verbose=params.verbose - 1, ), ModelCheckpoint( filepath=str(params.model_file), monitor=f"val_{params.val_metric}", save_best_only=True, mode="max" if params.val_metric == "f1" else "auto", - verbose=1, + verbose=params.verbose - 1, ), keras.callbacks.CSVLogger(params.job_dir / "history.csv"), ] - if env_flag("TENSORBOARD"): + if nse.utils.env_flag("TENSORBOARD"): model_callbacks.append( keras.callbacks.TensorBoard( log_dir=params.job_dir, write_steps_per_second=True, ) ) - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): model_callbacks.append(WandbMetricsLogger()) try: - model.fit( + history = model.fit( train_ds, steps_per_epoch=params.steps_per_epoch, verbose=2, @@ -159,3 +133,18 @@ def get_scheduler(): logger.warning("Stopping training due to keyboard interrupt") logger.debug(f"Model saved to {params.model_file}") + + setup_plotting(dark_theme) + nse.plotting.plot_history_metrics( + history.history, + metrics=["loss", "cos"], + save_path=params.job_dir / "history.png", + stack=True, + figsize=(9, 5), + ) + + metrics = model.evaluate(val_ds, verbose=2, return_dict=True) + + logger.info(f"Loss: {metrics['loss']:.2f}") + logger.info(f"Mean Squared Error: {metrics['mse']:.2f}") + logger.info(f"Cosine Similarity: {metrics['cos']:.2%}") diff --git a/heartkit/tasks/rhythm/__init__.py b/heartkit/tasks/rhythm/__init__.py index 360fe409..9e6ef07d 100644 --- a/heartkit/tasks/rhythm/__init__.py +++ b/heartkit/tasks/rhythm/__init__.py @@ -1,4 +1,4 @@ -from ...defines import HKDemoParams, HKExportParams, HKTestParams, HKTrainParams +from ...defines import HKTaskParams from ..task import HKTask from .defines import HKRhythm from .demo import demo @@ -18,17 +18,17 @@ def description() -> str: ) @staticmethod - def train(params: HKTrainParams): + def train(params: HKTaskParams): train(params) @staticmethod - def evaluate(params: HKTestParams): + def evaluate(params: HKTaskParams): evaluate(params) @staticmethod - def export(params: HKExportParams): + def export(params: HKTaskParams): export(params) @staticmethod - def demo(params: HKDemoParams): + def demo(params: HKTaskParams): demo(params) diff --git a/heartkit/tasks/rhythm/dataloaders/__init__.py b/heartkit/tasks/rhythm/dataloaders/__init__.py index ecb2836b..1feadddf 100644 --- a/heartkit/tasks/rhythm/dataloaders/__init__.py +++ b/heartkit/tasks/rhythm/dataloaders/__init__.py @@ -1,3 +1,14 @@ -from .icentia11k import icentia11k_data_generator, icentia11k_label_map -from .lsad import lsad_data_generator, lsad_label_map -from .ptbxl import ptbxl_data_generator, ptbxl_label_map +import neuralspot_edge as nse + +from ....datasets import HKDataloader + +from .icentia11k import Icentia11kDataloader +from .icentia_mini import IcentiaMiniDataloader +from .ptbxl import PtbxlDataloader +from .lsad import LsadDataloader + +RhythmDataloaderFactory = nse.utils.create_factory(factory="HKRhythmDataloaderFactory", type=HKDataloader) +RhythmDataloaderFactory.register("icentia11k", Icentia11kDataloader) +RhythmDataloaderFactory.register("icentia_mini", IcentiaMiniDataloader) +RhythmDataloaderFactory.register("ptbxl", PtbxlDataloader) +RhythmDataloaderFactory.register("lsad", LsadDataloader) diff --git a/heartkit/tasks/rhythm/dataloaders/icentia11k.py b/heartkit/tasks/rhythm/dataloaders/icentia11k.py index e27bc3e4..14b7cda3 100644 --- a/heartkit/tasks/rhythm/dataloaders/icentia11k.py +++ b/heartkit/tasks/rhythm/dataloaders/icentia11k.py @@ -4,9 +4,9 @@ import numpy as np import numpy.typing as npt import physiokit as pk +import neuralspot_edge as nse -from ....datasets.defines import PatientGenerator -from ....datasets.icentia11k import IcentiaDataset, IcentiaRhythm +from ....datasets import HKDataloader, IcentiaDataset, IcentiaRhythm from ..defines import HKRhythm IcentiaRhythmMap = { @@ -18,64 +18,25 @@ } -def icentia11k_label_map( - label_map: dict[int, int] | None = None, -) -> dict[int, int]: - """Get label map - - Args: - label_map (dict[int, int]|None): Label map - - Returns: - dict[int, int]: Label map - """ - return {k: label_map.get(v, -1) for (k, v) in IcentiaRhythmMap.items()} - - -def icentia11k_data_generator( - patient_generator: PatientGenerator, - ds: IcentiaDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, - label_map: dict[int, int] | None = None, -) -> Generator[tuple[npt.NDArray, int], None, None]: - """Generate frames w/ rhythm labels (e.g. afib) using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: IcentiaDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - label_map (dict[int, int] | None, optional): Label map. Defaults to None. - - Returns: - Generator[tuple[npt.NDArray, int], None, None]: Sample generator - """ - if target_rate is None: - target_rate = ds.sampling_rate - # END IF - - # Target labels and mapping - tgt_labels = sorted(list(set((lbl for lbl in label_map.values() if lbl != -1)))) - tgt_map = icentia11k_label_map(label_map=label_map) - label_key = ds.label_key("rhythm") - num_classes = len(tgt_labels) - - # If samples_per_patient is a list, then it must be the same length as num_classes - if isinstance(samples_per_patient, Iterable): - samples_per_tgt = samples_per_patient - else: - num_per_tgt = int(max(1, samples_per_patient / num_classes)) - samples_per_tgt = num_per_tgt * [num_classes] - # END IF - - input_size = int(np.round((ds.sampling_rate / target_rate) * frame_size)) - - # Group patient rhythms by type (segment, start, stop, delta) - for pt in patient_generator: - with ds.patient_data(pt) as segments: +class Icentia11kDataloader(HKDataloader): + def __init__(self, ds: IcentiaDataset, **kwargs): + super().__init__(ds=ds, **kwargs) + # Update label map + if self.label_map: + self.label_map = {k: self.label_map[v] for (k, v) in IcentiaRhythmMap.items() if v in self.label_map} + # END DEF + self.label_type = "rhythm" + # PT: [label_idx, segment, start, end] + self._pts_rhythm_map: dict[int, list[npt.NDArray]] = {} + + def _create_patient_rhythm_map(self, patient_id: int): + # Target labels and mapping + tgt_labels = sorted(set((self.label_map.values()))) + label_key = self.ds.label_key(self.label_type) + + input_size = int(np.ceil((self.ds.sampling_rate / self.sampling_rate) * self.frame_size)) + + with self.ds.patient_data(patient_id=patient_id) as segments: # This maps segment index to segment key seg_map: list[str] = list(segments.keys()) @@ -95,7 +56,7 @@ def icentia11k_data_generator( xs, xe, xl = labels[0::2, 0], labels[1::2, 0], labels[0::2, 1] # Map labels to target labels - xl = np.vectorize(tgt_map.get, otypes=[int])(xl) + xl = np.vectorize(self.label_map.get, otypes=[int])(xl) # Capture segment, start, and end for each target label for tgt_idx, tgt_class in enumerate(tgt_labels): @@ -104,7 +65,28 @@ def icentia11k_data_generator( pt_tgt_seg_map[tgt_idx] += seg_vals.tolist() # END FOR # END FOR + pt_tgt_seg_map = [np.array(b) for b in pt_tgt_seg_map] + self._pts_rhythm_map[patient_id] = pt_tgt_seg_map + + def patient_data_generator( + self, + patient_id: int, + samples_per_patient: list[int], + ): + # Target labels and mapping + tgt_labels = sorted(set(self.label_map.values())) + + input_size = int(np.ceil((self.ds.sampling_rate / self.sampling_rate) * self.frame_size)) + + # Group patient rhythms by type (segment, start, stop, delta) + + with self.ds.patient_data(patient_id=patient_id) as segments: + # This maps segment index to segment key + seg_map: list[str] = list(segments.keys()) + if patient_id not in self._pts_rhythm_map: + self._create_patient_rhythm_map(patient_id) + pt_tgt_seg_map = self._pts_rhythm_map[patient_id] # Grab target segments seg_samples: list[tuple[int, int, int, int]] = [] @@ -115,7 +97,7 @@ def icentia11k_data_generator( tgt_seg_indices: list[int] = random.choices( np.arange(tgt_segments.shape[0]), weights=tgt_segments[:, 2] - tgt_segments[:, 1], - k=samples_per_tgt[tgt_idx], + k=samples_per_patient[tgt_idx], ) for tgt_seg_idx in tgt_seg_indices: seg_idx, rhy_start, rhy_end = tgt_segments[tgt_seg_idx] @@ -128,12 +110,42 @@ def icentia11k_data_generator( # Shuffle segments random.shuffle(seg_samples) - # Yield selected samples for patient + # Grab selected samples for patient + samples = [] for seg_idx, frame_start, frame_end, label in seg_samples: x: npt.NDArray = segments[seg_map[seg_idx]]["data"][frame_start:frame_end].astype(np.float32) - if ds.sampling_rate != target_rate: - x = pk.signal.resample_signal(x, ds.sampling_rate, target_rate, axis=0) - yield x, label + if self.ds.sampling_rate != self.sampling_rate: + x = pk.signal.resample_signal(x, self.ds.sampling_rate, self.sampling_rate, axis=0) + x = x[: self.frame_size] # truncate to frame size + x = np.reshape(x, (self.frame_size, 1)) + samples.append((x, label)) # END FOR # END WITH - # END FOR + + for x, y in samples: + yield x, y + # END FOR + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + # Target labels and mapping + tgt_labels = sorted(set(self.label_map.values())) + num_classes = len(tgt_labels) + + # If samples_per_patient is a list, then it must be the same length as nclasses + if isinstance(samples_per_patient, Iterable): + samples_per_tgt = samples_per_patient + else: + num_per_tgt = int(max(1, samples_per_patient / num_classes)) + samples_per_tgt = num_per_tgt * [num_classes] + + self._pts_beat_map = {} + for pt_id in nse.utils.uniform_id_generator(patient_ids, shuffle=shuffle): + for x, y in self.patient_data_generator(pt_id, samples_per_tgt): + yield x, y + # END FOR + # END FOR diff --git a/heartkit/tasks/rhythm/dataloaders/icentia_mini.py b/heartkit/tasks/rhythm/dataloaders/icentia_mini.py new file mode 100644 index 00000000..a09965de --- /dev/null +++ b/heartkit/tasks/rhythm/dataloaders/icentia_mini.py @@ -0,0 +1,116 @@ +import random +from typing import Generator, Iterable + +import numpy as np +import numpy.typing as npt +import physiokit as pk +import neuralspot_edge as nse + +from ....datasets import HKDataloader, IcentiaMiniDataset, IcentiaMiniRhythm +from ..defines import HKRhythm + +IcentiaMiniRhythmMap = { + IcentiaMiniRhythm.normal: HKRhythm.sr, + IcentiaMiniRhythm.afib: HKRhythm.afib, + IcentiaMiniRhythm.aflut: HKRhythm.aflut, + IcentiaMiniRhythm.end: HKRhythm.noise, +} + + +class IcentiaMiniDataloader(HKDataloader): + def __init__(self, ds: IcentiaMiniDataset, **kwargs): + super().__init__(ds=ds, **kwargs) + # Update label map to map icentia mini label -> rhythm label -> user label + if self.label_map: + self.label_map = {k: self.label_map[v] for (k, v) in IcentiaMiniRhythmMap.items() if v in self.label_map} + self.label_type = "rhythm" + self._pts_rhythm_map: dict[int, dict[int, tuple[int, int, int]]] = {} + + def _create_patient_rhythm_map(self, patient_id: int): + label_key = self.ds.label_key(self.label_type) + tgt_labels = sorted(set(self.label_map.values())) + # input_size = int(np.ceil((self.ds.sampling_rate / self.sampling_rate) * self.frame_size)) + + pt_rhythm_map = {lbl: [] for lbl in tgt_labels} + with self.ds.patient_data(patient_id) as pt: + # rlabels is a mask with shape (N, M) + rlabels = pt[label_key][:] + + # Capture all rhythm locations + self.pts_rhythm_map: dict[int, tuple[int, int, int]] = {lbl: [] for lbl in tgt_labels} + for r in range(rlabels.shape[0]): + # Grab start and end locations by diffing the mask + starts = np.concatenate(([0], np.where(np.abs(np.diff(rlabels[r, :])) >= 1)[0])) + ends = np.concatenate((starts[1:], [rlabels.shape[1]])) + lengths = ends - starts + labels = rlabels[r, starts] + # iterate through the zip of labels, starts, ends and append to the rhythm map + for label, start, length in zip(labels, starts, lengths): + # Skip if label is not in the label map + if label not in self.label_map: + continue + # # Skip if the segment is too short + # if length < input_size: + # continue + pt_rhythm_map[self.label_map[label]].append((r, start, length)) + # END FOR + # END FOR + # END WITH + self._pts_rhythm_map[patient_id] = pt_rhythm_map + + def patient_data_generator( + self, + patient_id: int, + samples_per_patient: list[int], + ): + tgt_labels = sorted(set(self.label_map.values())) + input_size = int(np.ceil((self.ds.sampling_rate / self.sampling_rate) * self.frame_size)) + + # Create rhythm map for all patients if needed + if patient_id not in self._pts_rhythm_map: + self._create_patient_rhythm_map(patient_id) + + with self.ds.patient_data(patient_id) as pt: + data = pt["data"][:] # has shape (N, M, 1) + pt_rhythm_map = self._pts_rhythm_map[patient_id] + for i, samples in enumerate(samples_per_patient): + tgt_label = tgt_labels[i] + locs = pt_rhythm_map.get(tgt_label, None) + if not locs: + continue + loc_indices = random.choices(range(len(locs)), k=samples) + for loc_idx in loc_indices: + row, start, length = locs[loc_idx] + frame_start = max(0, random.randint(start, max(start, start + length - input_size) + 1)) + frame_end = frame_start + input_size + x = data[row, frame_start:frame_end].astype(np.float32) + if self.ds.sampling_rate != self.sampling_rate: + x = pk.signal.resample_signal(x, self.ds.sampling_rate, self.sampling_rate, axis=0) + x = x[: self.frame_size] # truncate to frame size + yield x, tgt_label + # END FOR + # END FOR + # END WITH + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + # Target labels and mapping + tgt_labels = sorted(set(self.label_map.values())) + num_classes = len(tgt_labels) + + # If samples_per_patient is a list, then it must be the same length as nclasses + if isinstance(samples_per_patient, Iterable): + samples_per_tgt = samples_per_patient + else: + num_per_tgt = int(max(1, samples_per_patient / num_classes)) + samples_per_tgt = num_per_tgt * [num_classes] + + for pt_id in nse.utils.uniform_id_generator(patient_ids, shuffle=shuffle): + for x, y in self.patient_data_generator(pt_id, samples_per_tgt): + yield x, y + # END FOR + # END FOR diff --git a/heartkit/tasks/rhythm/dataloaders/lsad.py b/heartkit/tasks/rhythm/dataloaders/lsad.py index a118acf0..84b1bbd4 100644 --- a/heartkit/tasks/rhythm/dataloaders/lsad.py +++ b/heartkit/tasks/rhythm/dataloaders/lsad.py @@ -1,9 +1,9 @@ from typing import Generator import numpy.typing as npt +import neuralspot_edge as nse -from ....datasets.defines import PatientGenerator -from ....datasets.lsad import LsadDataset, LsadScpCode +from ....datasets import HKDataloader, LsadDataset, LsadScpCode from ..defines import HKRhythm LsadRhythmMap = { @@ -31,51 +31,25 @@ } -def lsad_label_map( - label_map: dict[int, int] | None = None, -) -> dict[int, int]: - """Get label map - - Args: - label_map (dict[int, int]|None): Label map - - Returns: - dict[int, int]: Label map - """ - return {k: label_map.get(v, -1) for (k, v) in LsadRhythmMap.items()} - - -def lsad_data_generator( - patient_generator: PatientGenerator, - ds: LsadDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, - label_map: dict[int, int] | None = None, -) -> Generator[tuple[npt.NDArray, int], None, None]: - """Generate frames w/ rhythm labels (e.g. afib) using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: LsadDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - label_map (dict[int, int] | None, optional): Label map. Defaults to None. - - Returns: - SampleGenerator: Sample generator - - Yields: - Iterator[SampleGenerator] - """ - - return ds.signal_label_generator( - patient_generator=patient_generator, - frame_size=frame_size, - samples_per_patient=samples_per_patient, - target_rate=target_rate, - label_map=lsad_label_map(label_map=label_map), - label_type="scp", - label_format=None, - ) +class LsadDataloader(HKDataloader): + def __init__(self, ds: LsadDataset, **kwargs): + super().__init__(ds=ds, **kwargs) + if self.label_map: + self.label_map = {k: self.label_map[v] for (k, v) in LsadRhythmMap.items() if v in self.label_map} + self.label_type = "scp" + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + return self.ds.signal_label_generator( + patient_generator=nse.utils.uniform_id_generator(patient_ids, repeat=True, shuffle=shuffle), + frame_size=self.frame_size, + samples_per_patient=samples_per_patient, + target_rate=self.sampling_rate, + label_map=self.label_map, + label_type=self.label_type, + label_format=None, + ) diff --git a/heartkit/tasks/rhythm/dataloaders/ptbxl.py b/heartkit/tasks/rhythm/dataloaders/ptbxl.py index ec252663..741d1ca2 100644 --- a/heartkit/tasks/rhythm/dataloaders/ptbxl.py +++ b/heartkit/tasks/rhythm/dataloaders/ptbxl.py @@ -1,9 +1,9 @@ from typing import Generator import numpy.typing as npt +import neuralspot_edge as nse -from ....datasets.defines import PatientGenerator -from ....datasets.ptbxl import PtbxlDataset, PtbxlScpCode +from ....datasets import HKDataloader, PtbxlDataset, PtbxlScpCode from ..defines import HKRhythm PtbxlRhythmMap = { @@ -22,51 +22,25 @@ } -def ptbxl_label_map( - label_map: dict[int, int] | None = None, -) -> dict[int, int]: - """Get label map - - Args: - label_map (dict[int, int]|None): Label map - - Returns: - dict[int, int]: Label map - """ - return {k: label_map.get(v, -1) for (k, v) in PtbxlRhythmMap.items()} - - -def ptbxl_data_generator( - patient_generator: PatientGenerator, - ds: PtbxlDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, - label_map: dict[int, int] | None = None, -) -> Generator[tuple[npt.NDArray, int], None, None]: - """Generate frames w/ rhythm labels using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: PtbxlDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - label_map (dict[int, int] | None, optional): Label map. Defaults to None. - - Returns: - SampleGenerator: Sample generator - - Yields: - Iterator[SampleGenerator] - """ - - return ds.signal_label_generator( - patient_generator=patient_generator, - frame_size=frame_size, - samples_per_patient=samples_per_patient, - target_rate=target_rate, - label_map=ptbxl_label_map(label_map=label_map), - label_type="scp", - label_format=None, - ) +class PtbxlDataloader(HKDataloader): + def __init__(self, ds: PtbxlDataset, **kwargs): + super().__init__(ds=ds, **kwargs) + if self.label_map: + self.label_map = {k: self.label_map[v] for (k, v) in PtbxlRhythmMap.items() if v in self.label_map} + self.label_type = "scp" + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + return self.ds.signal_label_generator( + patient_generator=nse.utils.uniform_id_generator(patient_ids, repeat=True, shuffle=shuffle), + frame_size=self.frame_size, + samples_per_patient=samples_per_patient, + target_rate=self.sampling_rate, + label_map=self.label_map, + label_type=self.label_type, + label_format=None, + ) diff --git a/heartkit/tasks/rhythm/datasets.py b/heartkit/tasks/rhythm/datasets.py index d0c23520..72348405 100644 --- a/heartkit/tasks/rhythm/datasets.py +++ b/heartkit/tasks/rhythm/datasets.py @@ -1,365 +1,162 @@ -import functools -import logging -from pathlib import Path - import numpy as np -import numpy.typing as npt import tensorflow as tf +import neuralspot_edge as nse from ...datasets import ( HKDataset, - augment_pipeline, - preprocess_pipeline, - uniform_id_generator, -) -from ...datasets.dataloader import test_dataloader, train_val_dataloader -from ...defines import ( - AugmentationParams, - HKExportParams, - HKTestParams, - HKTrainParams, - PreprocessParams, -) -from ...utils import resolve_template_path -from .dataloaders import ( - icentia11k_data_generator, - icentia11k_label_map, - lsad_data_generator, - lsad_label_map, - ptbxl_data_generator, - ptbxl_label_map, + create_augmentation_pipeline, ) +from ...datasets.dataloader import HKDataloader +from ...defines import HKTaskParams, NamedParams -logger = logging.getLogger(__name__) - - -def preprocess(x: npt.NDArray, preprocesses: list[PreprocessParams], sample_rate: float) -> npt.NDArray: - """Preprocess data pipeline - - Args: - x (npt.NDArray): Input data - preprocesses (list[PreprocessParams]): Preprocess parameters - sample_rate (float): Sample rate - - Returns: - npt.NDArray: Preprocessed data - """ - return preprocess_pipeline(x, preprocesses=preprocesses, sample_rate=sample_rate) - - -def augment(x: npt.NDArray, augmentations: list[AugmentationParams], sample_rate: float) -> npt.NDArray: - """Augment data pipeline - - Args: - x (npt.NDArray): Input data - augmentations (list[AugmentationParams]): Augmentation parameters - sample_rate (float): Sample rate - - Returns: - npt.NDArray: Augmented data - """ - return augment_pipeline( - x=x, - augmentations=augmentations, - sample_rate=sample_rate, - ) - - -def prepare( - x_y: tuple[npt.NDArray, int], - sample_rate: float, - preprocesses: list[PreprocessParams], - augmentations: list[AugmentationParams], - spec: tuple[tf.TensorSpec, tf.TensorSpec], - num_classes: int, -) -> tuple[npt.NDArray, npt.NDArray]: - """Prepare dataset - - Args: - x_y (tuple[npt.NDArray, int]): Input data and label - sample_rate (float): Sample rate - preprocesses (list[PreprocessParams]|None): Preprocess parameters - augmentations (list[AugmentationParams]|None): Augmentation parameters - spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - num_classes (int): Number of classes - - Returns: - tuple[npt.NDArray, npt.NDArray]: Prepared data - """ - x, y = x_y[0].copy(), x_y[1] - - if augmentations: - x = augment(x, augmentations, sample_rate) - # END IF - - if preprocesses: - x = preprocess(x, preprocesses, sample_rate) - # END IF - - x = x.reshape(spec[0].shape) - y = tf.one_hot(y, num_classes) - - return x, y - - -def get_ds_label_map(ds: HKDataset, label_map: dict[int, int] | None = None) -> dict[int, int]: - """Get label map for dataset +from .dataloaders import RhythmDataloaderFactory - Args: - ds (HKDataset): Dataset - label_map (dict[int, int]|None): Label map +logger = nse.utils.setup_logger(__name__) - Returns: - dict[int, int]: Label map - """ - match ds.name: - case "icentia11k": - return icentia11k_label_map(label_map=label_map) - case "lsad": - return lsad_label_map(label_map=label_map) - case "ptbxl": - return ptbxl_label_map(label_map=label_map) - case _: - raise ValueError(f"Dataset {ds.name} not supported") - # END MATCH - -def get_data_generator( - ds: HKDataset, - frame_size: int, - samples_per_patient: int, - target_rate: int, - label_map: dict[int, int] | None = None, +def create_data_pipeline( + ds: tf.data.Dataset, + sampling_rate: int, + batch_size: int, + buffer_size: int | None = None, + augmentations: list[NamedParams] | None = None, + num_classes: int = 2, ): - """Get task data generator for dataset - - Args: - ds (HKDataset): Dataset - frame_size (int): Frame size - samples_per_patient (int): Samples per patient - target_rate (int): Target rate - label_map (dict[int, int] | None, optional): Label map. Defaults to None. - - Returns: - callable: Data generator - """ - match ds.name: - case "icentia11k": - data_generator = icentia11k_data_generator - case "lsad": - data_generator = lsad_data_generator - case "ptbxl": - data_generator = ptbxl_data_generator - case _: - raise ValueError(f"Dataset {ds.name} not supported") - # END MATCH - return functools.partial( - data_generator, - ds=ds, - frame_size=frame_size, - samples_per_patient=samples_per_patient, - target_rate=target_rate, - label_map=label_map, - ) - - -def get_label_type(ds: HKDataset) -> str: - """Get label type for dataset - - Args: - ds (HKDataset): Dataset - - Returns: - str: Label type - """ - match ds.name: - case "icentia11k": - return "rhythm" - case _: - return "scp" - # END MATCH - - -def resolve_ds_cache_path(fpath: Path | None, ds: HKDataset, task: str, frame_size: int, sample_rate: int): - """Resolve dataset cache path - - Args: - fpath (Path|None): File path - ds (HKDataset): Dataset - task (str): Task - frame_size (int): Frame size - sample_rate (int): Sampling rate - - Returns: - Path|None: Resolved path - """ - if not fpath: - return None - return resolve_template_path( - fpath=fpath, - dataset=ds.name, - task=task, - frame_size=frame_size, - sampling_rate=sample_rate, + if buffer_size: + ds = ds.shuffle( + buffer_size=buffer_size, + reshuffle_each_iteration=True, + ) + if batch_size: + ds = ds.batch( + batch_size=batch_size, + drop_remainder=True, + num_parallel_calls=tf.data.AUTOTUNE, + ) + augmenter = create_augmentation_pipeline(augmentations, sampling_rate=sampling_rate) + + ds = ( + ds.map( + lambda data, labels: { + "data": tf.cast(data, "float32"), + "labels": tf.one_hot(labels, num_classes), + }, + num_parallel_calls=tf.data.AUTOTUNE, + ) + .map( + augmenter, + num_parallel_calls=tf.data.AUTOTUNE, + ) + .map( + lambda data: (data["data"], data["labels"]), + num_parallel_calls=tf.data.AUTOTUNE, + ) ) + return ds.prefetch(tf.data.AUTOTUNE) def load_train_datasets( datasets: list[HKDataset], - params: HKTrainParams, - ds_spec: tuple[tf.TensorSpec, tf.TensorSpec], + params: HKTaskParams, ) -> tuple[tf.data.Dataset, tf.data.Dataset]: - """Load training and validation datasets - - Args: - datasets (list[HKDataset]): Datasets - params (HKTrainParams): Training parameters - ds_spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - - Returns: - tuple[tf.data.Dataset, tf.data.Dataset]: Train and validation datasets - """ - id_generator = functools.partial(uniform_id_generator, repeat=True) - train_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, - ) - val_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=None, - spec=ds_spec, - num_classes=params.num_classes, - ) - train_datasets = [] val_datasets = [] for ds in datasets: - val_file = resolve_ds_cache_path( - params.val_file, - ds=ds, - task="rhythm", - frame_size=params.frame_size, - sample_rate=params.sampling_rate, - ) - data_generator = get_data_generator( + dataloader: HKDataloader = RhythmDataloaderFactory.get(ds.name)( ds=ds, frame_size=params.frame_size, - samples_per_patient=params.samples_per_patient, - target_rate=params.sampling_rate, + sampling_rate=params.sampling_rate, label_map=params.class_map, ) - train_ds, val_ds = train_val_dataloader( - ds=ds, - spec=ds_spec, - data_generator=data_generator, - id_generator=id_generator, + train_patients, val_patients = dataloader.split_train_val_patients( train_patients=params.train_patients, val_patients=params.val_patients, - val_pt_samples=params.val_samples_per_patient, - val_file=val_file, - val_size=params.val_size, - label_map=get_ds_label_map(ds, label_map=params.class_map), - label_type=get_label_type(ds), - preprocess=train_prepare, - val_preprocess=val_prepare, - num_workers=params.data_parallelism, + ) + + train_ds = dataloader.create_dataloader( + patient_ids=train_patients, samples_per_patient=params.samples_per_patient, shuffle=True + ) + + val_ds = dataloader.create_dataloader( + patient_ids=val_patients, samples_per_patient=params.val_samples_per_patient, shuffle=False ) train_datasets.append(train_ds) val_datasets.append(val_ds) # END FOR - ds_weights = np.array([d.weight for d in params.datasets]) - ds_weights = ds_weights / ds_weights.sum() + ds_weights = None + if params.dataset_weights: + ds_weights = np.array(params.dataset_weights) + ds_weights = ds_weights / ds_weights.sum() train_ds = tf.data.Dataset.sample_from_datasets(train_datasets, weights=ds_weights) val_ds = tf.data.Dataset.sample_from_datasets(val_datasets, weights=ds_weights) # Shuffle and batch datasets for training - train_ds = ( - train_ds.shuffle( - buffer_size=params.buffer_size, - reshuffle_each_iteration=True, - ).batch( - batch_size=params.batch_size, - drop_remainder=True, - num_parallel_calls=tf.data.AUTOTUNE, - ) - # .prefetch(buffer_size=tf.data.AUTOTUNE) + train_ds = create_data_pipeline( + ds=train_ds, + sampling_rate=params.sampling_rate, + batch_size=params.batch_size, + buffer_size=params.buffer_size, + augmentations=params.augmentations + params.preprocesses, + num_classes=params.num_classes, ) - val_ds = val_ds.batch( + + val_ds = create_data_pipeline( + ds=val_ds, + sampling_rate=params.sampling_rate, batch_size=params.batch_size, - drop_remainder=True, - num_parallel_calls=tf.data.AUTOTUNE, + augmentations=params.preprocesses, + num_classes=params.num_classes, ) + + # If given fixed val size or steps, then capture and cache + val_steps_per_epoch = params.val_size // params.batch_size if params.val_size else params.val_steps_per_epoch + if val_steps_per_epoch: + logger.info(f"Validation steps per epoch: {val_steps_per_epoch}") + val_ds = val_ds.take(val_steps_per_epoch).cache() + return train_ds, val_ds def load_test_dataset( datasets: list[HKDataset], - params: HKTestParams | HKExportParams, - ds_spec: tuple[tf.TensorSpec, tf.TensorSpec], + params: HKTaskParams, ) -> tf.data.Dataset: - """Load test dataset - - Args: - datasets (list[HKDataset]): Datasets - params (HKTestParams|HKExportParams): Test parameters - ds_spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - - Returns: - tf.data.Dataset: Test dataset - """ - - id_generator = functools.partial(uniform_id_generator, repeat=True) - test_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=None, # params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, - ) test_datasets = [] for ds in datasets: - test_file = resolve_ds_cache_path( - fpath=params.test_file, + dataloader: HKDataloader = RhythmDataloaderFactory.get(ds.name)( ds=ds, - task="rhythm", frame_size=params.frame_size, - sample_rate=params.sampling_rate, - ) - data_generator = get_data_generator( - ds=ds, - frame_size=params.frame_size, - samples_per_patient=params.test_samples_per_patient, - target_rate=params.sampling_rate, + sampling_rate=params.sampling_rate, label_map=params.class_map, ) - test_ds = test_dataloader( - ds=ds, - spec=ds_spec, - data_generator=data_generator, - id_generator=id_generator, - test_patients=params.test_patients, - test_file=test_file, - label_map=get_ds_label_map(ds, label_map=params.class_map), - label_type=get_label_type(ds), - preprocess=test_prepare, - num_workers=params.data_parallelism, + test_patients = dataloader.test_patient_ids(params.test_patients) + test_ds = dataloader.create_dataloader( + patient_ids=test_patients, + samples_per_patient=params.test_samples_per_patient, + shuffle=False, ) test_datasets.append(test_ds) # END FOR - ds_weights = np.array([d.weight for d in params.datasets]) - ds_weights = ds_weights / ds_weights.sum() + ds_weights = None + if params.dataset_weights: + ds_weights = np.array(params.dataset_weights) + ds_weights = ds_weights / ds_weights.sum() test_ds = tf.data.Dataset.sample_from_datasets(test_datasets, weights=ds_weights) - # END WITH + test_ds = create_data_pipeline( + ds=test_ds, + sampling_rate=params.sampling_rate, + batch_size=params.batch_size, + augmentations=params.preprocesses, + num_classes=params.num_classes, + ) + + if params.test_size: + batch_size = getattr(params, "batch_size", 1) + test_ds = test_ds.take(params.test_size // batch_size).cache() + return test_ds diff --git a/heartkit/tasks/rhythm/demo.py b/heartkit/tasks/rhythm/demo.py index 5dc27bdf..52fef696 100644 --- a/heartkit/tasks/rhythm/demo.py +++ b/heartkit/tasks/rhythm/demo.py @@ -5,22 +5,20 @@ import plotly.graph_objects as go from plotly.subplots import make_subplots from tqdm import tqdm +import neuralspot_edge as nse -from ...datasets.utils import uniform_id_generator -from ...defines import HKDemoParams +from ...defines import HKTaskParams from ...rpc import BackendFactory -from ...utils import setup_logger -from ..utils import load_datasets -from .datasets import preprocess +from ...datasets import DatasetFactory, create_augmentation_pipeline -def demo(params: HKDemoParams): +def demo(params: HKTaskParams): """Run demo for model Args: - params (HKDemoParams): Demo parameters + params (HKTaskParams): Demo parameters """ - logger = setup_logger(__name__, level=params.verbose) + logger = nse.utils.setup_logger(__name__, level=params.verbose) bg_color = "rgba(38,42,50,1.0)" primary_color = "#11acd5" @@ -34,31 +32,29 @@ def demo(params: HKDemoParams): params.demo_size = params.demo_size or 2 * params.frame_size # Load backend inference engine - runner = BackendFactory.create(params.backend, params=params) + runner = BackendFactory.get(params.backend)(params=params) # Load data # classes = sorted(list(set(params.class_map.values()))) class_names = params.class_names or [f"Class {i}" for i in range(params.num_classes)] feat_shape = (params.frame_size, 1) - # class_shape = (params.num_classes,) - # input_spec = ( - # tf.TensorSpec(shape=feat_shape, dtype=tf.float32), - # tf.TensorSpec(shape=class_shape, dtype=tf.int32), - # ) - - dsets = load_datasets(datasets=params.datasets) - ds = random.choice(dsets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] + ds = random.choice(datasets) ds_gen = ds.signal_generator( - patient_generator=uniform_id_generator(ds.get_test_patient_ids(), repeat=False), + patient_generator=nse.utils.uniform_id_generator(ds.get_test_patient_ids(), repeat=False), frame_size=params.demo_size, samples_per_patient=5, target_rate=params.sampling_rate, ) x = next(ds_gen) + augmenter = create_augmentation_pipeline( + params.preprocesses + params.augmentations, sampling_rate=params.sampling_rate + ) + # Run inference runner.open() logger.debug("Running inference") @@ -68,13 +64,9 @@ def demo(params: HKDemoParams): start, stop = x.shape[0] - params.frame_size, x.shape[0] else: start, stop = i, i + params.frame_size - xx = preprocess( - x[start:stop], - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - ) - + xx = x[start:stop] xx = xx.reshape(feat_shape) + xx = augmenter(xx, training=False) runner.set_inputs(xx) runner.perform_inference() yy = runner.get_outputs() diff --git a/heartkit/tasks/rhythm/evaluate.py b/heartkit/tasks/rhythm/evaluate.py index b0ca3158..4bf43e63 100644 --- a/heartkit/tasks/rhythm/evaluate.py +++ b/heartkit/tasks/rhythm/evaluate.py @@ -1,49 +1,34 @@ -import logging import os import numpy as np -import tensorflow as tf -from sklearn.metrics import f1_score - +import keras import neuralspot_edge as nse -from ...defines import HKTestParams -from ...utils import set_random_seed, setup_logger -from ..utils import load_datasets + +from ...defines import HKTaskParams +from ...datasets import DatasetFactory from .datasets import load_test_dataset -def evaluate(params: HKTestParams): +def evaluate(params: HKTaskParams): """Evaluate model Args: - params (HKTestParams): Evaluation parameters + params (HKTaskParams): Evaluation parameters """ - logger = setup_logger(__name__, level=params.verbose) - - params.seed = set_random_seed(params.seed) - logger.debug(f"Random seed {params.seed}") - os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "test.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "test.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) + params.seed = nse.utils.set_random_seed(params.seed) + logger.debug(f"Random seed {params.seed}") class_names = params.class_names or [f"Class {i}" for i in range(params.num_classes)] - feat_shape = (params.frame_size, 1) - class_shape = (params.num_classes,) - - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype=tf.float32), - tf.TensorSpec(shape=class_shape, dtype=tf.int32), - ) - - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - test_ds = load_test_dataset(datasets=datasets, params=params, ds_spec=ds_spec) - test_x, test_y = next(test_ds.batch(params.test_size).as_numpy_iterator()) + test_ds = load_test_dataset(datasets=datasets, params=params) + test_x = np.concatenate([x for x, _ in test_ds.as_numpy_iterator()]) + test_y = np.concatenate([y for _, y in test_ds.as_numpy_iterator()]) logger.debug("Loading model") model = nse.models.load_model(params.model_file) @@ -53,35 +38,26 @@ def evaluate(params: HKTestParams): logger.debug(f"Model requires {flops/1e6:0.2f} MFLOPS") logger.debug("Performing inference") - y_true = np.argmax(test_y, axis=-1) - y_prob = tf.nn.softmax(model.predict(test_x)).numpy() - y_pred = np.argmax(y_prob, axis=-1) - - # Summarize results - logger.info("Testing Results") - test_acc = np.sum(y_pred == y_true) / len(y_true) - test_f1 = f1_score(y_true, y_pred, average="weighted") - logger.info(f"[TEST SET] ACC={test_acc:.2%}, F1={test_f1:.2%}") - - if params.num_classes == 2: - roc_path = params.job_dir / "roc_auc_test.png" - nse.plotting.roc.roc_auc_plot(y_true, y_prob[:, 1], labels=class_names, save_path=roc_path) - # END IF + rst = model.evaluate(test_ds, verbose=params.verbose, return_dict=True) + logger.info("[TEST SET] " + ", ".join([f"{k.upper()}={v:.2%}" for k, v in rst.items()])) # If threshold given, only count predictions above threshold + y_true = np.argmax(test_y, axis=-1) + y_prob = keras.ops.softmax(model.predict(test_x, verbose=params.verbose)).numpy() + y_pred = np.argmax(y_prob, axis=-1) if params.threshold: prev_numel = len(y_true) - y_prob, y_pred, y_true = nse.metrics.threshold.threshold_predictions(y_prob, y_pred, y_true, params.threshold) - drop_perc = 1 - len(y_true) / prev_numel - test_acc = np.sum(y_pred == y_true) / len(y_true) - test_f1 = f1_score(y_true, y_pred, average="weighted") - logger.info(f"[TEST SET] THRESH={params.threshold:0.2%}, DROP={drop_perc:.2%}") - logger.info(f"[TEST SET] ACC={test_acc:.2%}, F1={test_f1:.2%}") + indices = nse.metrics.threshold.get_predicted_threshold_indices(y_prob, y_pred, params.threshold) + test_x, test_y = test_x[indices], test_y[indices] + y_true, y_pred = y_true[indices], y_pred[indices] + rst = model.evaluate(test_x, test_y, verbose=params.verbose, return_dict=True) + logger.info(f"[TEST SET] THRESH={params.threshold:0.2%}, DROP={1 - len(indices) / prev_numel:.2%}") + logger.info("[TEST SET] " + ", ".join([f"{k.upper()}={v:.2%}" for k, v in rst.items()])) # END IF cm_path = params.job_dir / "confusion_matrix_test.png" - nse.plotting.cm.confusion_matrix_plot(y_true, y_pred, labels=class_names, save_path=cm_path, normalize="true") - nse.plotting.cm.px_plot_confusion_matrix( + nse.plotting.confusion_matrix_plot(y_true, y_pred, labels=class_names, save_path=cm_path, normalize="true") + nse.plotting.px_plot_confusion_matrix( y_true, y_pred, labels=class_names, diff --git a/heartkit/tasks/rhythm/export.py b/heartkit/tasks/rhythm/export.py index 7da19db8..2bfb0ff0 100644 --- a/heartkit/tasks/rhythm/export.py +++ b/heartkit/tasks/rhythm/export.py @@ -5,23 +5,20 @@ import keras import numpy as np -import tensorflow as tf -from sklearn.metrics import f1_score - import neuralspot_edge as nse -from ...defines import HKExportParams -from ...utils import setup_logger -from ..utils import load_datasets + +from ...defines import HKTaskParams +from ...datasets import DatasetFactory from .datasets import load_test_dataset -def export(params: HKExportParams): +def export(params: HKTaskParams): """Export model Args: - params (HKExportParams): Deployment parameters + params (HKTaskParams): Deployment parameters """ - logger = setup_logger(__name__, level=params.verbose) + logger = nse.utils.setup_logger(__name__, level=params.verbose) os.makedirs(params.job_dir, exist_ok=True) logger.debug(f"Creating working directory in {params.job_dir}") @@ -33,41 +30,24 @@ def export(params: HKExportParams): tfl_model_path = params.job_dir / "model.tflite" tflm_model_path = params.job_dir / "model_buffer.h" - # classes = sorted(list(set(params.class_map.values()))) - # class_names = params.class_names or [f"Class {i}" for i in range(params.num_classes)] - feat_shape = (params.frame_size, 1) - class_shape = (params.num_classes,) - - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype=tf.float32), - tf.TensorSpec(shape=class_shape, dtype=tf.int32), - ) - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - test_ds = load_test_dataset(datasets=datasets, params=params, ds_spec=ds_spec) - test_x, test_y = next(test_ds.batch(params.test_size).as_numpy_iterator()) + test_ds = load_test_dataset(datasets=datasets, params=params) + test_x = np.concatenate([x for x, _ in test_ds.as_numpy_iterator()]) + test_y = np.concatenate([y for _, y in test_ds.as_numpy_iterator()]) # Load model and set fixed batch size of 1 logger.debug("Loading trained model") model = nse.models.load_model(params.model_file) + # Add softmax layer if required if not params.use_logits and not isinstance(model.layers[-1], keras.layers.Softmax): - last_layer_name = model.layers[-1].name - - def call_function(layer, *args, **kwargs): - out = layer(*args, **kwargs) - if layer.name == last_layer_name: - out = keras.layers.Softmax()(out) - return out - - # END DEF - model_clone = keras.models.clone_model(model, call_function=call_function) - model_clone.set_weights(model.get_weights()) - model = model_clone + model = nse.models.append_layers(model, layers=[keras.layers.Softmax()], copy_weights=True) # END IF - inputs = keras.Input(shape=ds_spec[0].shape, batch_size=1, name="input", dtype=ds_spec[0].dtype.name) + + inputs = keras.Input(shape=feat_shape, batch_size=1, name="input", dtype="float32") model(inputs) flops = nse.metrics.flops.get_flops(model, batch_size=1, fpath=params.job_dir / "model_flops.log") @@ -102,25 +82,32 @@ def call_function(layer, *args, **kwargs): tflite.compile() # Verify TFLite results match TF results + metrics = [ + keras.metrics.CategoricalCrossentropy(name="loss", from_logits=params.use_logits), + keras.metrics.CategoricalAccuracy(name="acc"), + keras.metrics.F1Score(name="f1", average="weighted"), + ] + + if params.val_metric not in [m.name for m in metrics]: + raise ValueError(f"Metric {params.val_metric} not supported") + logger.info("Validating model results") - y_true = np.argmax(test_y, axis=-1) - y_pred_tf = np.argmax(model.predict(test_x), axis=-1) - y_pred_tfl = np.argmax(tflite.predict(x=test_x), axis=-1) + y_true = test_y + y_pred_tf = model.predict(test_x) + y_pred_tfl = tflite.predict(x=test_x) - tf_acc = np.sum(y_true == y_pred_tf) / y_true.size - tf_f1 = f1_score(y_true, y_pred_tf, average="weighted") - logger.info(f"[TF SET] ACC={tf_acc:.2%}, F1={tf_f1:.2%}") + tf_rst = nse.metrics.compute_metrics(metrics, y_true, y_pred_tf) + tfl_rst = nse.metrics.compute_metrics(metrics, y_true, y_pred_tfl) + logger.info("[TF METRICS] " + " ".join([f"{k.upper()}={v:.2%}" for k, v in tf_rst.items()])) + logger.info("[TFL METRICS] " + " ".join([f"{k.upper()}={v:.2%}" for k, v in tfl_rst.items()])) - tfl_acc = np.sum(y_true == y_pred_tfl) / y_true.size - tfl_f1 = f1_score(y_true, y_pred_tfl, average="weighted") - logger.info(f"[TFL SET] ACC={tfl_acc:.2%}, F1={tfl_f1:.2%}") + metric_diff = abs(tf_rst[params.val_metric] - tfl_rst[params.val_metric]) # Check accuracy hit - tfl_acc_drop = max(0, tf_acc - tfl_acc) - if params.val_acc_threshold is not None and (1 - tfl_acc_drop) < params.val_acc_threshold: - logger.warning(f"TFLite accuracy dropped by {tfl_acc_drop:0.2%}") - elif params.val_acc_threshold: - logger.info(f"Validation passed ({tfl_acc_drop:0.2%})") + if params.val_metric_threshold is not None and metric_diff > params.val_metric_threshold: + logger.warning(f"TFLite accuracy dropped by {metric_diff:0.2%}") + elif params.val_metric_threshold: + logger.info(f"Validation passed ({metric_diff:0.2%})") if params.tflm_file and tflm_model_path != params.tflm_file: logger.debug(f"Copying TFLM header to {params.tflm_file}") diff --git a/heartkit/tasks/rhythm/train.py b/heartkit/tasks/rhythm/train.py index c6343403..0d4977c6 100644 --- a/heartkit/tasks/rhythm/train.py +++ b/heartkit/tasks/rhythm/train.py @@ -1,20 +1,17 @@ -import logging import os import keras import numpy as np import sklearn.utils -import tensorflow as tf import wandb -from sklearn.metrics import f1_score from wandb.keras import WandbMetricsLogger, WandbModelCheckpoint - import neuralspot_edge as nse -from ...defines import HKTrainParams -from ...utils import env_flag, set_random_seed, setup_logger -from ..utils import load_datasets + +from ...defines import HKTaskParams +from ...datasets import DatasetFactory from .datasets import load_train_datasets from ...models import ModelFactory +from ...utils import dark_theme, setup_plotting class TermineTrainingError(Exception): @@ -26,28 +23,24 @@ def on_train_end(self, epoch, logs=None): raise TermineTrainingError("Training stopped by KillerCallBack") -def train(params: HKTrainParams): +def train(params: HKTaskParams): """Train model Args: - params (HKTrainParams): Training parameters + params (HKTaskParams): Training parameters """ - logger = setup_logger(__name__, level=params.verbose) - - params.seed = set_random_seed(params.seed) - logger.debug(f"Random seed {params.seed}") - os.makedirs(params.job_dir, exist_ok=True) + + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "train.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "train.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) + params.seed = nse.utils.set_random_seed(params.seed) + logger.debug(f"Random seed {params.seed}") with open(params.job_dir / "train_config.json", "w", encoding="utf-8") as fp: fp.write(params.model_dump_json(indent=2)) - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): wandb.init( project=params.project, entity="ambiq", @@ -56,40 +49,27 @@ def train(params: HKTrainParams): wandb.config.update(params.model_dump()) # END IF - classes = sorted(list(set(params.class_map.values()))) + classes = sorted(set(params.class_map.values())) class_names = params.class_names or [f"Class {i}" for i in range(params.num_classes)] feat_shape = (params.frame_size, 1) - class_shape = (params.num_classes,) - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype=tf.float32), - tf.TensorSpec(shape=class_shape, dtype=tf.int32), - ) - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - train_ds, val_ds = load_train_datasets( - datasets=datasets, - params=params, - ds_spec=ds_spec, - ) + train_ds, val_ds = load_train_datasets(datasets=datasets, params=params) - test_labels = [label.numpy() for _, label in val_ds] - y_true = np.argmax(np.concatenate(test_labels), axis=-1) + y_true = np.concatenate([y for _, y in val_ds.as_numpy_iterator()]) + y_true = np.argmax(y_true, axis=-1).flatten() class_weights = 0.25 if params.class_weights == "balanced": class_weights = sklearn.utils.compute_class_weight("balanced", classes=np.array(classes), y=y_true) class_weights = (class_weights + class_weights.mean()) / 2 # Smooth out + class_weights = class_weights.tolist() # END IF logger.debug(f"Class weights: {class_weights}") - inputs = keras.Input( - shape=ds_spec[0].shape, - batch_size=None, - name="input", - dtype=ds_spec[0].dtype.name, - ) + inputs = keras.Input(shape=feat_shape, name="input", dtype="float32") # Load existing model if params.resume and params.model_file: @@ -98,6 +78,8 @@ def train(params: HKTrainParams): params.model_file = None else: logger.debug("Creating model from scratch") + if params.architecture is None: + raise ValueError("Model architecture must be specified") model = ModelFactory.get(params.architecture.name)( x=inputs, params=params.architecture.params, @@ -107,19 +89,14 @@ def train(params: HKTrainParams): flops = nse.metrics.flops.get_flops(model, batch_size=1, fpath=params.job_dir / "model_flops.log") - if params.lr_cycles > 1: - scheduler = keras.optimizers.schedules.CosineDecayRestarts( - initial_learning_rate=params.lr_rate, - first_decay_steps=int(0.1 * params.steps_per_epoch * params.epochs), - t_mul=1.65 / (0.1 * params.lr_cycles * (params.lr_cycles - 1)), - m_mul=0.4, - ) - else: - scheduler = keras.optimizers.schedules.CosineDecay( - initial_learning_rate=params.lr_rate, - decay_steps=params.steps_per_epoch * params.epochs, - ) - # END IF + t_mul = 1 + first_steps = (params.steps_per_epoch * params.epochs) / (np.power(params.lr_cycles, t_mul) - t_mul + 1) + scheduler = keras.optimizers.schedules.CosineDecayRestarts( + initial_learning_rate=params.lr_rate, + first_decay_steps=np.ceil(first_steps), + t_mul=t_mul, + m_mul=0.5, + ) optimizer = keras.optimizers.Adam(scheduler) loss = keras.losses.CategoricalFocalCrossentropy(from_logits=True, alpha=class_weights) @@ -129,7 +106,7 @@ def train(params: HKTrainParams): keras.metrics.F1Score(name="f1", average="weighted"), ] - if params.resume and params.weights_file: + if params.resume and params.weights_file and params.weights_file.exists(): logger.debug(f"Hydrating model weights from file {params.weights_file}") model.load_weights(params.weights_file) @@ -141,7 +118,7 @@ def train(params: HKTrainParams): logger.debug(f"Model requires {flops/1e6:0.2f} MFLOPS") ModelCheckpoint = keras.callbacks.ModelCheckpoint - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): ModelCheckpoint = WandbModelCheckpoint model_callbacks = [ keras.callbacks.EarlyStopping( @@ -160,22 +137,22 @@ def train(params: HKTrainParams): ), keras.callbacks.CSVLogger(params.job_dir / "history.csv"), ] - if env_flag("TENSORBOARD"): + if nse.utils.env_flag("TENSORBOARD"): model_callbacks.append( keras.callbacks.TensorBoard( log_dir=params.job_dir, write_steps_per_second=True, ) ) - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): model_callbacks.append(WandbMetricsLogger()) # NOTE: A bug w/ Keras/TF causes model.fit to hang on last epoch. # This workaround terminates training on KeyboardInterrupt or last epoch. - model_callbacks.append(TerminateTrainingCallback()) + # model_callbacks.append(TerminateTrainingCallback()) try: - model.fit( + history = model.fit( train_ds, steps_per_epoch=params.steps_per_epoch, verbose=params.verbose, @@ -186,24 +163,33 @@ def train(params: HKTrainParams): except (KeyboardInterrupt, TermineTrainingError): logger.warning("Stopping training due to interrupt") - logger.debug(f"Model saved to {params.model_file}") - - # Get full validation results - model = keras.models.load_model(params.model_file) - logger.debug("Performing full validation") - y_pred = np.argmax(model.predict(val_ds), axis=-1) - - cm_path = params.job_dir / "confusion_matrix.png" - nse.plotting.cm.confusion_matrix_plot(y_true, y_pred, labels=class_names, save_path=cm_path, normalize="true") - if env_flag("WANDB"): - conf_mat = wandb.plot.confusion_matrix(preds=y_pred, y_true=y_true, class_names=class_names) - wandb.log({"conf_mat": conf_mat}) - # END IF - - # Summarize results - test_acc = np.sum(y_pred == y_true) / len(y_true) - test_f1 = f1_score(y_true, y_pred, average="weighted") - logger.info(f"[VAL SET] ACC={test_acc:.2%}, F1={test_f1:.2%}") - - # os.abort() + + logger.debug(f"Model saved to {params.model_file}") + + setup_plotting(dark_theme) + if history: + nse.plotting.plot_history_metrics( + history.history, + metrics=["loss", "acc"], + save_path=params.job_dir / "history.png", + stack=True, + figsize=(9, 5), + ) + + # Get full validation results + logger.debug("Performing full validation") + y_pred = np.argmax(model.predict(val_ds), axis=-1) + + cm_path = params.job_dir / "confusion_matrix.png" + nse.plotting.confusion_matrix_plot(y_true, y_pred, labels=class_names, save_path=cm_path, normalize="true") + if nse.utils.env_flag("WANDB"): + conf_mat = wandb.plot.confusion_matrix(preds=y_pred, y_true=y_true, class_names=class_names) + wandb.log({"conf_mat": conf_mat}) + # END IF + + # Summarize results + rst = model.evaluate(val_ds, return_dict=True) + logger.info("[VAL SET] " + ", ".join(f"{k.upper()}={v:.2%}" for k, v in rst.items())) + + # os.abort() # END TRY diff --git a/heartkit/tasks/segmentation/__init__.py b/heartkit/tasks/segmentation/__init__.py index 99613046..b8a7afa3 100644 --- a/heartkit/tasks/segmentation/__init__.py +++ b/heartkit/tasks/segmentation/__init__.py @@ -1,4 +1,4 @@ -from ...defines import HKDemoParams, HKExportParams, HKTestParams, HKTrainParams +from ...defines import HKTaskParams from ..task import HKTask from .defines import HKSegment from .demo import demo @@ -11,17 +11,17 @@ class SegmentationTask(HKTask): """HeartKit Segmentation Task""" @staticmethod - def train(params: HKTrainParams): + def train(params: HKTaskParams): train(params) @staticmethod - def evaluate(params: HKTestParams): + def evaluate(params: HKTaskParams): evaluate(params) @staticmethod - def export(params: HKExportParams): + def export(params: HKTaskParams): export(params) @staticmethod - def demo(params: HKDemoParams): + def demo(params: HKTaskParams): demo(params) diff --git a/heartkit/tasks/segmentation/dataloaders/__init__.py b/heartkit/tasks/segmentation/dataloaders/__init__.py index 5e446929..7b702755 100644 --- a/heartkit/tasks/segmentation/dataloaders/__init__.py +++ b/heartkit/tasks/segmentation/dataloaders/__init__.py @@ -1,7 +1,16 @@ -from .icentia11k import icentia11k_data_generator, icentia11k_label_map -from .ludb import ludb_data_generator, ludb_label_map -from .ptbxl import ptbxl_data_generator, ptbxl_label_map +import neuralspot_edge as nse -# from .qtdb import qtdb_data_generator -from .synthetic import synthetic_data_generator, synthetic_label_map -from .syntheticppg import syntheticppg_data_generator, syntheticppg_label_map +from ....datasets import HKDataloader + +from .icentia11k import Icentia11kDataloader +from .ludb import LudbDataloader +from .ptbxl import PtbxlDataloader +from .ecg_synthetic import EcgSyntheticDataloader +from .ppg_synthetic import PPgSyntheticDataloader + +SegmentationDataloaderFactory = nse.utils.create_factory(factory="HKSegmentationDataloaderFactory", type=HKDataloader) +SegmentationDataloaderFactory.register("icentia11k", Icentia11kDataloader) +SegmentationDataloaderFactory.register("ludb", LudbDataloader) +SegmentationDataloaderFactory.register("ptbxl", PtbxlDataloader) +SegmentationDataloaderFactory.register("ecg-synthetic", EcgSyntheticDataloader) +SegmentationDataloaderFactory.register("ppg-synthetic", PPgSyntheticDataloader) diff --git a/heartkit/tasks/segmentation/dataloaders/ecg_synthetic.py b/heartkit/tasks/segmentation/dataloaders/ecg_synthetic.py new file mode 100644 index 00000000..52638101 --- /dev/null +++ b/heartkit/tasks/segmentation/dataloaders/ecg_synthetic.py @@ -0,0 +1,88 @@ +import random +from typing import Generator, Iterable + +import numpy as np +import numpy.typing as npt +import physiokit as pk +import neuralspot_edge as nse + +from ....datasets import EcgSyntheticDataset, HKDataloader +from ..defines import HKSegment + +EcgSyntheticSegmentationMap = { + pk.ecg.EcgSegment.tp_overlap: HKSegment.pwave, + pk.ecg.EcgSegment.p_wave: HKSegment.pwave, + pk.ecg.EcgSegment.qrs_complex: HKSegment.qrs, + pk.ecg.EcgSegment.t_wave: HKSegment.twave, + pk.ecg.EcgSegment.background: HKSegment.normal, + pk.ecg.EcgSegment.u_wave: HKSegment.uwave, + pk.ecg.EcgSegment.pr_segment: HKSegment.normal, + pk.ecg.EcgSegment.st_segment: HKSegment.normal, + pk.ecg.EcgSegment.tp_segment: HKSegment.normal, +} + + +class EcgSyntheticDataloader(HKDataloader): + def __init__(self, ds: EcgSyntheticDataset, **kwargs): + super().__init__(ds=ds, **kwargs) + # Update label map + if self.label_map: + self.label_map = { + k: self.label_map[v] for (k, v) in EcgSyntheticSegmentationMap.items() if v in self.label_map + } + # END DEF + + def patient_data_generator( + self, + patient_id: int, + samples_per_patient: int, + ): + input_size = int(np.ceil((self.ds.sampling_rate / self.sampling_rate) * self.frame_size)) + + start_offset = 0 + + with self.ds.patient_data(patient_id) as h5: + data = h5["data"][:] + segs = h5["segmentations"][:] + # END WITH + + for _ in range(samples_per_patient): + lead = random.choice(self.ds.leads) + start = np.random.randint(start_offset, data.shape[1] - input_size) + x = data[lead, start : start + input_size].squeeze() + x = np.nan_to_num(x).astype(np.float32) + x = self.ds.add_noise(x) + y = segs[lead, start : start + input_size].squeeze() + y = y.astype(np.int32) + y = np.vectorize(lambda v: self.label_map.get(v, 0), otypes=[int])(y) + + if self.ds.sampling_rate != self.sampling_rate: + ratio = self.sampling_rate / self.ds.sampling_rate + x = pk.signal.resample_signal(x, self.ds.sampling_rate, self.sampling_rate, axis=0) + x = x[: self.frame_size] # Ensure frame size + y_tgt = np.zeros(x.shape, dtype=np.int32) + start_idxs = np.hstack((0, np.nonzero(np.diff(y))[0])) + end_idxs = np.hstack((start_idxs[1:], y.size)) + for s, e in zip(start_idxs, end_idxs): + y_tgt[int(s * ratio) : int(e * ratio)] = y[s] + # END FOR + y = y_tgt + # END IF + x = x.reshape(-1, 1) + yield x, y + # END FOR + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + if isinstance(samples_per_patient, Iterable): + samples_per_patient = samples_per_patient[0] + + for pt_id in nse.utils.uniform_id_generator(patient_ids, shuffle=shuffle): + for x, y in self.patient_data_generator(pt_id, samples_per_patient): + yield x, y + # END FOR + # END FOR diff --git a/heartkit/tasks/segmentation/dataloaders/icentia11k.py b/heartkit/tasks/segmentation/dataloaders/icentia11k.py index 6037aa8c..ab66bf6e 100644 --- a/heartkit/tasks/segmentation/dataloaders/icentia11k.py +++ b/heartkit/tasks/segmentation/dataloaders/icentia11k.py @@ -3,62 +3,24 @@ import numpy as np import numpy.typing as npt import physiokit as pk +import neuralspot_edge as nse -from ....datasets.defines import PatientGenerator -from ....datasets.icentia11k import IcentiaBeat, IcentiaDataset +from ....datasets import IcentiaBeat, IcentiaDataset, HKDataloader from ..defines import HKSegment -def icentia11k_label_map( - label_map: dict[int, int] | None = None, -) -> dict[int, int]: - """Get label map +class Icentia11kDataloader(HKDataloader): + def __init__(self, ds: IcentiaDataset, **kwargs): + super().__init__(ds=ds, **kwargs) - Args: - label_map (dict[int, int]|None): Label map + def patient_data_generator( + self, + patient_id: int, + samples_per_patient: int, + ): + input_size = int(np.ceil((self.ds.sampling_rate / self.sampling_rate) * self.frame_size)) - Returns: - dict[int, int]: Label map - """ - return label_map - - -def icentia11k_data_generator( - patient_generator: PatientGenerator, - ds: IcentiaDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, - label_map: dict[int, int] | None = None, -) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: - """Generate frames w/ segmentation labels (e.g. qrs) using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: IcentiaDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - label_map (dict[int, int] | None, optional): Label map. Defaults to None. - - Returns: - Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Sample generator - """ - - if target_rate is None: - target_rate = ds.sampling_rate - # END IF - - if isinstance(samples_per_patient, Iterable): - samples_per_patient = samples_per_patient[0] - - tgt_map = label_map # We generate the labels in the generator - - input_size = int(np.round((ds.sampling_rate / target_rate) * frame_size)) - - # For each patient - for pt in patient_generator: - with ds.patient_data(pt) as segments: + with self.ds.patient_data(patient_id) as segments: for _ in range(samples_per_patient): # Randomly pick a segment seg_key = np.random.choice(list(segments.keys())) @@ -68,9 +30,10 @@ def icentia11k_data_generator( # Get data and labels data = segments[seg_key]["data"][frame_start:frame_end].squeeze() - if ds.sampling_rate != target_rate: - ds_ratio = target_rate / ds.sampling_rate - data = pk.signal.resample_signal(data, ds.sampling_rate, target_rate, axis=0) + if self.ds.sampling_rate != self.sampling_rate: + ds_ratio = self.sampling_rate / self.ds.sampling_rate + data = pk.signal.resample_signal(data, self.ds.sampling_rate, self.sampling_rate, axis=0) + data = data[: self.frame_size] # Ensure frame size else: ds_ratio = 1 @@ -95,23 +58,13 @@ def icentia11k_data_generator( # Unclassifiable beat (treat as noise?) if btype == IcentiaBeat.undefined: pass - # noise_lbl = self.class_map.get(HeartSegment.noise.value, -1) - # # Skip if not in class map - # if noise_lbl == -1 - # continue - # # Mark region as noise - # win_len = max(1, int(0.2 * self.target_rate)) # 200 ms - # b_left = max(0, bidx - win_len) - # b_right = min(data.shape[0], bidx + win_len) - # mask[b_left:b_right] = noise_lbl - # Normal, PAC, PVC beat else: - qrs_width = int(0.08 * target_rate) # 80 ms + qrs_width = int(0.08 * self.sampling_rate) # 80 ms # Extract QRS segment qrs = pk.signal.moving_gradient_filter( data, - sample_rate=target_rate, + sample_rate=self.sampling_rate, sig_window=0.1, avg_window=1.0, sig_prom_weight=1.5, @@ -125,12 +78,27 @@ def icentia11k_data_generator( offset = offset[0] if offset.size else win_len qrs_onset = bidx - onset qrs_offset = bidx + offset - mask[qrs_onset:qrs_offset] = tgt_map.get(HKSegment.qrs.value, 0) + mask[qrs_onset:qrs_offset] = self.label_map.get(HKSegment.qrs.value, 0) # END IF # END FOR x = np.nan_to_num(data).astype(np.float32) + x = x.reshape(-1, 1) y = mask.astype(np.int32) yield x, y # END FOR # END WITH - # END FOR + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + if isinstance(samples_per_patient, Iterable): + samples_per_patient = samples_per_patient[0] + + for pt_id in nse.utils.uniform_id_generator(patient_ids, shuffle=shuffle): + for x, y in self.patient_data_generator(pt_id, samples_per_patient): + yield x, y + # END FOR + # END FOR diff --git a/heartkit/tasks/segmentation/dataloaders/ludb.py b/heartkit/tasks/segmentation/dataloaders/ludb.py index 4b7f3b5e..9cce52aa 100644 --- a/heartkit/tasks/segmentation/dataloaders/ludb.py +++ b/heartkit/tasks/segmentation/dataloaders/ludb.py @@ -1,20 +1,13 @@ import random -from typing import Generator +from typing import Generator, Iterable import numpy as np import numpy.typing as npt import physiokit as pk +import neuralspot_edge as nse -from ....datasets.defines import PatientGenerator -from ....datasets.ludb import ( - FID_LOC_IDX, - SEG_BEG_IDX, - SEG_END_IDX, - SEG_LBL_IDX, - SEG_LEAD_IDX, - LudbDataset, - LudbSegmentation, -) +from ....datasets import HKDataloader, LudbDataset, LudbSegmentation +from ....datasets.ludb import FID_LOC_IDX, SEG_BEG_IDX, SEG_END_IDX, SEG_LBL_IDX, SEG_LEAD_IDX from ..defines import HKSegment LudbSegmentationMap = { @@ -25,59 +18,26 @@ } -def ludb_label_map( - label_map: dict[int, int] | None = None, -) -> dict[int, int]: - """Get label map - - Args: - label_map (dict[int, int]|None): Label map - - Returns: - dict[int, int]: Label map - """ - return {k: label_map.get(v, -1) for (k, v) in LudbSegmentationMap.items()} - - -def ludb_data_generator( - patient_generator: PatientGenerator, - ds: LudbDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, - label_map: dict[int, int] | None = None, -) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: - """Generate frames w/ rhythm labels (e.g. afib) using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: IcentiaDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - label_map (dict[int, int] | None, optional): Label map. Defaults to None. - - Returns: - Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Sample generator - """ - - if target_rate is None: - target_rate = ds.sampling_rate - # END IF - - # Convert global labels -> ds labels -> class labels (-1 indicates not in class map) - tgt_map = ludb_label_map(label_map) - - for pt in patient_generator: - with ds.patient_data(pt) as h5: - data = h5["data"][:] - segs = h5["segmentations"][:] - fids = h5["fiducials"][:] +class LudbDataloader(HKDataloader): + def __init__(self, ds: LudbDataset, **kwargs): + super().__init__(ds=ds, **kwargs) + if self.label_map: + self.label_map = {k: self.label_map[v] for (k, v) in LudbSegmentationMap.items() if v in self.label_map} + + def patient_data_generator( + self, + patient_id: int, + samples_per_patient: int, + ): + with self.ds.patient_data(patient_id) as h5: + data = h5["data"][:].copy() + segs = h5["segmentations"][:].copy() + fids = h5["fiducials"][:].copy() # END WITH - if ds.sampling_rate != target_rate: - ratio = target_rate / ds.sampling_rate - data = pk.signal.resample_signal(data, ds.sampling_rate, target_rate, axis=0) + if self.ds.sampling_rate != self.sampling_rate: + ratio = self.sampling_rate / self.ds.sampling_rate + data = pk.signal.resample_signal(data, self.ds.sampling_rate, self.sampling_rate, axis=0) segs[:, (SEG_BEG_IDX, SEG_END_IDX)] = segs[:, (SEG_BEG_IDX, SEG_END_IDX)] * ratio fids[:, FID_LOC_IDX] = fids[:, FID_LOC_IDX] * ratio # END IF @@ -93,13 +53,29 @@ def ludb_data_generator( stop_offset = max(0, data.shape[0] - segs[-1][SEG_END_IDX] + 100) for _ in range(samples_per_patient): # Randomly pick an ECG lead - lead = random.choice(ds.leads) + lead = random.choice(self.ds.leads) # Randomly select frame within the segment - frame_start = np.random.randint(start_offset, data.shape[0] - frame_size - stop_offset) - frame_end = frame_start + frame_size - x = data[frame_start:frame_end, lead].astype(np.float32) + frame_start = np.random.randint(start_offset, data.shape[0] - self.frame_size - stop_offset) + frame_end = frame_start + self.frame_size + x = data[frame_start:frame_end, lead] + x = np.nan_to_num(x, neginf=0, posinf=0).astype(np.float32) + x = np.reshape(x, (-1, 1)) y = labels[frame_start:frame_end, lead].astype(np.int32) - y = np.vectorize(tgt_map.get, otypes=[int])(y) + y = np.vectorize(lambda v: self.label_map.get(v, 0), otypes=[int])(y) yield x, y # END FOR - # END FOR + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + if isinstance(samples_per_patient, Iterable): + samples_per_patient = samples_per_patient[0] + + for pt_id in nse.utils.uniform_id_generator(patient_ids, shuffle=shuffle): + for x, y in self.patient_data_generator(pt_id, samples_per_patient): + yield x, y + # END FOR + # END FOR diff --git a/heartkit/tasks/segmentation/dataloaders/ppg_synthetic.py b/heartkit/tasks/segmentation/dataloaders/ppg_synthetic.py new file mode 100644 index 00000000..08f5cc28 --- /dev/null +++ b/heartkit/tasks/segmentation/dataloaders/ppg_synthetic.py @@ -0,0 +1,80 @@ +from typing import Generator, Iterable + +import numpy as np +import numpy.typing as npt +import physiokit as pk +import neuralspot_edge as nse + +from ....datasets import PpgSyntheticDataset, HKDataloader +from ..defines import HKSegment + +PpgSyntheticSegmentationMap = { + pk.ppg.PpgSegment.background: HKSegment.normal, + pk.ppg.PpgSegment.systolic: HKSegment.systolic, + pk.ppg.PpgSegment.diastolic: HKSegment.diastolic, +} + + +class PPgSyntheticDataloader(HKDataloader): + def __init__(self, ds: PpgSyntheticDataset, **kwargs): + super().__init__(ds=ds, **kwargs) + # Update label map + if self.label_map: + self.label_map = { + k: self.label_map[v] for (k, v) in PpgSyntheticSegmentationMap.items() if v in self.label_map + } + # END DEF + + def patient_data_generator( + self, + patient_id: int, + samples_per_patient: int, + ): + input_size = int(np.ceil((self.ds.sampling_rate / self.sampling_rate) * self.frame_size)) + + start_offset = 0 + + with self.ds.patient_data(patient_id) as h5: + data = h5["data"][:] + segs = h5["segmentations"][:] + # END WITH + + for _ in range(samples_per_patient): + start = np.random.randint(start_offset, data.shape[0] - input_size) + x = data[start : start + input_size].squeeze() + x = np.nan_to_num(x).astype(np.float32) + x = self.ds.add_noise(x) + y = segs[start : start + input_size].squeeze() + y = y.astype(np.int32) + y = np.vectorize(lambda v: self.label_map.get(v, 0), otypes=[int])(y) + + if self.ds.sampling_rate != self.sampling_rate: + ratio = self.sampling_rate / self.ds.sampling_rate + x = pk.signal.resample_signal(x, self.ds.sampling_rate, self.sampling_rate, axis=0) + x = x[: self.frame_size] # Ensure frame size + y_tgt = np.zeros(x.shape, dtype=np.int32) + start_idxs = np.hstack((0, np.nonzero(np.diff(y))[0])) + end_idxs = np.hstack((start_idxs[1:], y.size)) + for s, e in zip(start_idxs, end_idxs): + y_tgt[int(s * ratio) : int(e * ratio)] = y[s] + # END FOR + y = y_tgt + # END IF + x = x.reshape(-1, 1) + yield x, y + # END FOR + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + if isinstance(samples_per_patient, Iterable): + samples_per_patient = samples_per_patient[0] + + for pt_id in nse.utils.uniform_id_generator(patient_ids, shuffle=shuffle): + for x, y in self.patient_data_generator(pt_id, samples_per_patient): + yield x, y + # END FOR + # END FOR diff --git a/heartkit/tasks/segmentation/dataloaders/ptbxl.py b/heartkit/tasks/segmentation/dataloaders/ptbxl.py index 271b0d40..4873a141 100644 --- a/heartkit/tasks/segmentation/dataloaders/ptbxl.py +++ b/heartkit/tasks/segmentation/dataloaders/ptbxl.py @@ -4,64 +4,24 @@ import numpy as np import numpy.typing as npt import physiokit as pk +import neuralspot_edge as nse -from ....datasets.defines import PatientGenerator -from ....datasets.ptbxl import PtbxlDataset +from ....datasets import HKDataloader, PtbxlDataset from ..defines import HKSegment -def ptbxl_label_map( - label_map: dict[int, int] | None = None, -) -> dict[int, int]: - """Get label map +class PtbxlDataloader(HKDataloader): + def __init__(self, ds: PtbxlDataset, **kwargs): + super().__init__(ds=ds, **kwargs) - Args: - label_map (dict[int, int]|None): Label map + def patient_data_generator( + self, + patient_id: int, + samples_per_patient: int, + ): + input_size = int(np.ceil((self.ds.sampling_rate / self.sampling_rate) * self.frame_size)) - Returns: - dict[int, int]: Label map - """ - - # We generate the labels in the generator - return label_map - - -def ptbxl_data_generator( - patient_generator: PatientGenerator, - ds: PtbxlDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, - label_map: dict[int, int] | None = None, -) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: - """Generate frames w/ segmentation labels (e.g. qrs) using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: PtbxlDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - label_map (dict[int, int] | None, optional): Label map. Defaults to None. - - Returns: - Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Sample generator - """ - - if target_rate is None: - target_rate = ds.sampling_rate - # END IF - - if isinstance(samples_per_patient, Iterable): - samples_per_patient = samples_per_patient[0] - - tgt_map = ptbxl_label_map(label_map=label_map) - - input_size = int(np.round((ds.sampling_rate / target_rate) * frame_size)) - - # For each patient - for pt in patient_generator: - with ds.patient_data(pt) as h5: + with self.ds.patient_data(patient_id) as h5: data = h5["data"][:] blabels = h5["blabels"][:] # END WITH @@ -70,16 +30,18 @@ def ptbxl_data_generator( blabels[:, 0] = blabels[:, 0] * 5 for _ in range(samples_per_patient): # Select random lead and start index - lead = random.choice(ds.leads) + lead = random.choice(self.ds.leads) frame_start = np.random.randint(0, data.shape[1] - input_size) frame_end = frame_start + input_size frame_blabels = blabels[(blabels[:, 0] >= frame_start) & (blabels[:, 0] < frame_end)] x = data[lead, frame_start:frame_end].copy() - if ds.sampling_rate != target_rate: - ds_ratio = target_rate / ds.sampling_rate - x = pk.signal.resample_signal(x, ds.sampling_rate, target_rate, axis=0) + if self.ds.sampling_rate != self.sampling_rate: + ds_ratio = self.sampling_rate / self.ds.sampling_rate + x = pk.signal.resample_signal(x, self.ds.sampling_rate, self.sampling_rate, axis=0) + x = x[: self.frame_size] # Ensure frame size else: ds_ratio = 1 + # Create segment mask mask = np.zeros_like(x, dtype=np.int32) @@ -99,20 +61,35 @@ def ptbxl_data_generator( # Extract QRS segment qrs = pk.signal.moving_gradient_filter( - x, sample_rate=target_rate, sig_window=0.1, avg_window=1.0, sig_prom_weight=1.5 + x, sample_rate=self.sampling_rate, sig_window=0.1, avg_window=1.0, sig_prom_weight=1.5 ) - win_len = max(1, int(0.08 * target_rate)) # 80 ms + win_len = max(1, int(0.08 * self.sampling_rate)) # 80 ms b_left = max(0, bidx - win_len) b_right = min(x.shape[0], bidx + win_len) onset = np.where(np.flip(qrs[b_left:bidx]) < 0)[0] onset = onset[0] if onset.size else win_len offset = np.where(qrs[bidx + 1 : b_right] < 0)[0] offset = offset[0] if offset.size else win_len - mask[bidx - onset : bidx + offset] = tgt_map.get(HKSegment.qrs.value, 0) + mask[bidx - onset : bidx + offset] = self.label_map.get(HKSegment.qrs.value, 0) # END IF # END FOR x = np.nan_to_num(x).astype(np.float32) + x = x.reshape(-1, 1) y = mask.astype(np.int32) yield x, y # END FOR - # END FOR + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + if isinstance(samples_per_patient, Iterable): + samples_per_patient = samples_per_patient[0] + + for pt_id in nse.utils.uniform_id_generator(patient_ids, shuffle=shuffle): + for x, y in self.patient_data_generator(pt_id, samples_per_patient): + yield x, y + # END FOR + # END FOR diff --git a/heartkit/tasks/segmentation/dataloaders/qtdb.py b/heartkit/tasks/segmentation/dataloaders/qtdb.py deleted file mode 100644 index d969850c..00000000 --- a/heartkit/tasks/segmentation/dataloaders/qtdb.py +++ /dev/null @@ -1,49 +0,0 @@ -# def segmentation_generator( -# self, -# patient_generator: PatientGenerator, -# samples_per_patient: int | list[int] = 1, -# ) -> SampleGenerator: -# """Generate frames and segment labels. - -# Args: -# patient_generator (PatientGenerator): Patient Generator -# samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. -# Returns: -# SampleGenerator: Sample generator -# Yields: -# Iterator[SampleGenerator] -# """ - -# for _, pt in patient_generator: -# # NOTE: [:] will load all data into RAM- ideal for small dataset -# data = pt["data"][:] -# segs = pt["segmentations"][:] -# fids = pt["fiducials"][:] - -# if self.sampling_rate != self.target_rate: -# ratio = self.target_rate / self.sampling_rate -# data = pk.signal.resample_signal(data, self.sampling_rate, self.target_rate, axis=0) -# segs[:, (SEG_BEG_IDX, SEG_END_IDX)] = segs[:, (SEG_BEG_IDX, SEG_END_IDX)] * ratio -# fids[:, FID_LOC_IDX] = fids[:, FID_LOC_IDX] * ratio -# # END IF - -# # Create segmentation mask -# labels = np.zeros_like(data) -# for seg_idx in range(segs.shape[0]): -# seg = segs[seg_idx] -# labels[seg[SEG_BEG_IDX] : seg[SEG_END_IDX], seg[SEG_LEAD_IDX]] = seg[SEG_LBL_IDX] -# # END FOR - -# start_offset = max(0, segs[0][SEG_BEG_IDX] - 100) -# stop_offset = max(0, data.shape[0] - segs[-1][SEG_END_IDX] + 100) -# for _ in range(samples_per_patient): -# # Randomly pick an ECG lead -# lead = np.random.randint(data.shape[1]) -# # Randomly select frame within the segment -# frame_start = np.random.randint(start_offset, data.shape[0] - self.frame_size - stop_offset) -# frame_end = frame_start + self.frame_size -# x = data[frame_start:frame_end, lead].astype(np.float32).reshape((self.frame_size,)) -# y = labels[frame_start:frame_end, lead].astype(np.int32) -# yield x, y -# # END FOR -# # END FOR diff --git a/heartkit/tasks/segmentation/dataloaders/synthetic.py b/heartkit/tasks/segmentation/dataloaders/synthetic.py deleted file mode 100644 index 3b253f89..00000000 --- a/heartkit/tasks/segmentation/dataloaders/synthetic.py +++ /dev/null @@ -1,104 +0,0 @@ -import random -from typing import Generator - -import numpy as np -import numpy.typing as npt -import physiokit as pk - -from ....datasets.defines import PatientGenerator -from ....datasets.synthetic import SyntheticDataset -from ..defines import HKSegment - -SyntheticSegmentationMap = { - pk.ecg.EcgSegment.tp_overlap: HKSegment.pwave, - pk.ecg.EcgSegment.p_wave: HKSegment.pwave, - pk.ecg.EcgSegment.qrs_complex: HKSegment.qrs, - pk.ecg.EcgSegment.t_wave: HKSegment.twave, - pk.ecg.EcgSegment.background: HKSegment.normal, - pk.ecg.EcgSegment.u_wave: HKSegment.uwave, - pk.ecg.EcgSegment.pr_segment: HKSegment.normal, - pk.ecg.EcgSegment.st_segment: HKSegment.normal, - pk.ecg.EcgSegment.tp_segment: HKSegment.normal, -} - - -def synthetic_label_map( - label_map: dict[int, int] | None = None, -) -> dict[int, int]: - """Get label map - - Args: - label_map (dict[int, int]|None): Label map - - Returns: - dict[int, int]: Label map - """ - return {k: label_map[v] for (k, v) in SyntheticSegmentationMap.items() if v in label_map} - # return {k: label_map.get(v, -1) for (k, v) in SyntheticSegmentationMap.items()} - - -def synthetic_data_generator( - patient_generator: PatientGenerator, - ds: SyntheticDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, - label_map: dict[int, int] | None = None, -) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: - """Generate frames w/ rhythm labels (e.g. afib) using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: IcentiaDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - label_map (dict[int, int] | None, optional): Label map. Defaults to None. - - Returns: - Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Sample generator - """ - - if target_rate is None: - target_rate = ds.sampling_rate - # END IF - - input_size = int(np.round((ds.sampling_rate / target_rate) * frame_size)) - - tgt_map = synthetic_label_map(label_map) - - start_offset = 0 - - for pt in patient_generator: - with ds.patient_data(pt) as h5: - data = h5["data"][:] - segs = h5["segmentations"][:] - # END WITH - - for _ in range(samples_per_patient): - lead = random.choice(ds.leads) - start = np.random.randint(start_offset, data.shape[1] - input_size) - x = data[lead, start : start + input_size].squeeze() - x = np.nan_to_num(x).astype(np.float32) - x = ds.add_noise(x) - y = segs[lead, start : start + input_size].squeeze() - y = y.astype(np.int32) - y = np.vectorize(lambda v: tgt_map.get(v, 0), otypes=[int])(y) - - if ds.sampling_rate != target_rate: - ratio = target_rate / ds.sampling_rate - x = pk.signal.resample_signal(x, ds.sampling_rate, target_rate, axis=0) - y_tgt = np.zeros(x.shape, dtype=np.int32) - start_idxs = np.hstack((0, np.nonzero(np.diff(y))[0])) - end_idxs = np.hstack((start_idxs[1:], y.size)) - for s, e in zip(start_idxs, end_idxs): - y_tgt[int(s * ratio) : int(e * ratio)] = y[s] - # END FOR - y = y_tgt - # NOTE: resample_categorical is not working - # y = pk.signal.filter.resample_categorical(y, ds.sampling_rate, target_rate, axis=0) - - # END IF - yield x, y - # END FOR - # END FOR diff --git a/heartkit/tasks/segmentation/dataloaders/syntheticppg.py b/heartkit/tasks/segmentation/dataloaders/syntheticppg.py deleted file mode 100644 index 137bec1b..00000000 --- a/heartkit/tasks/segmentation/dataloaders/syntheticppg.py +++ /dev/null @@ -1,92 +0,0 @@ -from typing import Generator - -import numpy as np -import numpy.typing as npt -import physiokit as pk - -from ....datasets.defines import PatientGenerator -from ....datasets.syntheticppg import SyntheticPpgDataset -from ..defines import HKSegment - -SyntheticPpgSegmentationMap = { - pk.ppg.PpgSegment.background: HKSegment.normal, - pk.ppg.PpgSegment.systolic: HKSegment.systolic, - pk.ppg.PpgSegment.diastolic: HKSegment.diastolic, -} - - -def syntheticppg_label_map( - label_map: dict[int, int] | None = None, -) -> dict[int, int]: - """Get label map - - Args: - label_map (dict[int, int]|None): Label map - - Returns: - dict[int, int]: Label map - """ - return {k: label_map[v] for (k, v) in SyntheticPpgSegmentationMap.items() if v in label_map} - - -def syntheticppg_data_generator( - patient_generator: PatientGenerator, - ds: SyntheticPpgDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, - label_map: dict[int, int] | None = None, -) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: - """Generate frames w/ rhythm labels (e.g. afib) using patient generator. - - Args: - patient_generator (PatientGenerator): Patient Generator - ds: IcentiaDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - label_map (dict[int, int] | None, optional): Label map. Defaults to None. - - Returns: - Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Sample generator - """ - - if target_rate is None: - target_rate = ds.sampling_rate - # END IF - - input_size = int(np.round((ds.sampling_rate / target_rate) * frame_size)) - - tgt_map = syntheticppg_label_map(label_map) - - start_offset = 0 - - for pt in patient_generator: - with ds.patient_data(pt) as h5: - data = h5["data"][:] - segs = h5["segmentations"][:] - # END WITH - - for _ in range(samples_per_patient): - start = np.random.randint(start_offset, data.shape[0] - input_size) - x = data[start : start + input_size].squeeze() - x = np.nan_to_num(x).astype(np.float32) - x = ds.add_noise(x) - y = segs[start : start + input_size].squeeze() - y = y.astype(np.int32) - y = np.vectorize(lambda v: tgt_map.get(v, 0), otypes=[int])(y) - - if ds.sampling_rate != target_rate: - ratio = target_rate / ds.sampling_rate - x = pk.signal.resample_signal(x, ds.sampling_rate, target_rate, axis=0) - y_tgt = np.zeros(x.shape, dtype=np.int32) - start_idxs = np.hstack((0, np.nonzero(np.diff(y))[0])) - end_idxs = np.hstack((start_idxs[1:], y.size)) - for s, e in zip(start_idxs, end_idxs): - y_tgt[int(s * ratio) : int(e * ratio)] = y[s] - # END FOR - y = y_tgt - # END IF - yield x, y - # END FOR - # END FOR diff --git a/heartkit/tasks/segmentation/datasets.py b/heartkit/tasks/segmentation/datasets.py index 89119446..72ce19b0 100644 --- a/heartkit/tasks/segmentation/datasets.py +++ b/heartkit/tasks/segmentation/datasets.py @@ -1,352 +1,166 @@ -import functools -import logging -from pathlib import Path - import numpy as np -import numpy.typing as npt import tensorflow as tf +import neuralspot_edge as nse from ...datasets import ( HKDataset, - augment_pipeline, - preprocess_pipeline, - uniform_id_generator, -) -from ...datasets.dataloader import test_dataloader, train_val_dataloader -from ...defines import ( - AugmentationParams, - HKExportParams, - HKTestParams, - HKTrainParams, - PreprocessParams, -) -from ...utils import resolve_template_path -from .dataloaders import ( - icentia11k_data_generator, - icentia11k_label_map, - ludb_data_generator, - ludb_label_map, - ptbxl_data_generator, - ptbxl_label_map, - synthetic_data_generator, - synthetic_label_map, - syntheticppg_data_generator, - syntheticppg_label_map, + create_augmentation_pipeline, ) +from ...datasets.dataloader import HKDataloader +from ...defines import HKTaskParams, NamedParams -logger = logging.getLogger(__name__) - - -def preprocess(x: npt.NDArray, preprocesses: list[PreprocessParams], sample_rate: float) -> npt.NDArray: - """Preprocess data pipeline - - Args: - x (npt.NDArray): Input data - preprocesses (list[PreprocessParams]): Preprocess parameters - sample_rate (float): Sample rate - - Returns: - npt.NDArray: Preprocessed data - """ - return preprocess_pipeline(x, preprocesses=preprocesses, sample_rate=sample_rate) - - -def augment(x: npt.NDArray, augmentations: list[AugmentationParams], sample_rate: float) -> npt.NDArray: - """Augment data pipeline - - Args: - x (npt.NDArray): Input data - augmentations (list[AugmentationParams]): Augmentation parameters - sample_rate (float): Sample rate - - Returns: - npt.NDArray: Augmented data - """ - return augment_pipeline( - x=x, - augmentations=augmentations, - sample_rate=sample_rate, - ) - - -def prepare( - x_y: tuple[npt.NDArray, npt.NDArray], - sample_rate: float, - preprocesses: list[PreprocessParams], - augmentations: list[AugmentationParams], - spec: tuple[tf.TensorSpec, tf.TensorSpec], - num_classes: int, -) -> tuple[npt.NDArray, npt.NDArray]: - """Prepare dataset - - Args: - x_y (tuple[npt.NDArray, int]): Input data and label - sample_rate (float): Sample rate - preprocesses (list[PreprocessParams]|None): Preprocess parameters - augmentations (list[AugmentationParams]|None): Augmentation parameters - spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - num_classes (int): Number of classes - - Returns: - tuple[npt.NDArray, npt.NDArray]: Data and label - """ - x, y = x_y[0].copy(), x_y[1] - - if augmentations: - x = augment(x, augmentations, sample_rate) - # END IF - - if preprocesses: - x = preprocess(x, preprocesses, sample_rate) - # END IF - - x = x.reshape(spec[0].shape) - y = tf.one_hot(y, num_classes) - - return x, y - +from .dataloaders import SegmentationDataloaderFactory as DataloaderFactory -def get_ds_label_map(ds: HKDataset, label_map: dict[int, int] | None = None) -> dict[int, int]: - """Get label map for dataset +logger = nse.utils.setup_logger(__name__) - Args: - ds (HKDataset): Dataset - label_map (dict[int, int]|None): Label map - Returns: - dict[int, int]: Label map - """ - match ds.name: - case "icentia11k": - return icentia11k_label_map(label_map=label_map) - case "ludb": - return ludb_label_map(label_map=label_map) - case "ptbxl": - return ptbxl_label_map(label_map=label_map) - case "synthetic": - return synthetic_label_map(label_map=label_map) - case "syntheticppg": - return syntheticppg_label_map(label_map=label_map) - case _: - raise ValueError(f"Dataset {ds.name} not supported") - # END MATCH - - -def get_data_generator( - ds: HKDataset, - frame_size: int, - samples_per_patient: int, - target_rate: int, - label_map: dict[int, int] | None = None, +def create_data_pipeline( + ds: tf.data.Dataset, + sampling_rate: int, + batch_size: int, + buffer_size: int | None = None, + augmentations: list[NamedParams] | None = None, + num_classes: int = 2, ): - """Get task data generator for dataset - - Args: - ds (HKDataset): Dataset - frame_size (int): Frame size - samples_per_patient (int): Samples per patient - target_rate (int): Target rate - label_map (dict[int, int]|None): Label map - - Returns: - callable: Data generator - """ - match ds.name: - case "icentia11k": - data_generator = icentia11k_data_generator - case "ludb": - data_generator = ludb_data_generator - case "ptbxl": - data_generator = ptbxl_data_generator - case "synthetic": - data_generator = synthetic_data_generator - case "syntheticppg": - data_generator = syntheticppg_data_generator - case _: - raise ValueError(f"Dataset {ds.name} not supported") - # END MATCH - return functools.partial( - data_generator, - ds=ds, - frame_size=frame_size, - samples_per_patient=samples_per_patient, - target_rate=target_rate, - label_map=label_map, + if buffer_size: + ds = ds.shuffle( + buffer_size=buffer_size, + reshuffle_each_iteration=True, + ) + if batch_size: + ds = ds.batch( + batch_size=batch_size, + drop_remainder=True, + num_parallel_calls=tf.data.AUTOTUNE, + ) + augmenter = create_augmentation_pipeline( + augmentations, + sampling_rate=sampling_rate, ) - - -def resolve_ds_cache_path(fpath: Path | None, ds: HKDataset, task: str, frame_size: int, sample_rate: int): - """Resolve dataset cache path - - Args: - fpath (Path|None): File path - ds (HKDataset): Dataset - task (str): Task - frame_size (int): Frame size - sample_rate (int): Sampling rate - - Returns: - Path|None: Resolved path - """ - if not fpath: - return None - return resolve_template_path( - fpath=fpath, - dataset=ds.name, - task=task, - frame_size=frame_size, - sampling_rate=sample_rate, + ds = ( + ds.map( + lambda data, labels: { + "data": tf.cast(data, "float32"), + "labels": tf.one_hot(labels, num_classes), + }, + num_parallel_calls=tf.data.AUTOTUNE, + ) + .map( + augmenter, + num_parallel_calls=tf.data.AUTOTUNE, + ) + .map( + lambda data: (data["data"], data["labels"]), + num_parallel_calls=tf.data.AUTOTUNE, + ) ) + return ds.prefetch(tf.data.AUTOTUNE) + def load_train_datasets( datasets: list[HKDataset], - params: HKTrainParams, - ds_spec: tuple[tf.TensorSpec, tf.TensorSpec], + params: HKTaskParams, ) -> tuple[tf.data.Dataset, tf.data.Dataset]: - """Load training and validation datasets - - Args: - datasets (list[HKDataset]): Datasets - params (HKTrainParams): Training parameters - ds_spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - - Returns: - tuple[tf.data.Dataset, tf.data.Dataset]: Train and validation datasets - """ - id_generator = functools.partial(uniform_id_generator, repeat=True) - train_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, - ) - train_datasets = [] val_datasets = [] for ds in datasets: - val_file = resolve_ds_cache_path( - params.val_file, - ds=ds, - task="segmentation", - frame_size=params.frame_size, - sample_rate=params.sampling_rate, - ) - data_generator = get_data_generator( + dataloader: HKDataloader = DataloaderFactory.get(ds.name)( ds=ds, frame_size=params.frame_size, - samples_per_patient=params.samples_per_patient, - target_rate=params.sampling_rate, + sampling_rate=params.sampling_rate, label_map=params.class_map, ) - train_ds, val_ds = train_val_dataloader( - ds=ds, - spec=ds_spec, - data_generator=data_generator, - id_generator=id_generator, + train_patients, val_patients = dataloader.split_train_val_patients( train_patients=params.train_patients, val_patients=params.val_patients, - val_pt_samples=params.val_samples_per_patient, - val_file=val_file, - val_size=params.val_size, - label_map=params.class_map, - label_type=None, - preprocess=train_prepare, - num_workers=params.data_parallelism, + ) + + train_ds = dataloader.create_dataloader( + patient_ids=train_patients, samples_per_patient=params.samples_per_patient, shuffle=True + ) + + val_ds = dataloader.create_dataloader( + patient_ids=val_patients, samples_per_patient=params.val_samples_per_patient, shuffle=False ) train_datasets.append(train_ds) val_datasets.append(val_ds) # END FOR - ds_weights = np.array([d.weight for d in params.datasets]) - ds_weights = ds_weights / ds_weights.sum() + ds_weights = None + if params.dataset_weights: + ds_weights = np.array(params.dataset_weights) + ds_weights = ds_weights / ds_weights.sum() train_ds = tf.data.Dataset.sample_from_datasets(train_datasets, weights=ds_weights) val_ds = tf.data.Dataset.sample_from_datasets(val_datasets, weights=ds_weights) # Shuffle and batch datasets for training - train_ds = ( - train_ds.shuffle( - buffer_size=params.buffer_size, - reshuffle_each_iteration=True, - ) - .batch( - batch_size=params.batch_size, - drop_remainder=False, - num_parallel_calls=tf.data.AUTOTUNE, - ) - .prefetch(buffer_size=tf.data.AUTOTUNE) + train_ds = create_data_pipeline( + ds=train_ds, + sampling_rate=params.sampling_rate, + batch_size=params.batch_size, + buffer_size=params.buffer_size, + augmentations=params.augmentations + params.preprocesses, + num_classes=params.num_classes, ) - val_ds = val_ds.batch( + + val_ds = create_data_pipeline( + ds=val_ds, + sampling_rate=params.sampling_rate, batch_size=params.batch_size, - drop_remainder=True, - num_parallel_calls=tf.data.AUTOTUNE, + buffer_size=None, + augmentations=params.augmentations + params.preprocesses, + num_classes=params.num_classes, ) + + # If given fixed val size or steps, then capture and cache + val_steps_per_epoch = params.val_size // params.batch_size if params.val_size else params.val_steps_per_epoch + if val_steps_per_epoch: + logger.info(f"Validation steps per epoch: {val_steps_per_epoch}") + val_ds = val_ds.take(val_steps_per_epoch).cache() + return train_ds, val_ds def load_test_dataset( datasets: list[HKDataset], - params: HKTestParams | HKExportParams, - ds_spec: tuple[tf.TensorSpec, tf.TensorSpec], + params: HKTaskParams, ) -> tf.data.Dataset: - """Load test dataset - - Args: - datasets (list[HKDataset]): Datasets - params (HKTestParams|HKExportParams): Test parameters - ds_spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - - Returns: - tf.data.Dataset: Test dataset - """ - - id_generator = functools.partial(uniform_id_generator, repeat=True) - test_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=None, # params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, - ) test_datasets = [] for ds in datasets: - test_file = resolve_ds_cache_path( - fpath=params.test_file, - ds=ds, - task="segmentation", - frame_size=params.frame_size, - sample_rate=params.sampling_rate, - ) - data_generator = get_data_generator( + dataloader: HKDataloader = DataloaderFactory.get(ds.name)( ds=ds, frame_size=params.frame_size, - samples_per_patient=params.test_samples_per_patient, - target_rate=params.sampling_rate, + sampling_rate=params.sampling_rate, label_map=params.class_map, ) - test_ds = test_dataloader( - ds=ds, - spec=ds_spec, - data_generator=data_generator, - id_generator=id_generator, - test_patients=params.test_patients, - test_file=test_file, - label_map=params.class_map, - label_type=None, - preprocess=test_prepare, - num_workers=params.data_parallelism, + test_patients = dataloader.test_patient_ids(params.test_patients) + test_ds = dataloader.create_dataloader( + patient_ids=test_patients, + samples_per_patient=params.test_samples_per_patient, + shuffle=False, ) test_datasets.append(test_ds) # END FOR - ds_weights = np.array([d.weight for d in params.datasets]) - ds_weights = ds_weights / ds_weights.sum() + ds_weights = None + if params.dataset_weights: + ds_weights = np.array(params.dataset_weights) + ds_weights = ds_weights / ds_weights.sum() test_ds = tf.data.Dataset.sample_from_datasets(test_datasets, weights=ds_weights) - # END WITH + test_ds = create_data_pipeline( + ds=test_ds, + sampling_rate=params.sampling_rate, + batch_size=params.batch_size, + augmentations=params.augmentations + params.preprocesses, + num_classes=params.num_classes, + ) + + if params.test_size: + batch_size = getattr(params, "batch_size", 1) + test_ds = test_ds.take(params.test_size // batch_size).cache() + return test_ds diff --git a/heartkit/tasks/segmentation/demo.py b/heartkit/tasks/segmentation/demo.py index 41c752ab..8e4be0ba 100644 --- a/heartkit/tasks/segmentation/demo.py +++ b/heartkit/tasks/segmentation/demo.py @@ -5,23 +5,21 @@ import plotly.graph_objects as go from plotly.subplots import make_subplots from tqdm import tqdm +import neuralspot_edge as nse -from ...datasets.utils import uniform_id_generator -from ...defines import HKDemoParams +from ...defines import HKTaskParams from ...rpc import BackendFactory -from ...utils import setup_logger -from ..utils import load_datasets -from .datasets import augment, preprocess +from ...datasets import DatasetFactory, create_augmentation_pipeline from .defines import HKSegment -def demo(params: HKDemoParams): +def demo(params: HKTaskParams): """Run segmentation demo. Args: - params (HKDemoParams): Demo parameters + params (HKTaskParams): Demo parameters """ - logger = setup_logger(__name__, level=params.verbose) + logger = nse.utils.setup_logger(__name__, level=params.verbose) bg_color = "rgba(38,42,50,1.0)" primary_color = "#11acd5" @@ -35,30 +33,30 @@ def demo(params: HKDemoParams): params.demo_size = params.demo_size or params.frame_size # Load backend inference engine - runner = BackendFactory.create(params.backend, params=params) + runner = BackendFactory.get(params.backend)(params) - classes = sorted(list(set(params.class_map.values()))) + classes = sorted(set(params.class_map.values())) class_names = params.class_names or [f"Class {i}" for i in range(params.num_classes)] feat_shape = (params.frame_size, 1) class_shape = (params.frame_size, params.num_classes) - # ds_spec = ( - # tf.TensorSpec(shape=feat_shape, dtype=tf.float32), - # tf.TensorSpec(shape=class_shape, dtype=tf.int32), - # ) - - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(cacheable=False, **ds.params) for ds in params.datasets] ds = random.choice(datasets) ds_gen = ds.signal_generator( - patient_generator=uniform_id_generator(ds.get_test_patient_ids(), repeat=False), + patient_generator=nse.utils.uniform_id_generator(ds.get_test_patient_ids(), repeat=False), frame_size=params.demo_size, samples_per_patient=5, target_rate=params.sampling_rate, ) x = next(ds_gen) - # Run inference + + augmenter = create_augmentation_pipeline( + augmentations=params.augmentations + params.preprocesses, + sampling_rate=params.sampling_rate, + ) + runner.open() logger.debug("Running inference") y_pred = np.zeros(x.size, dtype=np.int32) @@ -69,11 +67,11 @@ def demo(params: HKDemoParams): start, stop = i, i + params.frame_size xx = x[start:stop] yy = np.zeros(shape=class_shape, dtype=np.int32) - xx = augment(x=xx, augmentations=params.augmentations, sample_rate=params.sampling_rate) - xx = preprocess(xx, sample_rate=params.sampling_rate, preprocesses=params.preprocesses) xx = xx.reshape(feat_shape) + xx = augmenter(xx, training=True) runner.set_inputs(xx) runner.perform_inference() + x[start:stop] = xx.numpy().squeeze() yy = runner.get_outputs() y_pred[start:stop] = np.argmax(yy, axis=-1).flatten() # END FOR diff --git a/heartkit/tasks/segmentation/evaluate.py b/heartkit/tasks/segmentation/evaluate.py index 08d07231..6ae1ea63 100644 --- a/heartkit/tasks/segmentation/evaluate.py +++ b/heartkit/tasks/segmentation/evaluate.py @@ -2,25 +2,22 @@ import os import numpy as np -import tensorflow as tf - import neuralspot_edge as nse -from ...defines import HKTestParams -from ...metrics import compute_iou -from ...utils import set_random_seed, setup_logger -from ..utils import load_datasets + +from ...defines import HKTaskParams +from ...datasets import DatasetFactory from .datasets import load_test_dataset -def evaluate(params: HKTestParams): +def evaluate(params: HKTaskParams): """Evaluate model Args: - params (HKTestParams): Evaluation parameters + params (HKTaskParams): Evaluation parameters """ - logger = setup_logger(__name__, level=params.verbose) + logger = nse.utils.setup_logger(__name__, level=params.verbose) - params.seed = set_random_seed(params.seed) + params.seed = nse.utils.set_random_seed(params.seed) logger.debug(f"Random seed {params.seed}") os.makedirs(params.job_dir, exist_ok=True) @@ -32,18 +29,10 @@ def evaluate(params: HKTestParams): class_names = params.class_names or [f"Class {i}" for i in range(params.num_classes)] - feat_shape = (params.frame_size, 1) - class_shape = (params.frame_size, params.num_classes) - - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype="float32"), - tf.TensorSpec(shape=class_shape, dtype="int32"), - ) - - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - test_ds = load_test_dataset(datasets=datasets, params=params, ds_spec=ds_spec) - test_x, test_y = next(test_ds.batch(params.test_size).as_numpy_iterator()) + test_ds = load_test_dataset(datasets=datasets, params=params) + test_y = np.concatenate([y for _, y in test_ds.as_numpy_iterator()]) logger.debug("Loading model") model = nse.models.load_model(params.model_file) @@ -53,19 +42,18 @@ def evaluate(params: HKTestParams): logger.debug(f"Model requires {flops/1e6:0.2f} MFLOPS") logger.debug("Performing inference") - y_true = np.argmax(test_y, axis=-1) - y_pred = np.argmax(model.predict(test_x), axis=-1) + rst = model.evaluate(test_ds, verbose=params.verbose, return_dict=True) + logger.info("[TEST SET] " + ", ".join([f"{k.upper()}={v:.2%}" for k, v in rst.items()])) - # Summarize results - logger.info("Testing Results") - test_acc = np.sum(y_pred == y_true) / y_true.size - test_iou = compute_iou(y_true, y_pred, average="weighted") - logger.info(f"[TEST SET] ACC={test_acc:.2%}, IoU={test_iou:.2%}") + # Get predictions to compute CM + y_true = np.argmax(test_y, axis=-1) + y_pred = np.argmax(model.predict(test_ds), axis=-1) y_true = y_true.flatten() y_pred = y_pred.flatten() + cm_path = params.job_dir / "confusion_matrix_test.png" - nse.plotting.cm.confusion_matrix_plot(y_true, y_pred, labels=class_names, save_path=cm_path, normalize="true") - nse.plotting.cm.px_plot_confusion_matrix( + nse.plotting.confusion_matrix_plot(y_true, y_pred, labels=class_names, save_path=cm_path, normalize="true") + nse.plotting.px_plot_confusion_matrix( y_true, y_pred, labels=class_names, diff --git a/heartkit/tasks/segmentation/export.py b/heartkit/tasks/segmentation/export.py index 207a8f19..38a49ce7 100644 --- a/heartkit/tasks/segmentation/export.py +++ b/heartkit/tasks/segmentation/export.py @@ -1,69 +1,52 @@ -import logging import os import shutil import keras import numpy as np -import tensorflow as tf - import neuralspot_edge as nse -from ...defines import HKExportParams -from ...metrics import compute_iou -from ...utils import setup_logger -from ..utils import load_datasets + +from ...defines import HKTaskParams +from ...datasets import DatasetFactory from .datasets import load_test_dataset -def export(params: HKExportParams): +def export(params: HKTaskParams): """Export model Args: - params (HKExportParams): Deployment parameters + params (HKTaskParams): Deployment parameters """ - logger = setup_logger(__name__, level=params.verbose) - os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "export.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "export.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) - tfl_model_path = params.job_dir / "model.tflite" tflm_model_path = params.job_dir / "model_buffer.h" - feat_shape = (params.frame_size, 1) - class_shape = (params.frame_size, params.num_classes) + classes = sorted(set(params.class_map.values())) - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype=tf.float32), - tf.TensorSpec(shape=class_shape, dtype=tf.int32), - ) + feat_shape = (params.frame_size, 1) - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - test_ds = load_test_dataset(datasets=datasets, params=params, ds_spec=ds_spec) - test_x, test_y = next(test_ds.batch(params.test_size).as_numpy_iterator()) + test_ds = load_test_dataset(datasets=datasets, params=params) + test_x, test_y = [], [] + for x, y in test_ds.as_numpy_iterator(): + test_x.append(x) + test_y.append(y) + test_x = np.concatenate(test_x) + test_y = np.concatenate(test_y) # Load model and set fixed batch size of 1 logger.debug("Loading trained model") model = nse.models.load_model(params.model_file) + # Add softmax layer if required if not params.use_logits and not isinstance(model.layers[-1], keras.layers.Softmax): - last_layer_name = model.layers[-1].name - - def call_function(layer, *args, **kwargs): - out = layer(*args, **kwargs) - if layer.name == last_layer_name: - out = keras.layers.Softmax()(out) - return out - - # END DEF - model_clone = keras.models.clone_model(model, call_function=call_function) - model_clone.set_weights(model.get_weights()) - model = model_clone + model = nse.models.append_layers(model, layers=[keras.layers.Softmax()], copy_weights=True) # END IF - inputs = keras.Input(shape=ds_spec[0].shape, batch_size=1, name="input", dtype=ds_spec[0].dtype.name) + + inputs = keras.Input(feat_shape, batch_size=1, name="input", dtype="float32") model(inputs) flops = nse.metrics.flops.get_flops(model, batch_size=1, fpath=params.job_dir / "model_flops.log") @@ -97,26 +80,38 @@ def call_function(layer, *args, **kwargs): tflite = nse.interpreters.tflite.TfLiteKerasInterpreter(tflite_content) tflite.compile() - # Verify TFLite results match TF results - logger.info("Validating model results") - y_true = np.argmax(test_y, axis=-1) - y_pred_tf = np.argmax(model.predict(test_x), axis=-1) - y_pred_tfl = np.argmax(tflite.predict(x=test_x), axis=-1) - - tf_acc = np.sum(y_true == y_pred_tf) / y_true.size - tf_iou = compute_iou(y_true, y_pred_tf, average="weighted") - logger.info(f"[TF SET] ACC={tf_acc:.2%}, IoU={tf_iou:.2%}") - - tfl_acc = np.sum(y_true == y_pred_tfl) / y_true.size - tfl_iou = compute_iou(y_true, y_pred_tfl, average="weighted") - logger.info(f"[TFL SET] ACC={tfl_acc:.2%}, IoU={tfl_iou:.2%}") + # Verify TFLite results match TF results on example data + metrics = [ + keras.metrics.CategoricalCrossentropy(name="loss", from_logits=params.use_logits), + keras.metrics.CategoricalAccuracy(name="acc"), + nse.metrics.MultiF1Score(name="f1", average="weighted"), + keras.metrics.OneHotIoU( + num_classes=params.num_classes, + target_class_ids=classes, + name="iou", + ), + ] + + if params.val_metric not in [m.name for m in metrics]: + raise ValueError(f"Metric {params.val_metric} not supported") + + logger.debug("Validating model results") + y_true = test_y + y_pred_tf = model.predict(test_x) + y_pred_tfl = tflite.predict(x=test_x) + + tf_rst = nse.metrics.compute_metrics(metrics, y_true, y_pred_tf) + tfl_rst = nse.metrics.compute_metrics(metrics, y_true, y_pred_tfl) + logger.info("[TF METRICS] " + " ".join([f"{k.upper()}={v:.2%}" for k, v in tf_rst.items()])) + logger.info("[TFL METRICS] " + " ".join([f"{k.upper()}={v:.2%}" for k, v in tfl_rst.items()])) + + metric_diff = abs(tf_rst[params.val_metric] - tfl_rst[params.val_metric]) # Check accuracy hit - tfl_acc_drop = max(0, tf_acc - tfl_acc) - if params.val_acc_threshold is not None and (1 - tfl_acc_drop) < params.val_acc_threshold: - logger.warning(f"TFLite accuracy dropped by {tfl_acc_drop:0.2%}") - elif params.val_acc_threshold: - logger.debug(f"Validation passed ({tfl_acc_drop:0.2%})") + if params.val_metric_threshold is not None and metric_diff > params.val_metric_threshold: + logger.warning(f"TFLite accuracy dropped by {metric_diff:0.2%}") + elif params.val_metric_threshold: + logger.info(f"Validation passed ({metric_diff:0.2%})") if params.tflm_file and tflm_model_path != params.tflm_file: logger.debug(f"Copying TFLM header to {params.tflm_file}") diff --git a/heartkit/tasks/segmentation/metrics.py b/heartkit/tasks/segmentation/metrics.py deleted file mode 100644 index a80f2660..00000000 --- a/heartkit/tasks/segmentation/metrics.py +++ /dev/null @@ -1,45 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np -import numpy.typing as npt - -from .defines import HKSegment - - -def plot_segmentations( - data: npt.NDArray, - seg_mask: npt.NDArray | None = None, - fig: plt.Figure | None = None, - ax: plt.Axes | None = None, -) -> tuple[plt.Figure, plt.Axes]: - """Generate line plot of ECG data with lines colored based on segmentation mask - - Args: - data (npt.NDArray): ECG data - seg_mask (npt.NDArray | None, optional): Segmentation mask. Defaults to None. - fig (plt.Figure | None, optional): Existing figure handle. Defaults to None. - ax (plt.Axes | None, optional): Existing axes handle. Defaults to None. - - Returns: - tuple[plt.Figure, plt.Axes]: Figure and axes handle - """ - color_map = { - HKSegment.normal: "lightgray", - HKSegment.pwave: "blue", - HKSegment.qrs: "orange", - HKSegment.twave: "green", - } - t = np.arange(0, data.shape[0]) - if fig is None or ax is None: - fig, ax = plt.subplots(figsize=(10, 4), layout="constrained") - ax.plot(t, data, color="lightgray") - if seg_mask is not None: - pred_bnds = np.where(np.abs(np.diff(seg_mask)) > 0)[0] - pred_bnds = np.concatenate(([0], pred_bnds, [len(seg_mask) - 1])) - for i in range(pred_bnds.shape[0] - 1): - c = color_map.get(seg_mask[pred_bnds[i] + 1], "black") - ax.plot( - t[pred_bnds[i] : pred_bnds[i + 1]], - data[pred_bnds[i] : pred_bnds[i + 1]], - color=c, - ) - return fig, ax diff --git a/heartkit/tasks/segmentation/train.py b/heartkit/tasks/segmentation/train.py index 098c4cb3..167a5a55 100644 --- a/heartkit/tasks/segmentation/train.py +++ b/heartkit/tasks/segmentation/train.py @@ -1,46 +1,36 @@ -import logging import os import keras import numpy as np import sklearn.utils -import tensorflow as tf import wandb from wandb.keras import WandbMetricsLogger, WandbModelCheckpoint -from sklearn.metrics import f1_score - import neuralspot_edge as nse -from ...defines import HKTrainParams -from ...metrics import compute_iou -from ...utils import env_flag, set_random_seed, setup_logger -from ..utils import load_datasets + +from ...defines import HKTaskParams +from ...datasets import DatasetFactory from .datasets import load_train_datasets -from .utils import create_model +from ...models import ModelFactory -def train(params: HKTrainParams): +def train(params: HKTaskParams): """Train model Args: - params (HKTrainParams): Training parameters + params (HKTaskParams): Training parameters """ - logger = setup_logger(__name__, level=params.verbose) - - params.finetune = bool(getattr(params, "finetune", False)) - params.seed = set_random_seed(params.seed) - logger.debug(f"Random seed {params.seed}") - os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "train.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "train.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) + params.finetune = bool(getattr(params, "finetune", False)) + params.seed = nse.utils.set_random_seed(params.seed) + logger.debug(f"Random seed {params.seed}") with open(params.job_dir / "train_config.json", "w", encoding="utf-8") as fp: fp.write(params.model_dump_json(indent=2)) - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): wandb.init( project=f"hk-segmentation-{params.num_classes}", entity="ambiq", @@ -49,96 +39,61 @@ def train(params: HKTrainParams): wandb.config.update(params.model_dump()) # END IF - classes = sorted(list(set(params.class_map.values()))) + classes = sorted(set(params.class_map.values())) class_names = params.class_names or [f"Class {i}" for i in range(params.num_classes)] feat_shape = (params.frame_size, 1) - class_shape = (params.frame_size, params.num_classes) - - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype=tf.float32), - tf.TensorSpec(shape=class_shape, dtype=tf.int32), - ) - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] train_ds, val_ds = load_train_datasets( datasets=datasets, params=params, - ds_spec=ds_spec, ) - test_labels = [label.numpy() for _, label in val_ds] - # Where test_labels is all zeros, we assume it is a dummy label and should be ignored - y_mask = np.any(test_labels, axis=-1).flatten() - y_true = np.argmax(np.concatenate(test_labels).squeeze(), axis=-1).flatten() + y_true = np.concatenate([xy[1] for xy in val_ds.as_numpy_iterator()]) + y_true = np.argmax(y_true, axis=-1).flatten() class_weights = 0.25 if params.class_weights == "balanced": class_weights = sklearn.utils.compute_class_weight("balanced", classes=np.array(classes), y=y_true) class_weights = (class_weights + class_weights.mean()) / 2 # Smooth out + class_weights = class_weights.tolist() # END IF logger.debug(f"Class weights: {class_weights}") - inputs = keras.Input( - shape=ds_spec[0].shape, - batch_size=None, - name="input", - dtype=ds_spec[0].dtype.name, - ) + inputs = keras.Input(shape=feat_shape, name="input", dtype="float32") + if params.resume and params.model_file: logger.debug(f"Loading model from file {params.model_file}") model = nse.models.load_model(params.model_file) params.model_file = None else: logger.debug("Creating model from scratch") - model = create_model( - inputs, + model = ModelFactory.get(params.architecture.name)( + x=inputs, + params=params.architecture.params, num_classes=params.num_classes, - architecture=params.architecture, ) # END IF - # If fine-tune, freeze model encoder weights - if params.finetune: - for layer in model.layers: - if layer.name.startswith("ENC"): - logger.debug(f"Freezing {layer.name}") - layer.trainable = False - # END IF - # END FOR - # END IF - flops = nse.metrics.flops.get_flops(model, batch_size=1, fpath=params.job_dir / "model_flops.log") - if params.lr_cycles > 1: - scheduler = keras.optimizers.schedules.CosineDecayRestarts( - initial_learning_rate=params.lr_rate, - first_decay_steps=int(0.1 * params.steps_per_epoch * params.epochs), - t_mul=1.65 / (0.1 * params.lr_cycles * (params.lr_cycles - 1)), - m_mul=0.4, - ) - else: - scheduler = keras.optimizers.schedules.CosineDecay( - initial_learning_rate=params.lr_rate, - decay_steps=params.steps_per_epoch * params.epochs, - ) - # END IF + t_mul = 1 + first_steps = (params.steps_per_epoch * params.epochs) / (np.power(params.lr_cycles, t_mul) - t_mul + 1) + scheduler = keras.optimizers.schedules.CosineDecayRestarts( + initial_learning_rate=params.lr_rate, + first_decay_steps=np.ceil(first_steps), + t_mul=t_mul, + m_mul=0.5, + ) optimizer = keras.optimizers.Adam(scheduler) loss = keras.losses.CategoricalFocalCrossentropy( from_logits=True, alpha=class_weights, ) - metrics = [ - keras.metrics.CategoricalAccuracy(name="acc"), - # tfa.MultiF1Score(name="f1", average="weighted"), - keras.metrics.OneHotIoU( - num_classes=params.num_classes, - target_class_ids=classes, - name="iou", - ), - ] + metrics = [keras.metrics.CategoricalAccuracy(name="acc"), nse.metrics.MultiF1Score(name="f1", average="weighted")] if params.resume and params.weights_file: logger.debug(f"Hydrating model weights from file {params.weights_file}") @@ -153,7 +108,7 @@ def train(params: HKTrainParams): logger.debug(f"Model requires {flops/1e6:0.2f} MFLOPS") ModelCheckpoint = keras.callbacks.ModelCheckpoint - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): ModelCheckpoint = WandbModelCheckpoint model_callbacks = [ keras.callbacks.EarlyStopping( @@ -173,14 +128,14 @@ def train(params: HKTrainParams): ), keras.callbacks.CSVLogger(params.job_dir / "history.csv"), ] - if env_flag("TENSORBOARD"): + if nse.utils.env_flag("TENSORBOARD"): model_callbacks.append( keras.callbacks.TensorBoard( log_dir=params.job_dir, write_steps_per_second=True, ) ) - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): model_callbacks.append(WandbMetricsLogger()) try: @@ -198,23 +153,18 @@ def train(params: HKTrainParams): logger.debug(f"Model saved to {params.model_file}") # Get full validation results - keras.models.load_model(params.model_file) logger.debug("Performing full validation") - y_pred = np.argmax(model.predict(val_ds), axis=-1).flatten() - - # Keep only valid labels - y_true = y_true[y_mask] - y_pred = y_pred[y_mask] + y_pred = model.predict(val_ds) + y_pred = np.argmax(y_pred, axis=-1).flatten() cm_path = params.job_dir / "confusion_matrix.png" - nse.plotting.cm.confusion_matrix_plot(y_true, y_pred, labels=class_names, save_path=cm_path, normalize="true") - if env_flag("WANDB"): + nse.plotting.confusion_matrix_plot(y_true, y_pred, labels=class_names, save_path=cm_path, normalize="true") + if nse.utils.env_flag("WANDB"): conf_mat = wandb.plot.confusion_matrix(preds=y_pred, y_true=y_true, class_names=class_names) wandb.log({"conf_mat": conf_mat}) # END IF # Summarize results - test_acc = np.sum(y_pred == y_true) / y_true.size - test_f1 = f1_score(y_true=y_true, y_pred=y_pred, average="weighted") - test_iou = compute_iou(y_true, y_pred, average="weighted") - logger.info(f"[TEST SET] ACC={test_acc:.2%}, F1={test_f1:.2%} IoU={test_iou:0.2%}") + rst = model.evaluate(val_ds, verbose=params.verbose, return_dict=True) + msg = "[VAL SET] " + ", ".join([f"{k.upper()}={v:.2%}" for k, v in rst.items()]) + logger.info(msg) diff --git a/heartkit/tasks/segmentation/utils.py b/heartkit/tasks/segmentation/utils.py deleted file mode 100644 index b0f2ee40..00000000 --- a/heartkit/tasks/segmentation/utils.py +++ /dev/null @@ -1,60 +0,0 @@ -import keras -from neuralspot_edge.models.unet import UNet, UNetBlockParams, UNetParams -from rich.console import Console - -from ...defines import ModelArchitecture -from ...models import ModelFactory - -console = Console() - - -def create_model(inputs: keras.KerasTensor, num_classes: int, architecture: ModelArchitecture | None) -> keras.Model: - """Generate model or use default - - Args: - inputs (keras.KerasTensor): Model inputs - num_classes (int): Number of classes - architecture (ModelArchitecture|None): Model - - Returns: - keras.Model: Model - """ - if architecture: - return ModelFactory.get(architecture.name)( - x=inputs, - params=architecture.params, - num_classes=num_classes, - ) - - return default_model(inputs=inputs, num_classes=num_classes) - - -def default_model( - inputs: keras.KerasTensor, - num_classes: int, -) -> keras.Model: - """Reference model - - Args: - inputs (keras.KerasTensor): Model inputs - num_classes (int): Number of classes - - Returns: - keras.Model: Model - """ - blocks = [ - UNetBlockParams(filters=8, depth=2, ddepth=1, kernel=(1, 3), strides=(1, 2), skip=True), - UNetBlockParams(filters=16, depth=2, ddepth=1, kernel=(1, 3), strides=(1, 2), skip=True), - UNetBlockParams(filters=24, depth=2, ddepth=1, kernel=(1, 3), strides=(1, 2), skip=True), - UNetBlockParams(filters=32, depth=2, ddepth=1, kernel=(1, 3), strides=(1, 2), skip=True), - UNetBlockParams(filters=40, depth=2, ddepth=1, kernel=(1, 3), strides=(1, 2), skip=True), - ] - return UNet( - inputs, - params=UNetParams( - blocks=blocks, - output_kernel_size=(1, 3), - include_top=True, - ), - num_classes=num_classes, - ) diff --git a/heartkit/tasks/task.py b/heartkit/tasks/task.py index fe87e634..c45feae7 100644 --- a/heartkit/tasks/task.py +++ b/heartkit/tasks/task.py @@ -1,6 +1,6 @@ import abc -from ..defines import HKDemoParams, HKExportParams, HKTestParams, HKTrainParams +from ..defines import HKTaskParams class HKTask(abc.ABC): @@ -17,41 +17,41 @@ def description() -> str: return "" @staticmethod - def train(params: HKTrainParams) -> None: + def train(params: HKTaskParams) -> None: """Train a model Args: - params (HKTrainParams): train parameters + params (HKTaskParams): train parameters """ raise NotImplementedError @staticmethod - def evaluate(params: HKTestParams) -> None: + def evaluate(params: HKTaskParams) -> None: """Evaluate a model Args: - params (HKTestParams): test parameters + params (HKTaskParams): test parameters """ raise NotImplementedError @staticmethod - def export(params: HKExportParams) -> None: + def export(params: HKTaskParams) -> None: """Export a model Args: - params (HKExportParams): export parameters + params (HKTaskParams): export parameters """ raise NotImplementedError @staticmethod - def demo(params: HKDemoParams) -> None: + def demo(params: HKTaskParams) -> None: """Run a demo Args: - params (HKDemoParams): demo parameters + params (HKTaskParams): demo parameters """ raise NotImplementedError diff --git a/heartkit/tasks/translate/__init__.py b/heartkit/tasks/translate/__init__.py index 4543e708..7ac6f120 100644 --- a/heartkit/tasks/translate/__init__.py +++ b/heartkit/tasks/translate/__init__.py @@ -1,4 +1,4 @@ -from ...defines import HKDemoParams, HKExportParams, HKTestParams, HKTrainParams +from ...defines import HKTaskParams from ..task import HKTask from .defines import HKTranslate from .demo import demo @@ -11,17 +11,17 @@ class TranslateTask(HKTask): """HeartKit Translate Task""" @staticmethod - def train(params: HKTrainParams): + def train(params: HKTaskParams): train(params) @staticmethod - def evaluate(params: HKTestParams): + def evaluate(params: HKTaskParams): evaluate(params) @staticmethod - def export(params: HKExportParams): + def export(params: HKTaskParams): export(params) @staticmethod - def demo(params: HKDemoParams): + def demo(params: HKTaskParams): demo(params) diff --git a/heartkit/tasks/translate/dataloaders/__init__.py b/heartkit/tasks/translate/dataloaders/__init__.py index 968977f3..efc56a4e 100644 --- a/heartkit/tasks/translate/dataloaders/__init__.py +++ b/heartkit/tasks/translate/dataloaders/__init__.py @@ -1 +1,8 @@ -from .bidmc import bidmc_data_generator +import neuralspot_edge as nse + +from ....datasets import HKDataloader + +from .bidmc import BidmcDataloader + +TranslateTaskFactory = nse.utils.create_factory(factory="HKTranslateTaskFactory", type=HKDataloader) +TranslateTaskFactory.register("bidmc", BidmcDataloader) diff --git a/heartkit/tasks/translate/dataloaders/bidmc.py b/heartkit/tasks/translate/dataloaders/bidmc.py index 0d6a26ac..bebf27fb 100644 --- a/heartkit/tasks/translate/dataloaders/bidmc.py +++ b/heartkit/tasks/translate/dataloaders/bidmc.py @@ -3,56 +3,68 @@ import numpy as np import numpy.typing as npt import physiokit as pk +import neuralspot_edge as nse -from ....datasets import BidmcDataset, PatientGenerator +from ....datasets import BidmcDataset, HKDataloader +from ..defines import HKTranslate -def bidmc_data_generator( - patient_generator: PatientGenerator, - ds: BidmcDataset, - frame_size: int, - samples_per_patient: int | list[int] = 1, - target_rate: int | None = None, -) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: - """Generate frames using patient generator. +BidmcTranslateMap = {0: HKTranslate.ecg, 1: HKTranslate.ppg} - Args: - patient_generator (PatientGenerator): Patient Generator - ds: BidmcDataset - frame_size (int): Frame size - samples_per_patient (int | list[int], optional): # samples per patient. Defaults to 1. - target_rate (int|None, optional): Target rate. Defaults to None. - Returns: - Generator[tuple[npt.NDArray, npt.NDArray], None, None]: Sample generator +class BidmcDataloader(HKDataloader): + """Dataloader for the BIDMC dataset""" - """ - if isinstance(samples_per_patient, Iterable): - samples_per_patient = samples_per_patient[0] + def __init__(self, ds: BidmcDataset, **kwargs): + super().__init__(ds=ds, **kwargs) + if self.label_map is None: + self.label_map = {HKTranslate.ppg: HKTranslate.ecg} + if len(self.label_map) != 1: + raise ValueError("Only one source and target signal is supported") + self.label_map = {k: self.label_map[v] for (k, v) in BidmcTranslateMap.items() if k in self.label_map} - for pt in patient_generator: - with ds.patient_data(pt) as h5: - ecg = h5["data"][0, :] - ppg = h5["data"][1, :] - # END WITH + def patient_data_generator( + self, + patient_id: int, + samples_per_patient: int, + ): + # Use class_map to determine source and target signals + src, tgt = list(self.label_map.keys())[0], list(self.label_map.values())[0] - # Use translation map to determine source and target signals - x = ppg - y = ecg + with self.ds.patient_data(patient_id) as h5: + x = h5["data"][src, :] + y = h5["data"][tgt, :] + # END WITH # Resample signals if necessary - if ds.sampling_rate != target_rate: - x = pk.signal.resample_signal(x, ds.sampling_rate, target_rate, axis=0) - y = pk.signal.resample_signal(y, ds.sampling_rate, target_rate, axis=0) + if self.ds.sampling_rate != self.sampling_rate: + x = pk.signal.resample_signal(x, self.ds.sampling_rate, self.sampling_rate, axis=0) + y = pk.signal.resample_signal(y, self.ds.sampling_rate, self.sampling_rate, axis=0) # END IF # Generate samples for _ in range(samples_per_patient): - start = np.random.randint(0, x.size - frame_size) - xx = x[start : start + frame_size] + start = np.random.randint(0, x.size - self.frame_size) + xx = x[start : start + self.frame_size] xx = np.nan_to_num(xx).astype(np.float32) - yy = y[start : start + frame_size] + yy = y[start : start + self.frame_size] yy = np.nan_to_num(yy).astype(np.float32) + xx = xx.reshape(-1, 1) + yy = yy.reshape(-1, 1) yield xx, yy # END FOR - # END FOR + + def data_generator( + self, + patient_ids: list[int], + samples_per_patient: int | list[int], + shuffle: bool = False, + ) -> Generator[tuple[npt.NDArray, npt.NDArray], None, None]: + if isinstance(samples_per_patient, Iterable): + samples_per_patient = samples_per_patient[0] + + for pt_id in nse.utils.uniform_id_generator(patient_ids, shuffle=shuffle): + for x, y in self.patient_data_generator(pt_id, samples_per_patient): + yield x, y + # END FOR + # END FOR diff --git a/heartkit/tasks/translate/datasets.py b/heartkit/tasks/translate/datasets.py index a4852884..81191e1f 100644 --- a/heartkit/tasks/translate/datasets.py +++ b/heartkit/tasks/translate/datasets.py @@ -1,313 +1,157 @@ -import functools -import logging -from pathlib import Path - import numpy as np -import numpy.typing as npt import tensorflow as tf +import neuralspot_edge as nse from ...datasets import ( HKDataset, - augment_pipeline, - preprocess_pipeline, - uniform_id_generator, -) -from ...datasets.dataloader import test_dataloader, train_val_dataloader -from ...defines import ( - AugmentationParams, - HKExportParams, - HKTestParams, - HKTrainParams, - PreprocessParams, + create_augmentation_pipeline, ) -from ...utils import resolve_template_path -from .dataloaders import bidmc_data_generator - -logger = logging.getLogger(__name__) - - -def preprocess(x: npt.NDArray, preprocesses: list[PreprocessParams], sample_rate: float) -> npt.NDArray: - """Preprocess data pipeline - - Args: - x (npt.NDArray): Input data - preprocesses (list[PreprocessParams]): Preprocess parameters - sample_rate (float): Sample rate - - Returns: - npt.NDArray: Preprocessed data - """ - return preprocess_pipeline(x, preprocesses=preprocesses, sample_rate=sample_rate) - - -def augment(x: npt.NDArray, augmentations: list[AugmentationParams], sample_rate: float) -> npt.NDArray: - """Augment data pipeline - - Args: - x (npt.NDArray): Input data - augmentations (list[AugmentationParams]): Augmentation parameters - sample_rate (float): Sample rate - - Returns: - npt.NDArray: Augmented data - """ - - return augment_pipeline(x=x, augmentations=augmentations, sample_rate=sample_rate) - - -def prepare( - x_y: tuple[npt.NDArray, npt.NDArray], - sample_rate: float, - preprocesses: list[PreprocessParams], - augmentations: list[AugmentationParams], - spec: tuple[tf.TensorSpec, tf.TensorSpec], - num_classes: int, -) -> tuple[npt.NDArray, npt.NDArray]: - """Prepare dataset - - Args: - x_y (tuple[npt.NDArray, npt.NDArray]): Input data - sample_rate (float): Sample rate - preprocesses (list[PreprocessParams]|None): Preprocess parameters - augmentations (list[AugmentationParams]|None): Augmentation parameters - spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - num_classes (int): Number of classes - - Returns: - tuple[npt.NDArray, npt.NDArray]: Prepared data - """ - x, y = x_y[0].copy(), x_y[1].copy() - - if augmentations: - x = augment(x, augmentations, sample_rate) - y = augment(y, augmentations, sample_rate) - # END IF - - if preprocesses: - x = preprocess(x, preprocesses, sample_rate) - y = preprocess(y, preprocesses, sample_rate) - # END IF +from ...datasets.dataloader import HKDataloader +from ...defines import HKTaskParams, NamedParams - x = x.reshape(spec[0].shape) - # y = y.reshape(spec[0].shape) - y = y.reshape(spec[1].shape) +from .dataloaders import TranslateTaskFactory - return x, y +logger = nse.utils.setup_logger(__name__) -def get_ds_label_map(ds: HKDataset, label_map: dict[int, int] | None = None) -> dict[int, int]: - """Get label map for dataset - - Args: - ds (HKDataset): Dataset - label_map (dict[int, int]|None): Label map - - Returns: - dict[int, int]: Label map - """ - return label_map - - -def get_data_generator(ds: HKDataset, frame_size: int, samples_per_patient: int, target_rate: int): - """Get task data generator for dataset - - Args: - ds (HKDataset): Dataset - frame_size (int): Frame size - samples_per_patient (int): Samples per patient - target_rate (int): Target rate - - Returns: - callable: Data generator - """ - match ds.name: - case "bidmc": - data_generator = bidmc_data_generator - case _: - raise ValueError(f"Dataset {ds.name} not supported") - # END MATCH - return functools.partial( - data_generator, - ds=ds, - frame_size=frame_size, - samples_per_patient=samples_per_patient, - target_rate=target_rate, +def create_data_pipeline( + ds: tf.data.Dataset, + sampling_rate: int, + batch_size: int, + buffer_size: int | None = None, + augmentations: list[NamedParams] | None = None, +): + if buffer_size: + ds = ds.shuffle( + buffer_size=buffer_size, + reshuffle_each_iteration=True, + ) + if batch_size: + ds = ds.batch( + batch_size=batch_size, + drop_remainder=True, + num_parallel_calls=tf.data.AUTOTUNE, + ) + augmenter = create_augmentation_pipeline(augmentations, sampling_rate=sampling_rate) + ds = ( + ds.map( + lambda data, labels: { + "data": tf.cast(data, "float32"), + "labels": tf.cast(labels, "float32"), + }, + num_parallel_calls=tf.data.AUTOTUNE, + ) + .map( + augmenter, + num_parallel_calls=tf.data.AUTOTUNE, + ) + .map( + lambda data: (data["data"], data["labels"]), + num_parallel_calls=tf.data.AUTOTUNE, + ) ) - -def resolve_ds_cache_path(fpath: Path | None, ds: HKDataset, task: str, frame_size: int, sample_rate: int): - """Resolve dataset cache path - - Args: - fpath (Path|None): File path - ds (HKDataset): Dataset - task (str): Task - frame_size (int): Frame size - sample_rate (int): Sampling rate - - Returns: - Path|None: Resolved path - """ - if not fpath: - return None - return resolve_template_path( - fpath=fpath, - dataset=ds.name, - task=task, - frame_size=frame_size, - sampling_rate=sample_rate, - ) + return ds.prefetch(tf.data.AUTOTUNE) def load_train_datasets( datasets: list[HKDataset], - params: HKTrainParams, - ds_spec: tuple[tf.TensorSpec, tf.TensorSpec], + params: HKTaskParams, ) -> tuple[tf.data.Dataset, tf.data.Dataset]: - """Load training and validation datasets - - Args: - datasets (list[HKDataset]): Datasets - params (HKTrainParams): Training parameters - ds_spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - - Returns: - tuple[tf.data.Dataset, tf.data.Dataset]: Train and validation datasets - """ - id_generator = functools.partial(uniform_id_generator, repeat=True) - train_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, - ) - train_datasets = [] val_datasets = [] for ds in datasets: - val_file = resolve_ds_cache_path( - params.val_file, - ds=ds, - task="denoise", - frame_size=params.frame_size, - sample_rate=params.sampling_rate, - ) - data_generator = get_data_generator( + dataloader: HKDataloader = TranslateTaskFactory.get(ds.name)( ds=ds, frame_size=params.frame_size, - samples_per_patient=params.samples_per_patient, - target_rate=params.sampling_rate, + sampling_rate=params.sampling_rate, + label_map=params.class_map, ) - - train_ds, val_ds = train_val_dataloader( - ds=ds, - spec=ds_spec, - data_generator=data_generator, - id_generator=id_generator, + train_patients, val_patients = dataloader.split_train_val_patients( train_patients=params.train_patients, val_patients=params.val_patients, - val_pt_samples=params.val_samples_per_patient, - val_file=val_file, - val_size=params.val_size, - label_map=None, - label_type=None, - preprocess=train_prepare, - num_workers=params.data_parallelism, + ) + + train_ds = dataloader.create_dataloader( + patient_ids=train_patients, samples_per_patient=params.samples_per_patient, shuffle=True + ) + + val_ds = dataloader.create_dataloader( + patient_ids=val_patients, samples_per_patient=params.val_samples_per_patient, shuffle=False ) train_datasets.append(train_ds) val_datasets.append(val_ds) # END FOR - ds_weights = np.array([d.weight for d in params.datasets]) - ds_weights = ds_weights / ds_weights.sum() + ds_weights = None + if params.dataset_weights: + ds_weights = np.array(params.dataset_weights) + ds_weights = ds_weights / ds_weights.sum() train_ds = tf.data.Dataset.sample_from_datasets(train_datasets, weights=ds_weights) val_ds = tf.data.Dataset.sample_from_datasets(val_datasets, weights=ds_weights) # Shuffle and batch datasets for training - train_ds = ( - train_ds.shuffle( - buffer_size=params.buffer_size, - reshuffle_each_iteration=True, - ) - .batch( - batch_size=params.batch_size, - drop_remainder=False, - num_parallel_calls=tf.data.AUTOTUNE, - ) - .prefetch(buffer_size=tf.data.AUTOTUNE) + train_ds = create_data_pipeline( + ds=train_ds, + sampling_rate=params.sampling_rate, + batch_size=params.batch_size, + buffer_size=params.buffer_size, + augmentations=params.augmentations + params.preprocesses, ) - val_ds = val_ds.batch( + + val_ds = create_data_pipeline( + ds=val_ds, + sampling_rate=params.sampling_rate, batch_size=params.batch_size, - drop_remainder=True, - num_parallel_calls=tf.data.AUTOTUNE, + augmentations=params.preprocesses, ) + + # If given fixed val size or steps, then capture and cache + val_steps_per_epoch = params.val_size // params.batch_size if params.val_size else params.val_steps_per_epoch + if val_steps_per_epoch: + logger.info(f"Validation steps per epoch: {val_steps_per_epoch}") + val_ds = val_ds.take(val_steps_per_epoch).cache() + return train_ds, val_ds def load_test_dataset( datasets: list[HKDataset], - params: HKTestParams | HKExportParams, - ds_spec: tuple[tf.TensorSpec, tf.TensorSpec], + params: HKTaskParams, ) -> tf.data.Dataset: - """Load test dataset - - Args: - datasets (list[HKDataset]): Datasets - params (HKTestParams|HKExportParams): Test parameters - ds_spec (tuple[tf.TensorSpec, tf.TensorSpec]): TensorSpec - - Returns: - tf.data.Dataset: Test dataset - """ - - id_generator = functools.partial(uniform_id_generator, repeat=True) - test_prepare = functools.partial( - prepare, - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, - ) - test_datasets = [] for ds in datasets: - test_file = resolve_ds_cache_path( - fpath=params.test_file, + dataloader: HKDataloader = TranslateTaskFactory.get(ds.name)( ds=ds, - task="translate", frame_size=params.frame_size, - sample_rate=params.sampling_rate, + sampling_rate=params.sampling_rate, + label_map=params.class_map, ) - data_generator = get_data_generator( - ds=ds, - frame_size=params.frame_size, + test_patients = dataloader.test_patient_ids(params.test_patients) + test_ds = dataloader.create_dataloader( + patient_ids=test_patients, samples_per_patient=params.test_samples_per_patient, - target_rate=params.sampling_rate, - ) - - test_ds = test_dataloader( - ds=ds, - spec=ds_spec, - data_generator=data_generator, - id_generator=id_generator, - test_patients=params.test_patients, - test_file=test_file, - label_map=None, - label_type=None, - preprocess=test_prepare, - num_workers=params.data_parallelism, + shuffle=False, ) test_datasets.append(test_ds) # END FOR - ds_weights = np.array([d.weight for d in params.datasets]) - ds_weights = ds_weights / ds_weights.sum() + ds_weights = None + if params.dataset_weights: + ds_weights = np.array(params.dataset_weights) + ds_weights = ds_weights / ds_weights.sum() test_ds = tf.data.Dataset.sample_from_datasets(test_datasets, weights=ds_weights) + test_ds = create_data_pipeline( + ds=test_ds, + sampling_rate=params.sampling_rate, + batch_size=params.batch_size, + augmentations=params.preprocesses, + ) + + if params.test_size: + batch_size = getattr(params, "batch_size", 1) + test_ds = test_ds.take(params.test_size // batch_size).cache() - # END WITH return test_ds diff --git a/heartkit/tasks/translate/demo.py b/heartkit/tasks/translate/demo.py index 4ff6c190..d62ecfbe 100644 --- a/heartkit/tasks/translate/demo.py +++ b/heartkit/tasks/translate/demo.py @@ -2,25 +2,22 @@ import numpy as np import plotly.graph_objects as go -import tensorflow as tf from plotly.subplots import make_subplots from tqdm import tqdm +import neuralspot_edge as nse -from ...datasets.utils import uniform_id_generator -from ...defines import HKDemoParams +from ...defines import HKTaskParams from ...rpc import BackendFactory -from ...utils import setup_logger -from ..utils import load_datasets -from .datasets import get_data_generator, prepare +from ...datasets import DatasetFactory -logger = setup_logger(__name__) +logger = nse.utils.setup_logger(__name__) -def demo(params: HKDemoParams): +def demo(params: HKTaskParams): """Run task demo. Args: - params (HKDemoParams): Demo parameters + params (HKTaskParams): Demo parameters """ bg_color = "rgba(38,42,50,1.0)" primary_color = "#11acd5" @@ -32,36 +29,33 @@ def demo(params: HKDemoParams): params.demo_size = params.demo_size or 10 * params.sampling_rate # Load backend inference engine - runner = BackendFactory.create(params.backend, params=params) + runner = BackendFactory.get(params.backend)(params=params) - feat_shape = (params.demo_size, 1) - class_shape = (params.demo_size, 1) - - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype=tf.float32), - tf.TensorSpec(shape=class_shape, dtype=tf.float32), - ) + # feat_shape = (params.demo_size, 1) + # class_shape = (params.demo_size, 1) # Load data - dsets = load_datasets(datasets=params.datasets) - ds = random.choice(dsets) - - ds_gen = get_data_generator( - ds, frame_size=params.demo_size, samples_per_patient=5, target_rate=params.sampling_rate - ) - - ds_gen = ds_gen(patient_generator=uniform_id_generator(ds.get_test_patient_ids(), repeat=False)) - - x, y = next(ds_gen) - - x, y = prepare( - (x, y), - sample_rate=params.sampling_rate, - preprocesses=params.preprocesses, - augmentations=params.augmentations, - spec=ds_spec, - num_classes=params.num_classes, - ) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] + ds = random.choice(datasets) + print(ds) + + # ds_gen = get_data_generator( + # ds, frame_size=params.demo_size, samples_per_patient=5, target_rate=params.sampling_rate + # ) + + # ds_gen = ds_gen(patient_generator=nse.utils.uniform_id_generator(ds.get_test_patient_ids(), repeat=False)) + + # x, y = next(ds_gen) + + # x, y = prepare( + # (x, y), + # sample_rate=params.sampling_rate, + # preprocesses=params.preprocesses, + # augmentations=params.augmentations, + # spec=ds_spec, + # num_classes=params.num_classes, + # ) + x, y = None, None x = x.flatten() y = y.flatten() diff --git a/heartkit/tasks/translate/evaluate.py b/heartkit/tasks/translate/evaluate.py index 3fc57227..3ad470cd 100644 --- a/heartkit/tasks/translate/evaluate.py +++ b/heartkit/tasks/translate/evaluate.py @@ -1,27 +1,22 @@ import logging import os -import keras -import numpy as np -import tensorflow as tf - import neuralspot_edge as nse -from ...defines import HKTestParams -from ...utils import set_random_seed, setup_logger -from ..utils import load_datasets -from .datasets import load_test_dataset +from ...defines import HKTaskParams -logger = setup_logger(__name__) +from ...datasets import DatasetFactory +from .datasets import load_test_dataset -def evaluate(params: HKTestParams): +def evaluate(params: HKTaskParams): """Evaluate model Args: - params (HKTestParams): Evaluation parameters + params (HKTaskParams): Evaluation parameters """ + logger = nse.utils.setup_logger(__name__, level=params.verbose) - params.seed = set_random_seed(params.seed) + params.seed = nse.utils.set_random_seed(params.seed) logger.debug(f"Random seed {params.seed}") os.makedirs(params.job_dir, exist_ok=True) @@ -31,17 +26,9 @@ def evaluate(params: HKTestParams): handler.setLevel(logging.INFO) logger.addHandler(handler) - feat_shape = (params.frame_size, 1) - class_shape = (params.frame_size, 1) - - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype="float32"), - tf.TensorSpec(shape=class_shape, dtype="float32"), - ) - - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - test_ds = load_test_dataset(datasets=datasets, params=params, ds_spec=ds_spec) + test_ds = load_test_dataset(datasets=datasets, params=params) test_x, test_y = next(test_ds.batch(params.test_size).as_numpy_iterator()) logger.debug("Loading model") @@ -52,20 +39,10 @@ def evaluate(params: HKTestParams): logger.debug(f"Model requires {flops/1e6:0.2f} MFLOPS") logger.debug("Performing inference") - y_true = test_y.squeeze() - y_prob = model.predict(test_x) - y_pred = y_prob.squeeze() + # y_true = test_y.squeeze() + # y_prob = model.predict(test_x) + # y_pred = y_prob.squeeze() # Summarize results - cossim = keras.metrics.CosineSimilarity() - cossim.update_state(y_true, y_pred) # pylint: disable=E1102 - test_cossim = cossim.result().numpy() # pylint: disable=E1102 - logger.debug("Testing Results") - mae = keras.metrics.MeanAbsoluteError() - mae.update_state(y_true, y_pred) # pylint: disable=E1102 - test_mae = mae.result().numpy() # pylint: disable=E1102 - mse = keras.metrics.MeanSquaredError() - mse.update_state(y_true, y_pred) # pylint: disable=E1102 - test_mse = mse.result().numpy() # pylint: disable=E1102 - np.sqrt(np.mean(np.square(y_true - y_pred))) - logger.info(f"[TEST SET] MAE={test_mae:.2%}, MSE={test_mse:.2%}, COSSIM={test_cossim:.2%}") + metrics = model.evaluate(test_x, test_y, verbose=params.verbose, return_dict=True) + logger.info("[TEST SET] " + ", ".join([f"{k.upper()}={v:.2%}" for k, v in metrics.items()])) diff --git a/heartkit/tasks/translate/export.py b/heartkit/tasks/translate/export.py index 732c7839..6db2b7b8 100644 --- a/heartkit/tasks/translate/export.py +++ b/heartkit/tasks/translate/export.py @@ -1,56 +1,39 @@ -import logging import os import shutil import keras -import numpy as np -import tensorflow as tf - import neuralspot_edge as nse -from ...defines import HKExportParams -from ...utils import setup_logger -from ..utils import load_datasets -from .datasets import load_test_dataset -logger = setup_logger(__name__) +from ...defines import HKTaskParams +from ...datasets import DatasetFactory +from .datasets import load_test_dataset -def export(params: HKExportParams): +def export(params: HKTaskParams): """Export model Args: - params (HKExportParams): Deployment parameters + params (HKTaskParams): Deployment parameters """ - os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "export.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "export.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) - tfl_model_path = params.job_dir / "model.tflite" tflm_model_path = params.job_dir / "model_buffer.h" feat_shape = (params.frame_size, 1) - class_shape = (params.frame_size, 1) - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype=tf.float32), - tf.TensorSpec(shape=class_shape, dtype=tf.float32), - ) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] - datasets = load_datasets(datasets=params.datasets) - - test_ds = load_test_dataset(datasets=datasets, params=params, ds_spec=ds_spec) + test_ds = load_test_dataset(datasets=datasets, params=params) test_x, test_y = next(test_ds.batch(params.test_size).as_numpy_iterator()) # Load model and set fixed batch size of 1 logger.debug("Loading trained model") model = nse.models.load_model(params.model_file) - - inputs = keras.Input(shape=ds_spec[0].shape, batch_size=1, name="input", dtype=ds_spec[0].dtype) - model(inputs) # Build model with fixed batch size of 1 + inputs = keras.Input(shape=feat_shape, batch_size=1, name="input", dtype="float32") + model(inputs) flops = nse.metrics.flops.get_flops(model, batch_size=1, fpath=params.job_dir / "model_flops.log") model.summary(print_fn=logger.info) @@ -58,6 +41,7 @@ def export(params: HKExportParams): logger.debug(f"Converting model to TFLite (quantization={params.quantization.mode})") converter = nse.converters.tflite.TfLiteKerasConverter(model=model) + tflite_content = converter.convert( test_x=test_x, quantization=params.quantization.format, @@ -83,25 +67,32 @@ def export(params: HKExportParams): tflite.compile() # Verify TFLite results match TF results on example data - logger.debug("Validating model results") + metrics = [ + keras.metrics.MeanAbsoluteError(name="mae"), + keras.metrics.MeanSquaredError(name="mse"), + keras.metrics.RootMeanSquaredError(name="rmse"), + ] + + if params.val_metric not in [m.name for m in metrics]: + raise ValueError(f"Metric {params.val_metric} not supported") + + logger.info("Validating model results") y_true = test_y y_pred_tf = model.predict(test_x) y_pred_tfl = tflite.predict(x=test_x) - tf_mae = np.mean(np.abs(y_true - y_pred_tf)) - tf_rmse = np.sqrt(np.mean((y_true - y_pred_tf) ** 2)) - logger.debug(f"[TF SET] MAE={tf_mae:.2%}, RMSE={tf_rmse:.2%}") + tf_rst = nse.metrics.compute_metrics(metrics, y_true, y_pred_tf) + tfl_rst = nse.metrics.compute_metrics(metrics, y_true, y_pred_tfl) + logger.info("[TF METRICS] " + " ".join([f"{k.upper()}={v:.2%}" for k, v in tf_rst.items()])) + logger.info("[TFL METRICS] " + " ".join([f"{k.upper()}={v:.2%}" for k, v in tfl_rst.items()])) - tfl_mae = np.mean(np.abs(y_true - y_pred_tfl)) - tfl_rmse = np.sqrt(np.mean((y_true - y_pred_tfl) ** 2)) - logger.debug(f"[TFL SET] MAE={tfl_mae:.2%}, RMSE={tfl_rmse:.2%}") + metric_diff = abs(tf_rst[params.val_metric] - tfl_rst[params.val_metric]) # Check accuracy hit - tfl_acc_drop = max(0, tf_mae - tfl_mae) - if params.val_acc_threshold is not None and (1 - tfl_acc_drop) < params.val_acc_threshold: - logger.warning(f"TFLite accuracy dropped by {tfl_acc_drop:0.2%}") - elif params.val_acc_threshold: - logger.debug(f"Validation passed ({tfl_acc_drop:0.2%})") + if params.val_metric_threshold is not None and metric_diff > params.val_metric_threshold: + logger.warning(f"TFLite accuracy dropped by {metric_diff:0.2%}") + elif params.val_metric_threshold: + logger.info(f"Validation passed ({metric_diff:0.2%})") if params.tflm_file and tflm_model_path != params.tflm_file: logger.debug(f"Copying TFLM header to {params.tflm_file}") diff --git a/heartkit/tasks/translate/train.py b/heartkit/tasks/translate/train.py index 75b48753..e7f335f6 100644 --- a/heartkit/tasks/translate/train.py +++ b/heartkit/tasks/translate/train.py @@ -1,42 +1,34 @@ -import logging import os import keras -import tensorflow as tf +import neuralspot_edge as nse +import numpy as np import wandb from wandb.keras import WandbMetricsLogger, WandbModelCheckpoint -import neuralspot_edge as nse -from ...defines import HKTrainParams -from ...utils import env_flag, set_random_seed, setup_logger -from ..utils import load_datasets +from ...defines import HKTaskParams +from ...datasets import DatasetFactory +from ...models import ModelFactory from .datasets import load_train_datasets -from .utils import create_model -logger = setup_logger(__name__) - -def train(params: HKTrainParams): +def train(params: HKTaskParams): """Train model Args: - params (HKTrainParams): Training parameters + params (HKTaskParams): Training parameters """ - - params.seed = set_random_seed(params.seed) - logger.debug(f"Random seed {params.seed}") - os.makedirs(params.job_dir, exist_ok=True) + logger = nse.utils.setup_logger(__name__, level=params.verbose, file_path=params.job_dir / "train.log") logger.debug(f"Creating working directory in {params.job_dir}") - handler = logging.FileHandler(params.job_dir / "train.log", mode="w") - handler.setLevel(logging.INFO) - logger.addHandler(handler) + params.seed = nse.utils.set_random_seed(params.seed) + logger.debug(f"Random seed {params.seed}") with open(params.job_dir / "train_config.json", "w", encoding="utf-8") as fp: fp.write(params.model_dump_json(indent=2)) - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): wandb.init( project=params.project, entity="ambiq", @@ -46,54 +38,36 @@ def train(params: HKTrainParams): # END IF feat_shape = (params.frame_size, 1) - class_shape = (params.frame_size, 1) - class_shape = (128, 1) - ds_spec = ( - tf.TensorSpec(shape=feat_shape, dtype="float32"), - tf.TensorSpec(shape=class_shape, dtype="float32"), - ) - - datasets = load_datasets(datasets=params.datasets) + datasets = [DatasetFactory.get(ds.name)(**ds.params) for ds in params.datasets] train_ds, val_ds = load_train_datasets( datasets=datasets, params=params, - ds_spec=ds_spec, ) - inputs = keras.Input( - shape=ds_spec[0].shape, - batch_size=None, - name="input", - dtype=ds_spec[0].dtype.name, - ) + inputs = keras.Input(shape=feat_shape, name="input", dtype="float32") if params.resume and params.model_file: logger.debug(f"Loading model from file {params.model_file}") model = nse.models.load_model(params.model_file) params.model_file = None else: logger.debug("Creating model from scratch") - model = create_model( - inputs, + model = ModelFactory.get(params.architecture.name)( + x=inputs, + params=params.architecture.params, num_classes=params.num_classes, - architecture=params.architecture, ) # END IF - if params.lr_cycles > 1: - scheduler = keras.optimizers.schedules.CosineDecayRestarts( - initial_learning_rate=params.lr_rate, - first_decay_steps=int(0.1 * params.steps_per_epoch * params.epochs), - t_mul=1.65 / (0.1 * params.lr_cycles * (params.lr_cycles - 1)), - m_mul=0.4, - ) - else: - scheduler = keras.optimizers.schedules.CosineDecay( - initial_learning_rate=params.lr_rate, - decay_steps=params.steps_per_epoch * params.epochs, - ) - # END IF + t_mul = 1 + first_steps = (params.steps_per_epoch * params.epochs) / (np.power(params.lr_cycles, t_mul) - t_mul + 1) + scheduler = keras.optimizers.schedules.CosineDecayRestarts( + initial_learning_rate=params.lr_rate, + first_decay_steps=np.ceil(first_steps), + t_mul=t_mul, + m_mul=0.5, + ) optimizer = keras.optimizers.Adam(scheduler) loss = keras.losses.MeanSquaredError() @@ -118,7 +92,7 @@ def train(params: HKTrainParams): logger.debug(f"Model requires {flops/1e6:0.2f} MFLOPS") ModelCheckpoint = keras.callbacks.ModelCheckpoint - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): ModelCheckpoint = WandbModelCheckpoint model_callbacks = [ keras.callbacks.EarlyStopping( @@ -126,6 +100,7 @@ def train(params: HKTrainParams): patience=max(int(0.25 * params.epochs), 1), mode="max" if params.val_metric == "f1" else "auto", restore_best_weights=True, + verbose=params.verbose - 1, ), ModelCheckpoint( filepath=str(params.model_file), @@ -133,25 +108,25 @@ def train(params: HKTrainParams): save_best_only=True, save_weights_only=False, mode="max" if params.val_metric == "f1" else "auto", - verbose=1, + verbose=params.verbose - 1, ), keras.callbacks.CSVLogger(params.job_dir / "history.csv"), ] - if env_flag("TENSORBOARD"): + if nse.utils.env_flag("TENSORBOARD"): model_callbacks.append( keras.callbacks.TensorBoard( log_dir=params.job_dir, write_steps_per_second=True, ) ) - if env_flag("WANDB"): + if nse.utils.env_flag("WANDB"): model_callbacks.append(WandbMetricsLogger()) try: model.fit( train_ds, steps_per_epoch=params.steps_per_epoch, - verbose=2, + verbose=params.verbose, epochs=params.epochs, validation_data=val_ds, callbacks=model_callbacks, @@ -162,5 +137,4 @@ def train(params: HKTrainParams): logger.debug(f"Model saved to {params.model_file}") # Get full validation results - keras.models.load_model(params.model_file) logger.debug("Performing full validation") diff --git a/heartkit/tasks/translate/utils.py b/heartkit/tasks/translate/utils.py deleted file mode 100644 index bc09e55d..00000000 --- a/heartkit/tasks/translate/utils.py +++ /dev/null @@ -1,107 +0,0 @@ -import keras -from neuralspot_edge.models.tcn import Tcn, TcnBlockParams, TcnParams -from rich.console import Console - -from ...defines import ModelArchitecture -from ...models import ModelFactory - -console = Console() - - -def create_model(inputs: keras.KerasTensor, num_classes: int, architecture: ModelArchitecture | None) -> keras.Model: - """Generate model or use default - - Args: - inputs (keras.KerasTensor): Model inputs - num_classes (int): Number of classes - architecture (ModelArchitecture|None): Model - - Returns: - keras.Model: Model - """ - if architecture: - return ModelFactory.get(architecture.name)( - x=inputs, - params=architecture.params, - num_classes=num_classes, - ) - - return _default_model(inputs=inputs, num_classes=num_classes) - - -def _default_model( - inputs: keras.KerasTensor, - num_classes: int, -) -> keras.Model: - """Reference model - - Args: - inputs (keras.KerasTensor): Model inputs - num_classes (int): Number of classes - - Returns: - keras.Model: Model - """ - # Default model - - blocks = [ - TcnBlockParams( - filters=8, - kernel=(1, 7), - dilation=(1, 1), - dropout=0.1, - ex_ratio=1, - se_ratio=0, - norm="batch", - ), - TcnBlockParams( - filters=12, - kernel=(1, 7), - dilation=(1, 1), - dropout=0.1, - ex_ratio=1, - se_ratio=2, - norm="batch", - ), - TcnBlockParams( - filters=16, - kernel=(1, 7), - dilation=(1, 2), - dropout=0.1, - ex_ratio=1, - se_ratio=2, - norm="batch", - ), - TcnBlockParams( - filters=24, - kernel=(1, 7), - dilation=(1, 4), - dropout=0.1, - ex_ratio=1, - se_ratio=2, - norm="batch", - ), - TcnBlockParams( - filters=32, - kernel=(1, 7), - dilation=(1, 8), - dropout=0.1, - ex_ratio=1, - se_ratio=2, - norm="batch", - ), - ] - - return Tcn( - x=inputs, - params=TcnParams( - input_kernel=(1, 7), - input_norm="batch", - blocks=blocks, - output_kernel=(1, 7), - include_top=True, - use_logits=True, - model_name="tcn", - ), - num_classes=num_classes, - ) diff --git a/heartkit/tasks/utils.py b/heartkit/tasks/utils.py deleted file mode 100644 index 4675dac0..00000000 --- a/heartkit/tasks/utils.py +++ /dev/null @@ -1,22 +0,0 @@ -from ..datasets import DatasetFactory, HKDataset -from ..defines import DatasetParams - - -def load_datasets( - datasets: list[DatasetParams] = None, -) -> list[HKDataset]: - """Load datasets - - Args: - datasets (list[DatasetParams]): List of datasets - - Returns: - HKDataset: Dataset - """ - dsets = [] - for dset in datasets: - if DatasetFactory.has(dset.name): - dsets.append(DatasetFactory.get(dset.name)(ds_path=dset.path, **dset.params)) - # END IF - # END FOR - return dsets diff --git a/heartkit/utils/__init__.py b/heartkit/utils/__init__.py index 560cc9de..0a33f1ab 100644 --- a/heartkit/utils/__init__.py +++ b/heartkit/utils/__init__.py @@ -1,246 +1 @@ -import gzip -import hashlib -import logging - -import os -import pickle -from pathlib import Path -from string import Template -from typing import Any - -import numpy as np -import requests -from rich.logging import RichHandler -from tqdm import tqdm - -from .factory import ItemFactory, create_factory - - -def setup_logger(log_name: str, level: int | None = None) -> logging.Logger: - """Setup logger with Rich - - Args: - log_name (str): Logger name - - Returns: - logging.Logger: Logger - """ - new_logger = logging.getLogger(log_name) - needs_init = not new_logger.handlers - - match level: - case 0: - log_level = logging.ERROR - case 1: - log_level = logging.INFO - case 2 | 3 | 4: - log_level = logging.DEBUG - case None: - log_level = None - case _: - log_level = logging.INFO - # END MATCH - - if needs_init: - logging.basicConfig(level=log_level, force=True, handlers=[RichHandler(rich_tracebacks=True)]) - new_logger.propagate = False - new_logger.handlers = [RichHandler()] - - if log_level is not None: - new_logger.setLevel(log_level) - - return new_logger - - -logger = setup_logger(__name__) - - -def set_random_seed(seed: int | None = None) -> int: - """Set random seed across libraries: Keras, Numpy, Python - - Args: - seed (int | None, optional): Random seed state to use. Defaults to None. - - Returns: - int: Random seed - """ - seed = seed or np.random.randint(2**16) - try: - import keras # pylint: disable=import-outside-toplevel - except ImportError: - pass - else: - keras.utils.set_random_seed(seed) - return seed - - -def load_pkl(file: str, compress: bool = True) -> dict[str, Any]: - """Load pickled file. - - Args: - file (str): File path (.pkl) - compress (bool, optional): If file is compressed. Defaults to True. - - Returns: - dict[str, Any]: Dictionary of pickled objects - """ - if compress: - with gzip.open(file, "rb") as fh: - return pickle.load(fh) - else: - with open(file, "rb") as fh: - return pickle.load(fh) - - -def save_pkl(file: str, compress: bool = True, **kwargs): - """Save python objects into pickle file. - - Args: - file (str): File path (.pkl) - compress (bool, optional): Whether to compress file. Defaults to True. - """ - if compress: - with gzip.open(file, "wb") as fh: - pickle.dump(kwargs, fh, protocol=4) - else: - with open(file, "wb") as fh: - pickle.dump(kwargs, fh, protocol=4) - - -def env_flag(env_var: str, default: bool = False) -> bool: - """Return the specified environment variable coerced to a bool, as follows: - - When the variable is unset, or set to the empty string, return `default`. - - When the variable is set to a truthy value, returns `True`. - These are the truthy values: - - 1 - - true, yes, on - - When the variable is set to the anything else, returns False. - Example falsy values: - - 0 - - no - - Ignore case and leading/trailing whitespace. - - Args: - env_var (str): Environment variable name - default (bool, optional): Default value. Defaults to False. - - Returns: - bool: Value of environment variable - """ - environ_string = os.environ.get(env_var, "").strip().lower() - if not environ_string: - return default - return environ_string in ["1", "true", "yes", "on"] - - -def compute_checksum(file: Path, checksum_type: str = "md5", chunk_size: int = 8192) -> str: - """Compute checksum of file. - - Args: - file (Path): File path - checksum_type (str, optional): Checksum type. Defaults to "md5". - chunk_size (int, optional): Chunk size. Defaults to 8192. - - Returns: - str: Checksum value - """ - if not file.is_file(): - raise FileNotFoundError(f"File {file} not found.") - hash_algo = hashlib.new(checksum_type) - with open(file, "rb") as f: - for chunk in iter(lambda: f.read(chunk_size), b""): - hash_algo.update(chunk) - return hash_algo.hexdigest() - - -def download_file( - src: str, - dst: Path, - progress: bool = True, - chunk_size: int = 8192, - checksum: str | None = None, - checksum_type: str = "size", - timeout: int = 3600 * 24, -): - """Download file from supplied url to destination streaming. - - checksum: hd5, sha256, sha512, size - - Args: - src (str): Source URL path - dst (PathLike): Destination file path - progress (bool, optional): Display progress bar. Defaults to True. - chunk_size (int, optional): Chunk size. Defaults to 8192. - checksum (str|None, optional): Checksum value. Defaults to None. - checksum_type (str|None, optional): Checksum type or size. Defaults to None. - - Raises: - ValueError: If checksum doesn't match - - - """ - - # If file exists and checksum matches, skip download - if dst.is_file() and checksum: - match checksum_type: - case "size": - # Get number of bytes in file - calculated_checksum = str(dst.stat().st_size) - case _: - calculated_checksum = compute_checksum(dst, checksum_type, chunk_size) - if calculated_checksum == checksum: - logger.debug(f"File {dst} already exists and checksum matches. Skipping...") - return - # END IF - # END IF - - # Create parent directory if not exists - dst.parent.mkdir(parents=True, exist_ok=True) - - # Download file in chunks - with requests.get(src, stream=True, timeout=timeout) as r: - r.raise_for_status() - req_len = int(r.headers.get("Content-length", 0)) - prog_bar = tqdm(total=req_len, unit="iB", unit_scale=True) if progress else None - with open(dst, "wb") as f: - for chunk in r.iter_content(chunk_size=chunk_size): - f.write(chunk) - if prog_bar: - prog_bar.update(len(chunk)) - # END FOR - # END WITH - # END WITH - - -def resolve_template_path(fpath: Path, **kwargs: Any) -> Path: - """Resolve templated path w/ supplied substitutions. - - Args: - fpath (Path): File path - **kwargs (Any): Template arguments - - Returns: - Path: Resolved file path - """ - return Path(Template(str(fpath)).safe_substitute(**kwargs)) - - -def silence_tensorflow(): - """Silence every unnecessary warning from tensorflow.""" - logging.getLogger("tensorflow").setLevel(logging.ERROR) - os.environ["KMP_AFFINITY"] = "noverbose" - os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" - os.environ["AUTOGRAPH_VERBOSITY"] = "5" - # We wrap this inside a try-except block - # because we do not want to be the one package - # that crashes when TensorFlow is not installed - # when we are the only package that requires it - # in a given Jupyter Notebook, such as when the - # package import is simply copy-pasted. - try: - import tensorflow as tf - - tf.get_logger().setLevel("ERROR") - tf.autograph.set_verbosity(3) - except ModuleNotFoundError: - pass +from .plotting import setup_plotting, light_theme, dark_theme diff --git a/heartkit/utils/factory.py b/heartkit/utils/factory.py deleted file mode 100644 index d25fa2f7..00000000 --- a/heartkit/utils/factory.py +++ /dev/null @@ -1,105 +0,0 @@ -from typing import TypeVar, Generic, Type -from threading import Lock - -T = TypeVar("T") - - -class SingletonMeta(type): - """Thread-safe singleton.""" - - _instances = {} - _lock: Lock = Lock() - - def __call__(cls, *args, **kwargs): - with cls._lock: - if "singleton" in kwargs: - instance_name = kwargs.get("singleton") - del kwargs["singleton"] - else: - instance_name = cls - # END IF - if instance_name not in cls._instances: - instance = super().__call__(*args, **kwargs) - cls._instances[instance_name] = instance - return cls._instances[instance_name] - - -class ItemFactory(Generic[T], metaclass=SingletonMeta): - """Dataset factory enables registering, creating, and listing datasets. It is a singleton class.""" - - _items: dict[str, T] - - def __init__(self): - self._items = {} - - def __call__(cls, *args, **kwargs): - return super().__call__(*args, **kwargs) - - @classmethod - def shared(cls, factory: str): - """Get the shared instance of the factory - - Returns: - ItemFactory: shared instance - """ - return cls(singleton=factory) - - def register(self, name: str, item: T) -> None: - """Register an item - - Args: - name (str): Unique item name - item (T): Item - """ - self._items[name] = item - - def unregister(self, name: str) -> None: - """Unregister an item - - Args: - name (str): Item name - """ - self._items.pop(name, None) - - def list(self) -> list[str]: - """List registered items - - Returns: - list[str]: item names - """ - return list(self._items.keys()) - - def get(self, name: str) -> T: - """Get an item - - Args: - name (str): Item name - - Returns: - HKDataset: dataset - """ - return self._items[name] - - def has(self, name: str) -> bool: - """Check if an item is registered - - Args: - name (str): Item name - - Returns: - bool: True if dataset is registered - """ - return name in self._items - - -def create_factory(factory: str, type: Type[T]) -> ItemFactory[T]: - """Create a factory - - Args: - factory (str): Factory name - type (Type[T]): Item type - - Returns: - ItemFactory[T]: factory - """ - return ItemFactory[T].shared(factory) diff --git a/heartkit/utils/plotting.py b/heartkit/utils/plotting.py new file mode 100644 index 00000000..e033ecb8 --- /dev/null +++ b/heartkit/utils/plotting.py @@ -0,0 +1,67 @@ +import dataclasses +import matplotlib as mpl +import plotly.io as pio +import matplotlib.pyplot as plt + + +@dataclasses.dataclass +class PlotPallette: + bg_rgba_color: str = "rgba(38,42,50,1.0)" + bg_color: str = "#262a32" + primary_color: str = "#11acd5" + secondary_color: str = "#ce6cff" + tertiary_color: str = "#ea3424" + quaternary_color: str = "#5cc99a" + plotly_template: str = "plotly_dark" + matplot_template: str = "dark_background" + + @property + def colors(self): + return [self.primary_color, self.secondary_color, self.tertiary_color, self.quaternary_color] + + +# Make a light theme and a dark theme +light_theme = PlotPallette( + bg_rgba_color="rgba(255,255,255,1.0)", + bg_color="#ffffff", + primary_color="#11acd5", + secondary_color="#ce6cff", + tertiary_color="#ea3424", + quaternary_color="#5cc99a", + plotly_template="plotly", + matplot_template="default", +) + +dark_theme = PlotPallette( + bg_rgba_color="rgba(38,42,50,1.0)", + bg_color="#262a32", + primary_color="#11acd5", + secondary_color="#ce6cff", + tertiary_color="#ea3424", + quaternary_color="#5cc99a", + plotly_template="plotly_dark", + matplot_template="dark_background", +) + + +def setup_plotting(theme: PlotPallette = dark_theme): + """Setup plotting environment for matplotlib and plotly + + Args: + theme (PlotPallette, optional): Plotting theme. Defaults to dark_theme. + """ + SMALL_SIZE = 12 + MEDIUM_SIZE = 14 + BIGGER_SIZE = 16 + + pio.renderers.default = "notebook" + plt.style.use(theme.matplot_template) + mpl.rcParams["axes.facecolor"] = theme.bg_color + mpl.rcParams["figure.facecolor"] = theme.bg_color + plt.rc("font", size=SMALL_SIZE) # controls default text sizes + plt.rc("axes", titlesize=SMALL_SIZE) # fontsize of the axes title + plt.rc("axes", labelsize=MEDIUM_SIZE) # fontsize of the x and y labels + plt.rc("xtick", labelsize=SMALL_SIZE) # fontsize of the tick labels + plt.rc("ytick", labelsize=SMALL_SIZE) # fontsize of the tick labels + plt.rc("legend", fontsize=SMALL_SIZE) # legend fontsize + plt.rc("figure", titlesize=BIGGER_SIZE) # fontsize of the figure title diff --git a/mkdocs.yml b/mkdocs.yml index 1f411399..672141ba 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,14 +10,6 @@ nav: - Home: - Home: index.md - Quickstart: quickstart.md - - Modes: - - modes/index.md - - Configuration: modes/configuration.md - - Download: modes/download.md - - Train: modes/train.md - - Evaluate: modes/evaluate.md - - Export: modes/export.md - - Demo: modes/demo.md - Tasks: - tasks/index.md - Denoise: tasks/denoise.md @@ -26,14 +18,24 @@ nav: - Beat: tasks/beat.md # - Diagnostic: tasks/diagnostic.md - BYOT: tasks/byot.md - - Model Zoo: - - zoo/index.md - - Models: - - models/index.md + - Modes: + - modes/index.md + - Configuration: modes/configuration.md + - Download: modes/download.md + - Train: modes/train.md + - Evaluate: modes/evaluate.md + - Export: modes/export.md + - Demo: modes/demo.md - Datasets: - datasets/index.md + - Models: + - models/index.md + - Model Zoo: + - zoo/index.md - Guides: - guides/index.md + - API: + - api/index.md - Quickstart: - quickstart.md @@ -41,15 +43,6 @@ nav: - CLI: usage/cli.md - Python: usage/python.md - - Modes: - - modes/index.md - - Configuration: modes/configuration.md - - Download: modes/download.md - - Train: modes/train.md - - Evaluate: modes/evaluate.md - - Export: modes/export.md - - Demo: modes/demo.md - - Tasks: - tasks/index.md - Denoise: tasks/denoise.md @@ -59,16 +52,14 @@ nav: # - Diagnostic: tasks/diagnostic.md - BYOT: tasks/byot.md - - Model Zoo: - - zoo/index.md - - Denoise: zoo/denoise.md - - Segmentation: zoo/segmentation.md - - Rhythm: zoo/rhythm.md - - Beat: zoo/beat.md - # - Diagnostic: zoo/diagnostic.md - - - Models: - - models/index.md + - Modes: + - modes/index.md + - Configuration: modes/configuration.md + - Download: modes/download.md + - Train: modes/train.md + - Evaluate: modes/evaluate.md + - Export: modes/export.md + - Demo: modes/demo.md - Datasets: - datasets/index.md @@ -81,6 +72,18 @@ nav: - MIT-BIH: datasets/mitbih.md - BYOD: datasets/byod.md + - Models: + - models/index.md + - BYOM: models/byom.md + + - Model Zoo: + - zoo/index.md + - Denoise: zoo/denoise.md + - Segmentation: zoo/segmentation.md + - Rhythm: zoo/rhythm.md + - Beat: zoo/beat.md + # - Diagnostic: zoo/diagnostic.md + - Guides: - guides/index.md - EVB Setup: guides/evb-setup.md @@ -90,11 +93,31 @@ nav: - Train ECG Denoiser: guides/train-ecg-denoiser.ipynb - Train ECG Segmentation Model: guides/train-ecg-segmentation.ipynb - - Reference: + - API: - HeartKit: api/heartkit.md - - Datasets: api/datasets.md - - Models: api/models.md - - Tasks: api/tasks.md + - Datasets: + - Dataset: api/datasets/dataset.md + - DatasetFactory: api/datasets/factory.md + - Dataloader: api/datasets/dataloader.md + - Augmentations: api/datasets/augmentation.md + - Synthetic: api/datasets/synthetic.md + - Icentia11k: api/datasets/icentia11k.md + - QTDB: api/datasets/qtdb.md + - LUDB: api/datasets/ludb.md + - LSAD: api/datasets/lsad.md + - PTB-XL: api/datasets/ptbxl.md + - Models: + - Model: api/models/model.md + - ModelFactory: api/models/factory.md + - Tasks: + - Task: api/tasks/task.md + - TaskFactory: api/tasks/factory.md + - Beat: api/tasks/beat.md + - Denoise: api/tasks/denoise.md + - Foundation: api/tasks/foundation.md + - Segmentation: api/tasks/segmentation.md + - Rhythm: api/tasks/rhythm.md + theme: name: material @@ -139,12 +162,14 @@ theme: - navigation.tabs - navigation.tabs.sticky - navigation.prune + - navigation.path + - navigation.footer - navigation.tracking - navigation.instant - navigation.instant.progress - navigation.indexes - - navigation.sections # navigation.expand or navigation.sections + - navigation.expand # navigation.expand or navigation.sections - content.tabs.link # all code tabs change simultaneously plugins: @@ -156,10 +181,28 @@ plugins: - https://docs.python.org/3/objects.inv - https://numpy.org/doc/stable/objects.inv options: + show_bases: false + show_root_heading: false + parameter_headings: true + show_root_toc_entry: false + show_symbol_type_toc: false + group_by_category: true + show_category_heading: true docstring_style: google - docstring_section_style: list - line_length: 92 - show_root_heading: true + docstring_section_style: table + members_order: source + filters: ["!^_", "^__init__$"] + line_length: 120 + heading_level: 3 + merge_init_into_class: true + show_root_full_path: false + show_symbol_type_heading: false + modernize_annotations: true + show_signature: true + show_signature_annotations: false + separate_signature: false + show_source: true + - mkdocs-jupyter: include_requirejs: true include_source: true diff --git a/poetry.lock b/poetry.lock index 391b6fbf..f991951e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -66,32 +66,33 @@ files = [ [[package]] name = "argdantic" -version = "1.1.0" +version = "1.3.0" description = "Typed command line interfaces with argparse and pydantic" optional = false python-versions = "*" files = [ - {file = "argdantic-1.1.0-py2.py3-none-any.whl", hash = "sha256:0ddabe399bf9577fcd73c02aa484c0b66be95b1d1d53ed536c7738b5fe5c3726"}, - {file = "argdantic-1.1.0.tar.gz", hash = "sha256:6778204f926cb7d23aa1eaedf34d374a4d137977c8699843d7e1d2684df571ed"}, + {file = "argdantic-1.3.0-py2.py3-none-any.whl", hash = "sha256:a6d53d4dd9d2bc24fd891bab020a46675d86910c39dbe8e8154684110a1898b2"}, + {file = "argdantic-1.3.0.tar.gz", hash = "sha256:abf79612e18f0d4d0e9cf04227d4e1d9428de053f132a675e64faa7fa6db8a99"}, ] [package.dependencies] -orjson = {version = ">=3.9.0,<4.0", optional = true, markers = "extra == \"all\""} -pydantic = ">=2.3.0,<3.0" -pydantic-settings = ">=2.0.0,<3" +orjson = {version = ">=3.10.0,<4.0", optional = true, markers = "extra == \"all\""} +pydantic = ">=2.8.0,<3.0" +pydantic-settings = ">=2.4.0,<3" python-dotenv = {version = ">=1.0.0,<2.0", optional = true, markers = "extra == \"all\""} pyyaml = {version = ">=6.0.0,<7.0", optional = true, markers = "extra == \"all\""} +toml = {version = ">=0.10.0,<1.0", optional = true, markers = "extra == \"all\""} tomli = {version = ">=2.0,<3.0", optional = true, markers = "extra == \"all\""} tomli-w = {version = ">=1.0.0,<2.0", optional = true, markers = "extra == \"all\""} [package.extras] -all = ["orjson (>=3.9.0,<4.0)", "python-dotenv (>=1.0.0,<2.0)", "pyyaml (>=6.0.0,<7.0)", "tomli (>=2.0,<3.0)", "tomli-w (>=1.0.0,<2.0)"] -dev = ["black (>=23.9.0,<24.0)", "flake8 (>=6.1.0,<7.0)", "isort (>=5.10.0,<6.0)"] -docs = ["mdx-include (>=1.4.0,<2.0)", "mkdocs (>=1.5.0,<2.0)", "mkdocs-material (>=9.3.0,<10.0)"] +all = ["orjson (>=3.10.0,<4.0)", "python-dotenv (>=1.0.0,<2.0)", "pyyaml (>=6.0.0,<7.0)", "toml (>=0.10.0,<1.0)", "tomli (>=2.0,<3.0)", "tomli-w (>=1.0.0,<2.0)"] +dev = ["flit (>=3.9.0,<4.0)", "ruff (>=0.5.6,<1.0)"] +docs = ["mdx-include (>=1.4.0,<2.0)", "mkdocs (>=1.6.0,<2.0)", "mkdocs-material (>=9.5.0,<10.0)"] env = ["python-dotenv (>=1.0.0,<2.0)"] -json = ["orjson (>=3.9.0,<4.0)"] -test = ["coverage (>=7.3.0,<8.0)", "mock (>=5.1.0,<6.0)", "pytest (>=6.2.5,<7.0)", "pytest-cov (>=4.1.0,<5.0)", "pytest-xdist (>=3.3.0,<4.0)"] -toml = ["tomli (>=2.0,<3.0)", "tomli-w (>=1.0.0,<2.0)"] +json = ["orjson (>=3.10.0,<4.0)"] +test = ["coverage (>=7.6.0,<8.0)", "mock (>=5.1.0,<6.0)", "pytest (>=8.3.0,<9.0)", "pytest-cov (>=5.0.0,<6.0)", "pytest-xdist (>=3.6.0,<4.0)"] +toml = ["toml (>=0.10.0,<1.0)", "tomli (>=2.0,<3.0)", "tomli-w (>=1.0.0,<2.0)"] yaml = ["pyyaml (>=6.0.0,<7.0)"] [[package]] @@ -216,32 +217,32 @@ files = [ [[package]] name = "attrs" -version = "23.2.0" +version = "24.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, ] [package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "babel" -version = "2.15.0" +version = "2.16.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" files = [ - {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, - {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, ] [package.extras] @@ -288,17 +289,17 @@ css = ["tinycss2 (>=1.1.0,<1.3)"] [[package]] name = "boto3" -version = "1.34.144" +version = "1.34.158" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.144-py3-none-any.whl", hash = "sha256:b8433d481d50b68a0162c0379c0dd4aabfc3d1ad901800beb5b87815997511c1"}, - {file = "boto3-1.34.144.tar.gz", hash = "sha256:2f3e88b10b8fcc5f6100a9d74cd28230edc9d4fa226d99dd40a3ab38ac213673"}, + {file = "boto3-1.34.158-py3-none-any.whl", hash = "sha256:c29e9b7e1034e8734ccaffb9f2b3f3df2268022fd8a93d836604019f8759ce27"}, + {file = "boto3-1.34.158.tar.gz", hash = "sha256:5b7b2ce0ec1e498933f600d29f3e1c641f8c44dd7e468c26795359d23d81fa39"}, ] [package.dependencies] -botocore = ">=1.34.144,<1.35.0" +botocore = ">=1.34.158,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -307,13 +308,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.144" +version = "1.34.158" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.144-py3-none-any.whl", hash = "sha256:a2cf26e1bf10d5917a2285e50257bc44e94a1d16574f282f3274f7a5d8d1f08b"}, - {file = "botocore-1.34.144.tar.gz", hash = "sha256:4215db28d25309d59c99507f1f77df9089e5bebbad35f6e19c7c44ec5383a3e8"}, + {file = "botocore-1.34.158-py3-none-any.whl", hash = "sha256:0e6fceba1e39bfa8feeba70ba3ac2af958b3387df4bd3b5f2db3f64c1754c756"}, + {file = "botocore-1.34.158.tar.gz", hash = "sha256:5934082e25ad726673afbf466092fb1223dafa250e6e756c819430ba6b1b3da5"}, ] [package.dependencies] @@ -322,7 +323,7 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] -crt = ["awscrt (==0.20.11)"] +crt = ["awscrt (==0.21.2)"] [[package]] name = "certifi" @@ -337,63 +338,78 @@ files = [ [[package]] name = "cffi" -version = "1.16.0" +version = "1.17.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, + {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, + {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, + {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, + {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, + {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, + {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, + {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, + {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, + {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, + {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, + {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, + {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, + {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, + {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, + {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, + {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, + {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, + {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, ] [package.dependencies] @@ -743,33 +759,33 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "debugpy" -version = "1.8.2" +version = "1.8.5" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" files = [ - {file = "debugpy-1.8.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:7ee2e1afbf44b138c005e4380097d92532e1001580853a7cb40ed84e0ef1c3d2"}, - {file = "debugpy-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f8c3f7c53130a070f0fc845a0f2cee8ed88d220d6b04595897b66605df1edd6"}, - {file = "debugpy-1.8.2-cp310-cp310-win32.whl", hash = "sha256:f179af1e1bd4c88b0b9f0fa153569b24f6b6f3de33f94703336363ae62f4bf47"}, - {file = "debugpy-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:0600faef1d0b8d0e85c816b8bb0cb90ed94fc611f308d5fde28cb8b3d2ff0fe3"}, - {file = "debugpy-1.8.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8a13417ccd5978a642e91fb79b871baded925d4fadd4dfafec1928196292aa0a"}, - {file = "debugpy-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acdf39855f65c48ac9667b2801234fc64d46778021efac2de7e50907ab90c634"}, - {file = "debugpy-1.8.2-cp311-cp311-win32.whl", hash = "sha256:2cbd4d9a2fc5e7f583ff9bf11f3b7d78dfda8401e8bb6856ad1ed190be4281ad"}, - {file = "debugpy-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:d3408fddd76414034c02880e891ea434e9a9cf3a69842098ef92f6e809d09afa"}, - {file = "debugpy-1.8.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:5d3ccd39e4021f2eb86b8d748a96c766058b39443c1f18b2dc52c10ac2757835"}, - {file = "debugpy-1.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62658aefe289598680193ff655ff3940e2a601765259b123dc7f89c0239b8cd3"}, - {file = "debugpy-1.8.2-cp312-cp312-win32.whl", hash = "sha256:bd11fe35d6fd3431f1546d94121322c0ac572e1bfb1f6be0e9b8655fb4ea941e"}, - {file = "debugpy-1.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:15bc2f4b0f5e99bf86c162c91a74c0631dbd9cef3c6a1d1329c946586255e859"}, - {file = "debugpy-1.8.2-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:5a019d4574afedc6ead1daa22736c530712465c0c4cd44f820d803d937531b2d"}, - {file = "debugpy-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40f062d6877d2e45b112c0bbade9a17aac507445fd638922b1a5434df34aed02"}, - {file = "debugpy-1.8.2-cp38-cp38-win32.whl", hash = "sha256:c78ba1680f1015c0ca7115671fe347b28b446081dada3fedf54138f44e4ba031"}, - {file = "debugpy-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:cf327316ae0c0e7dd81eb92d24ba8b5e88bb4d1b585b5c0d32929274a66a5210"}, - {file = "debugpy-1.8.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1523bc551e28e15147815d1397afc150ac99dbd3a8e64641d53425dba57b0ff9"}, - {file = "debugpy-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e24ccb0cd6f8bfaec68d577cb49e9c680621c336f347479b3fce060ba7c09ec1"}, - {file = "debugpy-1.8.2-cp39-cp39-win32.whl", hash = "sha256:7f8d57a98c5a486c5c7824bc0b9f2f11189d08d73635c326abef268f83950326"}, - {file = "debugpy-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:16c8dcab02617b75697a0a925a62943e26a0330da076e2a10437edd9f0bf3755"}, - {file = "debugpy-1.8.2-py2.py3-none-any.whl", hash = "sha256:16e16df3a98a35c63c3ab1e4d19be4cbc7fdda92d9ddc059294f18910928e0ca"}, - {file = "debugpy-1.8.2.zip", hash = "sha256:95378ed08ed2089221896b9b3a8d021e642c24edc8fef20e5d4342ca8be65c00"}, + {file = "debugpy-1.8.5-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7e4d594367d6407a120b76bdaa03886e9eb652c05ba7f87e37418426ad2079f7"}, + {file = "debugpy-1.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4413b7a3ede757dc33a273a17d685ea2b0c09dbd312cc03f5534a0fd4d40750a"}, + {file = "debugpy-1.8.5-cp310-cp310-win32.whl", hash = "sha256:dd3811bd63632bb25eda6bd73bea8e0521794cda02be41fa3160eb26fc29e7ed"}, + {file = "debugpy-1.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:b78c1250441ce893cb5035dd6f5fc12db968cc07f91cc06996b2087f7cefdd8e"}, + {file = "debugpy-1.8.5-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:606bccba19f7188b6ea9579c8a4f5a5364ecd0bf5a0659c8a5d0e10dcee3032a"}, + {file = "debugpy-1.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db9fb642938a7a609a6c865c32ecd0d795d56c1aaa7a7a5722d77855d5e77f2b"}, + {file = "debugpy-1.8.5-cp311-cp311-win32.whl", hash = "sha256:4fbb3b39ae1aa3e5ad578f37a48a7a303dad9a3d018d369bc9ec629c1cfa7408"}, + {file = "debugpy-1.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:345d6a0206e81eb68b1493ce2fbffd57c3088e2ce4b46592077a943d2b968ca3"}, + {file = "debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156"}, + {file = "debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb"}, + {file = "debugpy-1.8.5-cp312-cp312-win32.whl", hash = "sha256:c9f7c15ea1da18d2fcc2709e9f3d6de98b69a5b0fff1807fb80bc55f906691f7"}, + {file = "debugpy-1.8.5-cp312-cp312-win_amd64.whl", hash = "sha256:28ced650c974aaf179231668a293ecd5c63c0a671ae6d56b8795ecc5d2f48d3c"}, + {file = "debugpy-1.8.5-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:3df6692351172a42af7558daa5019651f898fc67450bf091335aa8a18fbf6f3a"}, + {file = "debugpy-1.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd04a73eb2769eb0bfe43f5bfde1215c5923d6924b9b90f94d15f207a402226"}, + {file = "debugpy-1.8.5-cp38-cp38-win32.whl", hash = "sha256:8f913ee8e9fcf9d38a751f56e6de12a297ae7832749d35de26d960f14280750a"}, + {file = "debugpy-1.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:a697beca97dad3780b89a7fb525d5e79f33821a8bc0c06faf1f1289e549743cf"}, + {file = "debugpy-1.8.5-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0a1029a2869d01cb777216af8c53cda0476875ef02a2b6ff8b2f2c9a4b04176c"}, + {file = "debugpy-1.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84c276489e141ed0b93b0af648eef891546143d6a48f610945416453a8ad406"}, + {file = "debugpy-1.8.5-cp39-cp39-win32.whl", hash = "sha256:ad84b7cde7fd96cf6eea34ff6c4a1b7887e0fe2ea46e099e53234856f9d99a34"}, + {file = "debugpy-1.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:7b0fe36ed9d26cb6836b0a51453653f8f2e347ba7348f2bbfe76bfeb670bfb1c"}, + {file = "debugpy-1.8.5-py2.py3-none-any.whl", hash = "sha256:55919dce65b471eff25901acf82d328bbd5b833526b6c1364bd5133754777a44"}, + {file = "debugpy-1.8.5.zip", hash = "sha256:b2112cfeb34b4507399d298fe7023a16656fc553ed5246536060ca7bd0e668d0"}, ] [[package]] @@ -1123,13 +1139,13 @@ six = "*" [[package]] name = "griffe" -version = "0.47.0" +version = "0.48.0" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false python-versions = ">=3.8" files = [ - {file = "griffe-0.47.0-py3-none-any.whl", hash = "sha256:07a2fd6a8c3d21d0bbb0decf701d62042ccc8a576645c7f8799fe1f10de2b2de"}, - {file = "griffe-0.47.0.tar.gz", hash = "sha256:95119a440a3c932b13293538bdbc405bee4c36428547553dc6b327e7e7d35e5a"}, + {file = "griffe-0.48.0-py3-none-any.whl", hash = "sha256:f944c6ff7bd31cf76f264adcd6ab8f3d00a2f972ae5cc8db2d7b6dcffeff65a2"}, + {file = "griffe-0.48.0.tar.gz", hash = "sha256:f099461c02f016b6be4af386d5aa92b01fb4efe6c1c2c360dda9a5d0a863bb7f"}, ] [package.dependencies] @@ -1137,61 +1153,61 @@ colorama = ">=0.4" [[package]] name = "grpcio" -version = "1.64.1" +version = "1.65.4" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.8" files = [ - {file = "grpcio-1.64.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:55697ecec192bc3f2f3cc13a295ab670f51de29884ca9ae6cd6247df55df2502"}, - {file = "grpcio-1.64.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:3b64ae304c175671efdaa7ec9ae2cc36996b681eb63ca39c464958396697daff"}, - {file = "grpcio-1.64.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:bac71b4b28bc9af61efcdc7630b166440bbfbaa80940c9a697271b5e1dabbc61"}, - {file = "grpcio-1.64.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c024ffc22d6dc59000faf8ad781696d81e8e38f4078cb0f2630b4a3cf231a90"}, - {file = "grpcio-1.64.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7cd5c1325f6808b8ae31657d281aadb2a51ac11ab081ae335f4f7fc44c1721d"}, - {file = "grpcio-1.64.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0a2813093ddb27418a4c99f9b1c223fab0b053157176a64cc9db0f4557b69bd9"}, - {file = "grpcio-1.64.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2981c7365a9353f9b5c864595c510c983251b1ab403e05b1ccc70a3d9541a73b"}, - {file = "grpcio-1.64.1-cp310-cp310-win32.whl", hash = "sha256:1262402af5a511c245c3ae918167eca57342c72320dffae5d9b51840c4b2f86d"}, - {file = "grpcio-1.64.1-cp310-cp310-win_amd64.whl", hash = "sha256:19264fc964576ddb065368cae953f8d0514ecc6cb3da8903766d9fb9d4554c33"}, - {file = "grpcio-1.64.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:58b1041e7c870bb30ee41d3090cbd6f0851f30ae4eb68228955d973d3efa2e61"}, - {file = "grpcio-1.64.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bbc5b1d78a7822b0a84c6f8917faa986c1a744e65d762ef6d8be9d75677af2ca"}, - {file = "grpcio-1.64.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:5841dd1f284bd1b3d8a6eca3a7f062b06f1eec09b184397e1d1d43447e89a7ae"}, - {file = "grpcio-1.64.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8caee47e970b92b3dd948371230fcceb80d3f2277b3bf7fbd7c0564e7d39068e"}, - {file = "grpcio-1.64.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73819689c169417a4f978e562d24f2def2be75739c4bed1992435d007819da1b"}, - {file = "grpcio-1.64.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6503b64c8b2dfad299749cad1b595c650c91e5b2c8a1b775380fcf8d2cbba1e9"}, - {file = "grpcio-1.64.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1de403fc1305fd96cfa75e83be3dee8538f2413a6b1685b8452301c7ba33c294"}, - {file = "grpcio-1.64.1-cp311-cp311-win32.whl", hash = "sha256:d4d29cc612e1332237877dfa7fe687157973aab1d63bd0f84cf06692f04c0367"}, - {file = "grpcio-1.64.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e56462b05a6f860b72f0fa50dca06d5b26543a4e88d0396259a07dc30f4e5aa"}, - {file = "grpcio-1.64.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:4657d24c8063e6095f850b68f2d1ba3b39f2b287a38242dcabc166453e950c59"}, - {file = "grpcio-1.64.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:62b4e6eb7bf901719fce0ca83e3ed474ae5022bb3827b0a501e056458c51c0a1"}, - {file = "grpcio-1.64.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:ee73a2f5ca4ba44fa33b4d7d2c71e2c8a9e9f78d53f6507ad68e7d2ad5f64a22"}, - {file = "grpcio-1.64.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:198908f9b22e2672a998870355e226a725aeab327ac4e6ff3a1399792ece4762"}, - {file = "grpcio-1.64.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39b9d0acaa8d835a6566c640f48b50054f422d03e77e49716d4c4e8e279665a1"}, - {file = "grpcio-1.64.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5e42634a989c3aa6049f132266faf6b949ec2a6f7d302dbb5c15395b77d757eb"}, - {file = "grpcio-1.64.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1a82e0b9b3022799c336e1fc0f6210adc019ae84efb7321d668129d28ee1efb"}, - {file = "grpcio-1.64.1-cp312-cp312-win32.whl", hash = "sha256:55260032b95c49bee69a423c2f5365baa9369d2f7d233e933564d8a47b893027"}, - {file = "grpcio-1.64.1-cp312-cp312-win_amd64.whl", hash = "sha256:c1a786ac592b47573a5bb7e35665c08064a5d77ab88a076eec11f8ae86b3e3f6"}, - {file = "grpcio-1.64.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:a011ac6c03cfe162ff2b727bcb530567826cec85eb8d4ad2bfb4bd023287a52d"}, - {file = "grpcio-1.64.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4d6dab6124225496010bd22690f2d9bd35c7cbb267b3f14e7a3eb05c911325d4"}, - {file = "grpcio-1.64.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:a5e771d0252e871ce194d0fdcafd13971f1aae0ddacc5f25615030d5df55c3a2"}, - {file = "grpcio-1.64.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3c1b90ab93fed424e454e93c0ed0b9d552bdf1b0929712b094f5ecfe7a23ad"}, - {file = "grpcio-1.64.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20405cb8b13fd779135df23fabadc53b86522d0f1cba8cca0e87968587f50650"}, - {file = "grpcio-1.64.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0cc79c982ccb2feec8aad0e8fb0d168bcbca85bc77b080d0d3c5f2f15c24ea8f"}, - {file = "grpcio-1.64.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a3a035c37ce7565b8f4f35ff683a4db34d24e53dc487e47438e434eb3f701b2a"}, - {file = "grpcio-1.64.1-cp38-cp38-win32.whl", hash = "sha256:1257b76748612aca0f89beec7fa0615727fd6f2a1ad580a9638816a4b2eb18fd"}, - {file = "grpcio-1.64.1-cp38-cp38-win_amd64.whl", hash = "sha256:0a12ddb1678ebc6a84ec6b0487feac020ee2b1659cbe69b80f06dbffdb249122"}, - {file = "grpcio-1.64.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:75dbbf415026d2862192fe1b28d71f209e2fd87079d98470db90bebe57b33179"}, - {file = "grpcio-1.64.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e3d9f8d1221baa0ced7ec7322a981e28deb23749c76eeeb3d33e18b72935ab62"}, - {file = "grpcio-1.64.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:5f8b75f64d5d324c565b263c67dbe4f0af595635bbdd93bb1a88189fc62ed2e5"}, - {file = "grpcio-1.64.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c84ad903d0d94311a2b7eea608da163dace97c5fe9412ea311e72c3684925602"}, - {file = "grpcio-1.64.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:940e3ec884520155f68a3b712d045e077d61c520a195d1a5932c531f11883489"}, - {file = "grpcio-1.64.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f10193c69fc9d3d726e83bbf0f3d316f1847c3071c8c93d8090cf5f326b14309"}, - {file = "grpcio-1.64.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac15b6c2c80a4d1338b04d42a02d376a53395ddf0ec9ab157cbaf44191f3ffdd"}, - {file = "grpcio-1.64.1-cp39-cp39-win32.whl", hash = "sha256:03b43d0ccf99c557ec671c7dede64f023c7da9bb632ac65dbc57f166e4970040"}, - {file = "grpcio-1.64.1-cp39-cp39-win_amd64.whl", hash = "sha256:ed6091fa0adcc7e4ff944090cf203a52da35c37a130efa564ded02b7aff63bcd"}, - {file = "grpcio-1.64.1.tar.gz", hash = "sha256:8d51dd1c59d5fa0f34266b80a3805ec29a1f26425c2a54736133f6d87fc4968a"}, + {file = "grpcio-1.65.4-cp310-cp310-linux_armv7l.whl", hash = "sha256:0e85c8766cf7f004ab01aff6a0393935a30d84388fa3c58d77849fcf27f3e98c"}, + {file = "grpcio-1.65.4-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:e4a795c02405c7dfa8affd98c14d980f4acea16ea3b539e7404c645329460e5a"}, + {file = "grpcio-1.65.4-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:d7b984a8dd975d949c2042b9b5ebcf297d6d5af57dcd47f946849ee15d3c2fb8"}, + {file = "grpcio-1.65.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644a783ce604a7d7c91412bd51cf9418b942cf71896344b6dc8d55713c71ce82"}, + {file = "grpcio-1.65.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5764237d751d3031a36fafd57eb7d36fd2c10c658d2b4057c516ccf114849a3e"}, + {file = "grpcio-1.65.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ee40d058cf20e1dd4cacec9c39e9bce13fedd38ce32f9ba00f639464fcb757de"}, + {file = "grpcio-1.65.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4482a44ce7cf577a1f8082e807a5b909236bce35b3e3897f839f2fbd9ae6982d"}, + {file = "grpcio-1.65.4-cp310-cp310-win32.whl", hash = "sha256:66bb051881c84aa82e4f22d8ebc9d1704b2e35d7867757f0740c6ef7b902f9b1"}, + {file = "grpcio-1.65.4-cp310-cp310-win_amd64.whl", hash = "sha256:870370524eff3144304da4d1bbe901d39bdd24f858ce849b7197e530c8c8f2ec"}, + {file = "grpcio-1.65.4-cp311-cp311-linux_armv7l.whl", hash = "sha256:85e9c69378af02e483bc626fc19a218451b24a402bdf44c7531e4c9253fb49ef"}, + {file = "grpcio-1.65.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2bd672e005afab8bf0d6aad5ad659e72a06dd713020554182a66d7c0c8f47e18"}, + {file = "grpcio-1.65.4-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:abccc5d73f5988e8f512eb29341ed9ced923b586bb72e785f265131c160231d8"}, + {file = "grpcio-1.65.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:886b45b29f3793b0c2576201947258782d7e54a218fe15d4a0468d9a6e00ce17"}, + {file = "grpcio-1.65.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be952436571dacc93ccc7796db06b7daf37b3b56bb97e3420e6503dccfe2f1b4"}, + {file = "grpcio-1.65.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8dc9ddc4603ec43f6238a5c95400c9a901b6d079feb824e890623da7194ff11e"}, + {file = "grpcio-1.65.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ade1256c98cba5a333ef54636095f2c09e6882c35f76acb04412f3b1aa3c29a5"}, + {file = "grpcio-1.65.4-cp311-cp311-win32.whl", hash = "sha256:280e93356fba6058cbbfc6f91a18e958062ef1bdaf5b1caf46c615ba1ae71b5b"}, + {file = "grpcio-1.65.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2b819f9ee27ed4e3e737a4f3920e337e00bc53f9e254377dd26fc7027c4d558"}, + {file = "grpcio-1.65.4-cp312-cp312-linux_armv7l.whl", hash = "sha256:926a0750a5e6fb002542e80f7fa6cab8b1a2ce5513a1c24641da33e088ca4c56"}, + {file = "grpcio-1.65.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:2a1d4c84d9e657f72bfbab8bedf31bdfc6bfc4a1efb10b8f2d28241efabfaaf2"}, + {file = "grpcio-1.65.4-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:17de4fda50967679677712eec0a5c13e8904b76ec90ac845d83386b65da0ae1e"}, + {file = "grpcio-1.65.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dee50c1b69754a4228e933696408ea87f7e896e8d9797a3ed2aeed8dbd04b74"}, + {file = "grpcio-1.65.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74c34fc7562bdd169b77966068434a93040bfca990e235f7a67cdf26e1bd5c63"}, + {file = "grpcio-1.65.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:24a2246e80a059b9eb981e4c2a6d8111b1b5e03a44421adbf2736cc1d4988a8a"}, + {file = "grpcio-1.65.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18c10f0d054d2dce34dd15855fcca7cc44ec3b811139437543226776730c0f28"}, + {file = "grpcio-1.65.4-cp312-cp312-win32.whl", hash = "sha256:d72962788b6c22ddbcdb70b10c11fbb37d60ae598c51eb47ec019db66ccfdff0"}, + {file = "grpcio-1.65.4-cp312-cp312-win_amd64.whl", hash = "sha256:7656376821fed8c89e68206a522522317787a3d9ed66fb5110b1dff736a5e416"}, + {file = "grpcio-1.65.4-cp38-cp38-linux_armv7l.whl", hash = "sha256:4934077b33aa6fe0b451de8b71dabde96bf2d9b4cb2b3187be86e5adebcba021"}, + {file = "grpcio-1.65.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0cef8c919a3359847c357cb4314e50ed1f0cca070f828ee8f878d362fd744d52"}, + {file = "grpcio-1.65.4-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:a925446e6aa12ca37114840d8550f308e29026cdc423a73da3043fd1603a6385"}, + {file = "grpcio-1.65.4-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf53e6247f1e2af93657e62e240e4f12e11ee0b9cef4ddcb37eab03d501ca864"}, + {file = "grpcio-1.65.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdb34278e4ceb224c89704cd23db0d902e5e3c1c9687ec9d7c5bb4c150f86816"}, + {file = "grpcio-1.65.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e6cbdd107e56bde55c565da5fd16f08e1b4e9b0674851d7749e7f32d8645f524"}, + {file = "grpcio-1.65.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:626319a156b1f19513156a3b0dbfe977f5f93db63ca673a0703238ebd40670d7"}, + {file = "grpcio-1.65.4-cp38-cp38-win32.whl", hash = "sha256:3d1bbf7e1dd1096378bd83c83f554d3b93819b91161deaf63e03b7022a85224a"}, + {file = "grpcio-1.65.4-cp38-cp38-win_amd64.whl", hash = "sha256:a99e6dffefd3027b438116f33ed1261c8d360f0dd4f943cb44541a2782eba72f"}, + {file = "grpcio-1.65.4-cp39-cp39-linux_armv7l.whl", hash = "sha256:874acd010e60a2ec1e30d5e505b0651ab12eb968157cd244f852b27c6dbed733"}, + {file = "grpcio-1.65.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b07f36faf01fca5427d4aa23645e2d492157d56c91fab7e06fe5697d7e171ad4"}, + {file = "grpcio-1.65.4-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:b81711bf4ec08a3710b534e8054c7dcf90f2edc22bebe11c1775a23f145595fe"}, + {file = "grpcio-1.65.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88fcabc332a4aef8bcefadc34a02e9ab9407ab975d2c7d981a8e12c1aed92aa1"}, + {file = "grpcio-1.65.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9ba3e63108a8749994f02c7c0e156afb39ba5bdf755337de8e75eb685be244b"}, + {file = "grpcio-1.65.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8eb485801957a486bf5de15f2c792d9f9c897a86f2f18db8f3f6795a094b4bb2"}, + {file = "grpcio-1.65.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075f3903bc1749ace93f2b0664f72964ee5f2da5c15d4b47e0ab68e4f442c257"}, + {file = "grpcio-1.65.4-cp39-cp39-win32.whl", hash = "sha256:0a0720299bdb2cc7306737295d56e41ce8827d5669d4a3cd870af832e3b17c4d"}, + {file = "grpcio-1.65.4-cp39-cp39-win_amd64.whl", hash = "sha256:a146bc40fa78769f22e1e9ff4f110ef36ad271b79707577bf2a31e3e931141b9"}, + {file = "grpcio-1.65.4.tar.gz", hash = "sha256:2a4f476209acffec056360d3e647ae0e14ae13dcf3dfb130c227ae1c594cbe39"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.64.1)"] +protobuf = ["grpcio-tools (>=1.65.4)"] [[package]] name = "gviz-api" @@ -1677,13 +1693,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (> [[package]] name = "jupyterlab" -version = "4.2.3" +version = "4.2.4" description = "JupyterLab computational environment" optional = false python-versions = ">=3.8" files = [ - {file = "jupyterlab-4.2.3-py3-none-any.whl", hash = "sha256:0b59d11808e84bb84105c73364edfa867dd475492429ab34ea388a52f2e2e596"}, - {file = "jupyterlab-4.2.3.tar.gz", hash = "sha256:df6e46969ea51d66815167f23d92f105423b7f1f06fa604d4f44aeb018c82c7b"}, + {file = "jupyterlab-4.2.4-py3-none-any.whl", hash = "sha256:807a7ec73637744f879e112060d4b9d9ebe028033b7a429b2d1f4fc523d00245"}, + {file = "jupyterlab-4.2.4.tar.gz", hash = "sha256:343a979fb9582fd08c8511823e320703281cd072a0049bcdafdc7afeda7f2537"}, ] [package.dependencies] @@ -1706,7 +1722,7 @@ dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"] docs-screenshots = ["altair (==5.3.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.2)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.1.post2)", "matplotlib (==3.8.3)", "nbconvert (>=7.0.0)", "pandas (==2.2.1)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"] test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] -upgrade-extension = ["copier (>=8,<10)", "jinja2-time (<0.3)", "pydantic (<2.0)", "pyyaml-include (<2.0)", "tomli-w (<2.0)"] +upgrade-extension = ["copier (>=9,<10)", "jinja2-time (<0.3)", "pydantic (<3.0)", "pyyaml-include (<3.0)", "tomli-w (<2.0)"] [[package]] name = "jupyterlab-pygments" @@ -1721,13 +1737,13 @@ files = [ [[package]] name = "jupyterlab-server" -version = "2.27.2" +version = "2.27.3" description = "A set of server components for JupyterLab and JupyterLab like applications." optional = false python-versions = ">=3.8" files = [ - {file = "jupyterlab_server-2.27.2-py3-none-any.whl", hash = "sha256:54aa2d64fd86383b5438d9f0c032f043c4d8c0264b8af9f60bd061157466ea43"}, - {file = "jupyterlab_server-2.27.2.tar.gz", hash = "sha256:15cbb349dc45e954e09bacf81b9f9bcb10815ff660fb2034ecd7417db3a7ea27"}, + {file = "jupyterlab_server-2.27.3-py3-none-any.whl", hash = "sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4"}, + {file = "jupyterlab_server-2.27.3.tar.gz", hash = "sha256:eb36caca59e74471988f0ae25c77945610b887f777255aa21f8065def9e51ed4"}, ] [package.dependencies] @@ -1746,13 +1762,13 @@ test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-v [[package]] name = "jupytext" -version = "1.16.3" +version = "1.16.4" description = "Jupyter notebooks as Markdown documents, Julia, Python or R scripts" optional = false python-versions = ">=3.8" files = [ - {file = "jupytext-1.16.3-py3-none-any.whl", hash = "sha256:870e0d7a716dcb1303df6ad1cec65e3315a20daedd808a55cb3dae2d56e4ed20"}, - {file = "jupytext-1.16.3.tar.gz", hash = "sha256:1ebac990461dd9f477ff7feec9e3003fa1acc89f3c16ba01b73f79fd76f01a98"}, + {file = "jupytext-1.16.4-py3-none-any.whl", hash = "sha256:76989d2690e65667ea6fb411d8056abe7cd0437c07bd774660b83d62acf9490a"}, + {file = "jupytext-1.16.4.tar.gz", hash = "sha256:28e33f46f2ce7a41fb9d677a4a2c95327285579b64ca104437c4b9eb1e4174e9"}, ] [package.dependencies] @@ -2052,40 +2068,40 @@ files = [ [[package]] name = "matplotlib" -version = "3.9.1" +version = "3.9.1.post1" description = "Python plotting package" optional = false python-versions = ">=3.9" files = [ - {file = "matplotlib-3.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ccd6270066feb9a9d8e0705aa027f1ff39f354c72a87efe8fa07632f30fc6bb"}, - {file = "matplotlib-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:591d3a88903a30a6d23b040c1e44d1afdd0d778758d07110eb7596f811f31842"}, - {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2a59ff4b83d33bca3b5ec58203cc65985367812cb8c257f3e101632be86d92"}, - {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc001516ffcf1a221beb51198b194d9230199d6842c540108e4ce109ac05cc0"}, - {file = "matplotlib-3.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83c6a792f1465d174c86d06f3ae85a8fe36e6f5964633ae8106312ec0921fdf5"}, - {file = "matplotlib-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:421851f4f57350bcf0811edd754a708d2275533e84f52f6760b740766c6747a7"}, - {file = "matplotlib-3.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b3fce58971b465e01b5c538f9d44915640c20ec5ff31346e963c9e1cd66fa812"}, - {file = "matplotlib-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a973c53ad0668c53e0ed76b27d2eeeae8799836fd0d0caaa4ecc66bf4e6676c0"}, - {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd5acf8f3ef43f7532c2f230249720f5dc5dd40ecafaf1c60ac8200d46d7eb"}, - {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab38a4f3772523179b2f772103d8030215b318fef6360cb40558f585bf3d017f"}, - {file = "matplotlib-3.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2315837485ca6188a4b632c5199900e28d33b481eb083663f6a44cfc8987ded3"}, - {file = "matplotlib-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0c977c5c382f6696caf0bd277ef4f936da7e2aa202ff66cad5f0ac1428ee15b"}, - {file = "matplotlib-3.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:565d572efea2b94f264dd86ef27919515aa6d629252a169b42ce5f570db7f37b"}, - {file = "matplotlib-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d397fd8ccc64af2ec0af1f0efc3bacd745ebfb9d507f3f552e8adb689ed730a"}, - {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26040c8f5121cd1ad712abffcd4b5222a8aec3a0fe40bc8542c94331deb8780d"}, - {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12cb1837cffaac087ad6b44399d5e22b78c729de3cdae4629e252067b705e2b"}, - {file = "matplotlib-3.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0e835c6988edc3d2d08794f73c323cc62483e13df0194719ecb0723b564e0b5c"}, - {file = "matplotlib-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:44a21d922f78ce40435cb35b43dd7d573cf2a30138d5c4b709d19f00e3907fd7"}, - {file = "matplotlib-3.9.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0c584210c755ae921283d21d01f03a49ef46d1afa184134dd0f95b0202ee6f03"}, - {file = "matplotlib-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11fed08f34fa682c2b792942f8902e7aefeed400da71f9e5816bea40a7ce28fe"}, - {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0000354e32efcfd86bda75729716b92f5c2edd5b947200be9881f0a671565c33"}, - {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db17fea0ae3aceb8e9ac69c7e3051bae0b3d083bfec932240f9bf5d0197a049"}, - {file = "matplotlib-3.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:208cbce658b72bf6a8e675058fbbf59f67814057ae78165d8a2f87c45b48d0ff"}, - {file = "matplotlib-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:dc23f48ab630474264276be156d0d7710ac6c5a09648ccdf49fef9200d8cbe80"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3fda72d4d472e2ccd1be0e9ccb6bf0d2eaf635e7f8f51d737ed7e465ac020cb3"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:84b3ba8429935a444f1fdc80ed930babbe06725bcf09fbeb5c8757a2cd74af04"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b918770bf3e07845408716e5bbda17eadfc3fcbd9307dc67f37d6cf834bb3d98"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f1f2e5d29e9435c97ad4c36fb6668e89aee13d48c75893e25cef064675038ac9"}, - {file = "matplotlib-3.9.1.tar.gz", hash = "sha256:de06b19b8db95dd33d0dc17c926c7c9ebed9f572074b6fac4f65068a6814d010"}, + {file = "matplotlib-3.9.1.post1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3779ad3e8b72df22b8a622c5796bbcfabfa0069b835412e3c1dec8ee3de92d0c"}, + {file = "matplotlib-3.9.1.post1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec400340f8628e8e2260d679078d4e9b478699f386e5cc8094e80a1cb0039c7c"}, + {file = "matplotlib-3.9.1.post1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82c18791b8862ea095081f745b81f896b011c5a5091678fb33204fef641476af"}, + {file = "matplotlib-3.9.1.post1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:621a628389c09a6b9f609a238af8e66acecece1cfa12febc5fe4195114ba7446"}, + {file = "matplotlib-3.9.1.post1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9a54734ca761ebb27cd4f0b6c2ede696ab6861052d7d7e7b8f7a6782665115f5"}, + {file = "matplotlib-3.9.1.post1-cp310-cp310-win_amd64.whl", hash = "sha256:0721f93db92311bb514e446842e2b21c004541dcca0281afa495053e017c5458"}, + {file = "matplotlib-3.9.1.post1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b08b46058fe2a31ecb81ef6aa3611f41d871f6a8280e9057cb4016cb3d8e894a"}, + {file = "matplotlib-3.9.1.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:22b344e84fcc574f561b5731f89a7625db8ef80cdbb0026a8ea855a33e3429d1"}, + {file = "matplotlib-3.9.1.post1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b49fee26d64aefa9f061b575f0f7b5fc4663e51f87375c7239efa3d30d908fa"}, + {file = "matplotlib-3.9.1.post1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89eb7e89e2b57856533c5c98f018aa3254fa3789fcd86d5f80077b9034a54c9a"}, + {file = "matplotlib-3.9.1.post1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c06e742bade41fda6176d4c9c78c9ea016e176cd338e62a1686384cb1eb8de41"}, + {file = "matplotlib-3.9.1.post1-cp311-cp311-win_amd64.whl", hash = "sha256:c44edab5b849e0fc1f1c9d6e13eaa35ef65925f7be45be891d9784709ad95561"}, + {file = "matplotlib-3.9.1.post1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bf28b09986aee06393e808e661c3466be9c21eff443c9bc881bce04bfbb0c500"}, + {file = "matplotlib-3.9.1.post1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:92aeb8c439d4831510d8b9d5e39f31c16c7f37873879767c26b147cef61e54cd"}, + {file = "matplotlib-3.9.1.post1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f15798b0691b45c80d3320358a88ce5a9d6f518b28575b3ea3ed31b4bd95d009"}, + {file = "matplotlib-3.9.1.post1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d59fc6096da7b9c1df275f9afc3fef5cbf634c21df9e5f844cba3dd8deb1847d"}, + {file = "matplotlib-3.9.1.post1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ab986817a32a70ce22302438691e7df4c6ee4a844d47289db9d583d873491e0b"}, + {file = "matplotlib-3.9.1.post1-cp312-cp312-win_amd64.whl", hash = "sha256:0d78e7d2d86c4472da105d39aba9b754ed3dfeaeaa4ac7206b82706e0a5362fa"}, + {file = "matplotlib-3.9.1.post1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bd07eba6431b4dc9253cce6374a28c415e1d3a7dc9f8aba028ea7592f06fe172"}, + {file = "matplotlib-3.9.1.post1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ca230cc4482010d646827bd2c6d140c98c361e769ae7d954ebf6fff2a226f5b1"}, + {file = "matplotlib-3.9.1.post1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ace27c0fdeded399cbc43f22ffa76e0f0752358f5b33106ec7197534df08725a"}, + {file = "matplotlib-3.9.1.post1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a4f3aeb7ba14c497dc6f021a076c48c2e5fbdf3da1e7264a5d649683e284a2f"}, + {file = "matplotlib-3.9.1.post1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:23f96fbd4ff4cfa9b8a6b685a65e7eb3c2ced724a8d965995ec5c9c2b1f7daf5"}, + {file = "matplotlib-3.9.1.post1-cp39-cp39-win_amd64.whl", hash = "sha256:2808b95452b4ffa14bfb7c7edffc5350743c31bda495f0d63d10fdd9bc69e895"}, + {file = "matplotlib-3.9.1.post1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ffc91239f73b4179dec256b01299d46d0ffa9d27d98494bc1476a651b7821cbe"}, + {file = "matplotlib-3.9.1.post1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f965ebca9fd4feaaca45937c4849d92b70653057497181100fcd1e18161e5f29"}, + {file = "matplotlib-3.9.1.post1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801ee9323fd7b2da0d405aebbf98d1da77ea430bbbbbec6834c0b3af15e5db44"}, + {file = "matplotlib-3.9.1.post1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:50113e9b43ceb285739f35d43db36aa752fb8154325b35d134ff6e177452f9ec"}, + {file = "matplotlib-3.9.1.post1.tar.gz", hash = "sha256:c91e585c65092c975a44dc9d4239ba8c594ba3c193d7c478b6d178c4ef61f406"}, ] [package.dependencies] @@ -2264,13 +2280,13 @@ pygments = ">2.12.0" [[package]] name = "mkdocs-material" -version = "9.5.28" +version = "9.5.31" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.28-py3-none-any.whl", hash = "sha256:ff48b11b2a9f705dd210409ec3b418ab443dd36d96915bcba45a41f10ea27bfd"}, - {file = "mkdocs_material-9.5.28.tar.gz", hash = "sha256:9cba305283ad1600e3d0a67abe72d7a058b54793b47be39930911a588fe0336b"}, + {file = "mkdocs_material-9.5.31-py3-none-any.whl", hash = "sha256:1b1f49066fdb3824c1e96d6bacd2d4375de4ac74580b47e79ff44c4d835c5fcb"}, + {file = "mkdocs_material-9.5.31.tar.gz", hash = "sha256:31833ec664772669f5856f4f276bf3fdf0e642a445e64491eda459249c3a1ca8"}, ] [package.dependencies] @@ -2304,13 +2320,13 @@ files = [ [[package]] name = "mkdocstrings" -version = "0.25.1" +version = "0.25.2" description = "Automatic documentation from sources, for MkDocs." optional = false python-versions = ">=3.8" files = [ - {file = "mkdocstrings-0.25.1-py3-none-any.whl", hash = "sha256:da01fcc2670ad61888e8fe5b60afe9fee5781017d67431996832d63e887c2e51"}, - {file = "mkdocstrings-0.25.1.tar.gz", hash = "sha256:c3a2515f31577f311a9ee58d089e4c51fc6046dbd9e9b4c3de4c3194667fe9bf"}, + {file = "mkdocstrings-0.25.2-py3-none-any.whl", hash = "sha256:9e2cda5e2e12db8bb98d21e3410f3f27f8faab685a24b03b06ba7daa5b92abfc"}, + {file = "mkdocstrings-0.25.2.tar.gz", hash = "sha256:5cf57ad7f61e8be3111a2458b4e49c2029c9cb35525393b179f9c916ca8042dc"}, ] [package.dependencies] @@ -2330,17 +2346,17 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "1.10.5" +version = "1.10.7" description = "A Python handler for mkdocstrings." optional = false python-versions = ">=3.8" files = [ - {file = "mkdocstrings_python-1.10.5-py3-none-any.whl", hash = "sha256:92e3c588ef1b41151f55281d075de7558dd8092e422cb07a65b18ee2b0863ebb"}, - {file = "mkdocstrings_python-1.10.5.tar.gz", hash = "sha256:acdc2a98cd9d46c7ece508193a16ca03ccabcb67520352b7449f84b57c162bdf"}, + {file = "mkdocstrings_python-1.10.7-py3-none-any.whl", hash = "sha256:8999acb8e2cb6ae5edb844ce1ed6a5fcc14285f85cfd9df374d9a0f0be8a40b6"}, + {file = "mkdocstrings_python-1.10.7.tar.gz", hash = "sha256:bfb5e29acfc69c9177d2b11c18d3127d16e553b8da9bb6d184e428d54795600b"}, ] [package.dependencies] -griffe = ">=0.47" +griffe = ">=0.48" mkdocstrings = ">=0.25" [[package]] @@ -2496,24 +2512,29 @@ name = "neuralspot-edge" version = "0.1.3" description = "" optional = false -python-versions = "<3.13,>=3.11" -files = [ - {file = "neuralspot_edge-0.1.3-py3-none-any.whl", hash = "sha256:c346da7d3d2e6dca9bd221a4c3ddcef1ca0aa4a8adb21995284cf394b5a110bb"}, - {file = "neuralspot_edge-0.1.3.tar.gz", hash = "sha256:4fc082a0b956fd604616a4933dd46f84e04161a8260eaf014ddb6ccb4db6bedd"}, -] +python-versions = ">=3.11,<3.13" +files = [] +develop = false [package.dependencies] -h5py = ">=3.10.0,<4.0.0" -keras = ">=3.0.4,<4.0.0" -matplotlib = ">=3.9.0,<4.0.0" -pandas = ">=2.2.2,<3.0.0" -plotly = ">=5.22.0,<6.0.0" -pydantic = ">=2.6.1,<3.0.0" -requests = ">=2.31.0,<3.0.0" -scikit-learn = ">=1.5.1,<2.0.0" -seaborn = ">=0.13.2,<0.14.0" -tensorflow = ">=2.16.1,<3.0.0" -tqdm = ">=4.66.4,<5.0.0" +boto3 = "^1.34.151" +h5py = "^3.10.0" +keras = "^3.0.4" +matplotlib = "^3.9.0" +pandas = "^2.2.2" +plotly = "^5.22.0" +pydantic = "^2.6.1" +requests = "^2.31.0" +scikit-learn = "^1.5.1" +seaborn = "^0.13.2" +tensorflow = "^2.16.1" +tqdm = "^4.66.4" + +[package.source] +type = "git" +url = "https://github.com/AmbiqAI/neuralspot-edge.git" +reference = "HEAD" +resolved_reference = "1dfc7f7343b4a084d3912fc9c1daee33d64330b9" [[package]] name = "nodeenv" @@ -2720,62 +2741,68 @@ torch = ["torch"] [[package]] name = "orjson" -version = "3.10.6" +version = "3.10.7" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eadc8fd310edb4bdbd333374f2c8fec6794bbbae99b592f448d8214a5e4050c0"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61272a5aec2b2661f4fa2b37c907ce9701e821b2c1285d5c3ab0207ebd358d38"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57985ee7e91d6214c837936dc1608f40f330a6b88bb13f5a57ce5257807da143"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:633a3b31d9d7c9f02d49c4ab4d0a86065c4a6f6adc297d63d272e043472acab5"}, - {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1c680b269d33ec444afe2bdc647c9eb73166fa47a16d9a75ee56a374f4a45f43"}, - {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f759503a97a6ace19e55461395ab0d618b5a117e8d0fbb20e70cfd68a47327f2"}, - {file = "orjson-3.10.6-cp310-none-win32.whl", hash = "sha256:95a0cce17f969fb5391762e5719575217bd10ac5a189d1979442ee54456393f3"}, - {file = "orjson-3.10.6-cp310-none-win_amd64.whl", hash = "sha256:df25d9271270ba2133cc88ee83c318372bdc0f2cd6f32e7a450809a111efc45c"}, - {file = "orjson-3.10.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b1ec490e10d2a77c345def52599311849fc063ae0e67cf4f84528073152bb2ba"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d43d3feb8f19d07e9f01e5b9be4f28801cf7c60d0fa0d279951b18fae1932b"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3045267e98fe749408eee1593a142e02357c5c99be0802185ef2170086a863"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27bc6a28ae95923350ab382c57113abd38f3928af3c80be6f2ba7eb8d8db0b0"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d27456491ca79532d11e507cadca37fb8c9324a3976294f68fb1eff2dc6ced5a"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05ac3d3916023745aa3b3b388e91b9166be1ca02b7c7e41045da6d12985685f0"}, - {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1335d4ef59ab85cab66fe73fd7a4e881c298ee7f63ede918b7faa1b27cbe5212"}, - {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4bbc6d0af24c1575edc79994c20e1b29e6fb3c6a570371306db0993ecf144dc5"}, - {file = "orjson-3.10.6-cp311-none-win32.whl", hash = "sha256:450e39ab1f7694465060a0550b3f6d328d20297bf2e06aa947b97c21e5241fbd"}, - {file = "orjson-3.10.6-cp311-none-win_amd64.whl", hash = "sha256:227df19441372610b20e05bdb906e1742ec2ad7a66ac8350dcfd29a63014a83b"}, - {file = "orjson-3.10.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ea2977b21f8d5d9b758bb3f344a75e55ca78e3ff85595d248eee813ae23ecdfb"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6f3d167d13a16ed263b52dbfedff52c962bfd3d270b46b7518365bcc2121eed"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f710f346e4c44a4e8bdf23daa974faede58f83334289df80bc9cd12fe82573c7"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7275664f84e027dcb1ad5200b8b18373e9c669b2a9ec33d410c40f5ccf4b257e"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0943e4c701196b23c240b3d10ed8ecd674f03089198cf503105b474a4f77f21f"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:446dee5a491b5bc7d8f825d80d9637e7af43f86a331207b9c9610e2f93fee22a"}, - {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:64c81456d2a050d380786413786b057983892db105516639cb5d3ee3c7fd5148"}, - {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, - {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, - {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, - {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2c116072a8533f2fec435fde4d134610f806bdac20188c7bd2081f3e9e0133f"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6eeb13218c8cf34c61912e9df2de2853f1d009de0e46ea09ccdf3d757896af0a"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965a916373382674e323c957d560b953d81d7a8603fbeee26f7b8248638bd48b"}, - {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03c95484d53ed8e479cade8628c9cea00fd9d67f5554764a1110e0d5aa2de96e"}, - {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e060748a04cccf1e0a6f2358dffea9c080b849a4a68c28b1b907f272b5127e9b"}, - {file = "orjson-3.10.6-cp38-none-win32.whl", hash = "sha256:738dbe3ef909c4b019d69afc19caf6b5ed0e2f1c786b5d6215fbb7539246e4c6"}, - {file = "orjson-3.10.6-cp38-none-win_amd64.whl", hash = "sha256:d40f839dddf6a7d77114fe6b8a70218556408c71d4d6e29413bb5f150a692ff7"}, - {file = "orjson-3.10.6-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:697a35a083c4f834807a6232b3e62c8b280f7a44ad0b759fd4dce748951e70db"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd502f96bf5ea9a61cbc0b2b5900d0dd68aa0da197179042bdd2be67e51a1e4b"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f215789fb1667cdc874c1b8af6a84dc939fd802bf293a8334fce185c79cd359b"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2debd8ddce948a8c0938c8c93ade191d2f4ba4649a54302a7da905a81f00b56"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5410111d7b6681d4b0d65e0f58a13be588d01b473822483f77f513c7f93bd3b2"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1f28a137337fdc18384079fa5726810681055b32b92253fa15ae5656e1dddb"}, - {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bf2fbbce5fe7cd1aa177ea3eab2b8e6a6bc6e8592e4279ed3db2d62e57c0e1b2"}, - {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:79b9b9e33bd4c517445a62b90ca0cc279b0f1f3970655c3df9e608bc3f91741a"}, - {file = "orjson-3.10.6-cp39-none-win32.whl", hash = "sha256:30b0a09a2014e621b1adf66a4f705f0809358350a757508ee80209b2d8dae219"}, - {file = "orjson-3.10.6-cp39-none-win_amd64.whl", hash = "sha256:49e3bc615652617d463069f91b867a4458114c5b104e13b7ae6872e5f79d0844"}, - {file = "orjson-3.10.6.tar.gz", hash = "sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7"}, + {file = "orjson-3.10.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91"}, + {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250"}, + {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84"}, + {file = "orjson-3.10.7-cp310-none-win32.whl", hash = "sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175"}, + {file = "orjson-3.10.7-cp310-none-win_amd64.whl", hash = "sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c"}, + {file = "orjson-3.10.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6"}, + {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6"}, + {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0"}, + {file = "orjson-3.10.7-cp311-none-win32.whl", hash = "sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f"}, + {file = "orjson-3.10.7-cp311-none-win_amd64.whl", hash = "sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5"}, + {file = "orjson-3.10.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09"}, + {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5"}, + {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b"}, + {file = "orjson-3.10.7-cp312-none-win32.whl", hash = "sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb"}, + {file = "orjson-3.10.7-cp312-none-win_amd64.whl", hash = "sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1"}, + {file = "orjson-3.10.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149"}, + {file = "orjson-3.10.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe"}, + {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c"}, + {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad"}, + {file = "orjson-3.10.7-cp313-none-win32.whl", hash = "sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2"}, + {file = "orjson-3.10.7-cp313-none-win_amd64.whl", hash = "sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024"}, + {file = "orjson-3.10.7-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98"}, + {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354"}, + {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866"}, + {file = "orjson-3.10.7-cp38-none-win32.whl", hash = "sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c"}, + {file = "orjson-3.10.7-cp38-none-win_amd64.whl", hash = "sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e"}, + {file = "orjson-3.10.7-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff"}, + {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd"}, + {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5"}, + {file = "orjson-3.10.7-cp39-none-win32.whl", hash = "sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2"}, + {file = "orjson-3.10.7-cp39-none-win_amd64.whl", hash = "sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58"}, + {file = "orjson-3.10.7.tar.gz", hash = "sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3"}, ] [[package]] @@ -3064,13 +3091,13 @@ type = ["mypy (>=1.8)"] [[package]] name = "plotly" -version = "5.22.0" +version = "5.23.0" description = "An open-source, interactive data visualization library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "plotly-5.22.0-py3-none-any.whl", hash = "sha256:68fc1901f098daeb233cc3dd44ec9dc31fb3ca4f4e53189344199c43496ed006"}, - {file = "plotly-5.22.0.tar.gz", hash = "sha256:859fdadbd86b5770ae2466e542b761b247d1c6b49daed765b95bb8c7063e7469"}, + {file = "plotly-5.23.0-py3-none-any.whl", hash = "sha256:76cbe78f75eddc10c56f5a4ee3e7ccaade7c0a57465546f02098c0caed6c2d1a"}, + {file = "plotly-5.23.0.tar.gz", hash = "sha256:89e57d003a116303a34de6700862391367dd564222ab71f8531df70279fc0193"}, ] [package.dependencies] @@ -3094,13 +3121,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "3.7.1" +version = "3.8.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, - {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, ] [package.dependencies] @@ -3140,22 +3167,22 @@ wcwidth = "*" [[package]] name = "protobuf" -version = "4.25.3" +version = "4.25.4" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, - {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, - {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"}, - {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"}, - {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"}, - {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"}, - {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"}, - {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"}, - {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"}, - {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"}, - {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, + {file = "protobuf-4.25.4-cp310-abi3-win32.whl", hash = "sha256:db9fd45183e1a67722cafa5c1da3e85c6492a5383f127c86c4c4aa4845867dc4"}, + {file = "protobuf-4.25.4-cp310-abi3-win_amd64.whl", hash = "sha256:ba3d8504116a921af46499471c63a85260c1a5fc23333154a427a310e015d26d"}, + {file = "protobuf-4.25.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:eecd41bfc0e4b1bd3fa7909ed93dd14dd5567b98c941d6c1ad08fdcab3d6884b"}, + {file = "protobuf-4.25.4-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:4c8a70fdcb995dcf6c8966cfa3a29101916f7225e9afe3ced4395359955d3835"}, + {file = "protobuf-4.25.4-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:3319e073562e2515c6ddc643eb92ce20809f5d8f10fead3332f71c63be6a7040"}, + {file = "protobuf-4.25.4-cp38-cp38-win32.whl", hash = "sha256:7e372cbbda66a63ebca18f8ffaa6948455dfecc4e9c1029312f6c2edcd86c4e1"}, + {file = "protobuf-4.25.4-cp38-cp38-win_amd64.whl", hash = "sha256:051e97ce9fa6067a4546e75cb14f90cf0232dcb3e3d508c448b8d0e4265b61c1"}, + {file = "protobuf-4.25.4-cp39-cp39-win32.whl", hash = "sha256:90bf6fd378494eb698805bbbe7afe6c5d12c8e17fca817a646cd6a1818c696ca"}, + {file = "protobuf-4.25.4-cp39-cp39-win_amd64.whl", hash = "sha256:ac79a48d6b99dfed2729ccccee547b34a1d3d63289c71cef056653a846a2240f"}, + {file = "protobuf-4.25.4-py3-none-any.whl", hash = "sha256:bfbebc1c8e4793cfd58589acfb8a1026be0003e852b9da7db5a4285bde996978"}, + {file = "protobuf-4.25.4.tar.gz", hash = "sha256:0dc4a62cc4052a036ee2204d26fe4d835c62827c855c8a03f29fe6da146b380d"}, ] [[package]] @@ -3199,13 +3226,13 @@ files = [ [[package]] name = "pure-eval" -version = "0.2.2" +version = "0.2.3" description = "Safely evaluate AST nodes without side effects" optional = false python-versions = "*" files = [ - {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, - {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, + {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, + {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, ] [package.extras] @@ -3344,13 +3371,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-settings" -version = "2.3.4" +version = "2.4.0" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_settings-2.3.4-py3-none-any.whl", hash = "sha256:11ad8bacb68a045f00e4f862c7a718c8a9ec766aa8fd4c32e39a0594b207b53a"}, - {file = "pydantic_settings-2.3.4.tar.gz", hash = "sha256:c5802e3d62b78e82522319bbc9b8f8ffb28ad1c988a99311d04f2a6051fca0a7"}, + {file = "pydantic_settings-2.4.0-py3-none-any.whl", hash = "sha256:bb6849dc067f1687574c12a639e231f3a6feeed0a12d710c1382045c5db1c315"}, + {file = "pydantic_settings-2.4.0.tar.gz", hash = "sha256:ed81c3a0f46392b4d7c0a565c05884e6e54b3456e6f0fe4d8814981172dc9a88"}, ] [package.dependencies] @@ -3358,6 +3385,7 @@ pydantic = ">=2.7.0" python-dotenv = ">=0.21.0" [package.extras] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] toml = ["tomli (>=2.0.1)"] yaml = ["pyyaml (>=6.0.1)"] @@ -3396,13 +3424,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.8.1" +version = "10.9" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.8.1-py3-none-any.whl", hash = "sha256:f938326115884f48c6059c67377c46cf631c733ef3629b6eed1349989d1b30cb"}, - {file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"}, + {file = "pymdown_extensions-10.9-py3-none-any.whl", hash = "sha256:d323f7e90d83c86113ee78f3fe62fc9dee5f56b54d912660703ea1816fed5626"}, + {file = "pymdown_extensions-10.9.tar.gz", hash = "sha256:6ff740bcd99ec4172a938970d42b96128bdc9d4b9bcad72494f29921dc69b753"}, ] [package.dependencies] @@ -3442,20 +3470,20 @@ cp2110 = ["hidapi"] [[package]] name = "pytest" -version = "8.2.2" +version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.5,<2.0" +pluggy = ">=1.5,<2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] @@ -3550,62 +3578,64 @@ files = [ [[package]] name = "pyyaml" -version = "6.0.1" +version = "6.0.2" description = "YAML parser and emitter for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] @@ -3624,99 +3654,120 @@ pyyaml = "*" [[package]] name = "pyzmq" -version = "26.0.3" +version = "26.1.0" description = "Python bindings for 0MQ" optional = false python-versions = ">=3.7" files = [ - {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:44dd6fc3034f1eaa72ece33588867df9e006a7303725a12d64c3dff92330f625"}, - {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:acb704195a71ac5ea5ecf2811c9ee19ecdc62b91878528302dd0be1b9451cc90"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbb9c997932473a27afa93954bb77a9f9b786b4ccf718d903f35da3232317de"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bcb34f869d431799c3ee7d516554797f7760cb2198ecaa89c3f176f72d062be"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ece17ec5f20d7d9b442e5174ae9f020365d01ba7c112205a4d59cf19dc38ee"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ba6e5e6588e49139a0979d03a7deb9c734bde647b9a8808f26acf9c547cab1bf"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3bf8b000a4e2967e6dfdd8656cd0757d18c7e5ce3d16339e550bd462f4857e59"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2136f64fbb86451dbbf70223635a468272dd20075f988a102bf8a3f194a411dc"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e8918973fbd34e7814f59143c5f600ecd38b8038161239fd1a3d33d5817a38b8"}, - {file = "pyzmq-26.0.3-cp310-cp310-win32.whl", hash = "sha256:0aaf982e68a7ac284377d051c742610220fd06d330dcd4c4dbb4cdd77c22a537"}, - {file = "pyzmq-26.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:f1a9b7d00fdf60b4039f4455afd031fe85ee8305b019334b72dcf73c567edc47"}, - {file = "pyzmq-26.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:80b12f25d805a919d53efc0a5ad7c0c0326f13b4eae981a5d7b7cc343318ebb7"}, - {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:a72a84570f84c374b4c287183debc776dc319d3e8ce6b6a0041ce2e400de3f32"}, - {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ca684ee649b55fd8f378127ac8462fb6c85f251c2fb027eb3c887e8ee347bcd"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e222562dc0f38571c8b1ffdae9d7adb866363134299264a1958d077800b193b7"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f17cde1db0754c35a91ac00b22b25c11da6eec5746431d6e5092f0cd31a3fea9"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7c0c0b3244bb2275abe255d4a30c050d541c6cb18b870975553f1fb6f37527"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac97a21de3712afe6a6c071abfad40a6224fd14fa6ff0ff8d0c6e6cd4e2f807a"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:88b88282e55fa39dd556d7fc04160bcf39dea015f78e0cecec8ff4f06c1fc2b5"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:72b67f966b57dbd18dcc7efbc1c7fc9f5f983e572db1877081f075004614fcdd"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4b6cecbbf3b7380f3b61de3a7b93cb721125dc125c854c14ddc91225ba52f83"}, - {file = "pyzmq-26.0.3-cp311-cp311-win32.whl", hash = "sha256:eed56b6a39216d31ff8cd2f1d048b5bf1700e4b32a01b14379c3b6dde9ce3aa3"}, - {file = "pyzmq-26.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:3191d312c73e3cfd0f0afdf51df8405aafeb0bad71e7ed8f68b24b63c4f36500"}, - {file = "pyzmq-26.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:b6907da3017ef55139cf0e417c5123a84c7332520e73a6902ff1f79046cd3b94"}, - {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:068ca17214038ae986d68f4a7021f97e187ed278ab6dccb79f837d765a54d753"}, - {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7821d44fe07335bea256b9f1f41474a642ca55fa671dfd9f00af8d68a920c2d4"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb438a26d87c123bb318e5f2b3d86a36060b01f22fbdffd8cf247d52f7c9a2b"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69ea9d6d9baa25a4dc9cef5e2b77b8537827b122214f210dd925132e34ae9b12"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7daa3e1369355766dea11f1d8ef829905c3b9da886ea3152788dc25ee6079e02"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6ca7a9a06b52d0e38ccf6bca1aeff7be178917893f3883f37b75589d42c4ac20"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1b7d0e124948daa4d9686d421ef5087c0516bc6179fdcf8828b8444f8e461a77"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e746524418b70f38550f2190eeee834db8850088c834d4c8406fbb9bc1ae10b2"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6b3146f9ae6af82c47a5282ac8803523d381b3b21caeae0327ed2f7ecb718798"}, - {file = "pyzmq-26.0.3-cp312-cp312-win32.whl", hash = "sha256:2b291d1230845871c00c8462c50565a9cd6026fe1228e77ca934470bb7d70ea0"}, - {file = "pyzmq-26.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:926838a535c2c1ea21c903f909a9a54e675c2126728c21381a94ddf37c3cbddf"}, - {file = "pyzmq-26.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:5bf6c237f8c681dfb91b17f8435b2735951f0d1fad10cc5dfd96db110243370b"}, - {file = "pyzmq-26.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c0991f5a96a8e620f7691e61178cd8f457b49e17b7d9cfa2067e2a0a89fc1d5"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dbf012d8fcb9f2cf0643b65df3b355fdd74fc0035d70bb5c845e9e30a3a4654b"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:01fbfbeb8249a68d257f601deb50c70c929dc2dfe683b754659569e502fbd3aa"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c8eb19abe87029c18f226d42b8a2c9efdd139d08f8bf6e085dd9075446db450"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5344b896e79800af86ad643408ca9aa303a017f6ebff8cee5a3163c1e9aec987"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:204e0f176fd1d067671157d049466869b3ae1fc51e354708b0dc41cf94e23a3a"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a42db008d58530efa3b881eeee4991146de0b790e095f7ae43ba5cc612decbc5"}, - {file = "pyzmq-26.0.3-cp37-cp37m-win32.whl", hash = "sha256:8d7a498671ca87e32b54cb47c82a92b40130a26c5197d392720a1bce1b3c77cf"}, - {file = "pyzmq-26.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:3b4032a96410bdc760061b14ed6a33613ffb7f702181ba999df5d16fb96ba16a"}, - {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2cc4e280098c1b192c42a849de8de2c8e0f3a84086a76ec5b07bfee29bda7d18"}, - {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bde86a2ed3ce587fa2b207424ce15b9a83a9fa14422dcc1c5356a13aed3df9d"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34106f68e20e6ff253c9f596ea50397dbd8699828d55e8fa18bd4323d8d966e6"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ebbbd0e728af5db9b04e56389e2299a57ea8b9dd15c9759153ee2455b32be6ad"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b1d1c631e5940cac5a0b22c5379c86e8df6a4ec277c7a856b714021ab6cfad"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e891ce81edd463b3b4c3b885c5603c00141151dd9c6936d98a680c8c72fe5c67"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9b273ecfbc590a1b98f014ae41e5cf723932f3b53ba9367cfb676f838038b32c"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b32bff85fb02a75ea0b68f21e2412255b5731f3f389ed9aecc13a6752f58ac97"}, - {file = "pyzmq-26.0.3-cp38-cp38-win32.whl", hash = "sha256:f6c21c00478a7bea93caaaef9e7629145d4153b15a8653e8bb4609d4bc70dbfc"}, - {file = "pyzmq-26.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:3401613148d93ef0fd9aabdbddb212de3db7a4475367f49f590c837355343972"}, - {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:2ed8357f4c6e0daa4f3baf31832df8a33334e0fe5b020a61bc8b345a3db7a606"}, - {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1c8f2a2ca45292084c75bb6d3a25545cff0ed931ed228d3a1810ae3758f975f"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b63731993cdddcc8e087c64e9cf003f909262b359110070183d7f3025d1c56b5"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b3cd31f859b662ac5d7f4226ec7d8bd60384fa037fc02aee6ff0b53ba29a3ba8"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115f8359402fa527cf47708d6f8a0f8234f0e9ca0cab7c18c9c189c194dbf620"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:715bdf952b9533ba13dfcf1f431a8f49e63cecc31d91d007bc1deb914f47d0e4"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e1258c639e00bf5e8a522fec6c3eaa3e30cf1c23a2f21a586be7e04d50c9acab"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15c59e780be8f30a60816a9adab900c12a58d79c1ac742b4a8df044ab2a6d920"}, - {file = "pyzmq-26.0.3-cp39-cp39-win32.whl", hash = "sha256:d0cdde3c78d8ab5b46595054e5def32a755fc028685add5ddc7403e9f6de9879"}, - {file = "pyzmq-26.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ce828058d482ef860746bf532822842e0ff484e27f540ef5c813d516dd8896d2"}, - {file = "pyzmq-26.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:788f15721c64109cf720791714dc14afd0f449d63f3a5487724f024345067381"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c18645ef6294d99b256806e34653e86236eb266278c8ec8112622b61db255de"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e6bc96ebe49604df3ec2c6389cc3876cabe475e6bfc84ced1bf4e630662cb35"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:971e8990c5cc4ddcff26e149398fc7b0f6a042306e82500f5e8db3b10ce69f84"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8416c23161abd94cc7da80c734ad7c9f5dbebdadfdaa77dad78244457448223"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:082a2988364b60bb5de809373098361cf1dbb239623e39e46cb18bc035ed9c0c"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d57dfbf9737763b3a60d26e6800e02e04284926329aee8fb01049635e957fe81"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77a85dca4c2430ac04dc2a2185c2deb3858a34fe7f403d0a946fa56970cf60a1"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4c82a6d952a1d555bf4be42b6532927d2a5686dd3c3e280e5f63225ab47ac1f5"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4496b1282c70c442809fc1b151977c3d967bfb33e4e17cedbf226d97de18f709"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e4946d6bdb7ba972dfda282f9127e5756d4f299028b1566d1245fa0d438847e6"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03c0ae165e700364b266876d712acb1ac02693acd920afa67da2ebb91a0b3c09"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3e3070e680f79887d60feeda051a58d0ac36622e1759f305a41059eff62c6da7"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6ca08b840fe95d1c2bd9ab92dac5685f949fc6f9ae820ec16193e5ddf603c3b2"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e76654e9dbfb835b3518f9938e565c7806976c07b37c33526b574cc1a1050480"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:871587bdadd1075b112e697173e946a07d722459d20716ceb3d1bd6c64bd08ce"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d0a2d1bd63a4ad79483049b26514e70fa618ce6115220da9efdff63688808b17"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0270b49b6847f0d106d64b5086e9ad5dc8a902413b5dbbb15d12b60f9c1747a4"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:703c60b9910488d3d0954ca585c34f541e506a091a41930e663a098d3b794c67"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74423631b6be371edfbf7eabb02ab995c2563fee60a80a30829176842e71722a"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4adfbb5451196842a88fda3612e2c0414134874bffb1c2ce83ab4242ec9e027d"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3516119f4f9b8671083a70b6afaa0a070f5683e431ab3dc26e9215620d7ca1ad"}, - {file = "pyzmq-26.0.3.tar.gz", hash = "sha256:dba7d9f2e047dfa2bca3b01f4f84aa5246725203d6284e3790f2ca15fba6b40a"}, + {file = "pyzmq-26.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:263cf1e36862310bf5becfbc488e18d5d698941858860c5a8c079d1511b3b18e"}, + {file = "pyzmq-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d5c8b17f6e8f29138678834cf8518049e740385eb2dbf736e8f07fc6587ec682"}, + {file = "pyzmq-26.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a95c2358fcfdef3374cb8baf57f1064d73246d55e41683aaffb6cfe6862917"}, + {file = "pyzmq-26.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f99de52b8fbdb2a8f5301ae5fc0f9e6b3ba30d1d5fc0421956967edcc6914242"}, + {file = "pyzmq-26.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bcbfbab4e1895d58ab7da1b5ce9a327764f0366911ba5b95406c9104bceacb0"}, + {file = "pyzmq-26.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:77ce6a332c7e362cb59b63f5edf730e83590d0ab4e59c2aa5bd79419a42e3449"}, + {file = "pyzmq-26.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ba0a31d00e8616149a5ab440d058ec2da621e05d744914774c4dde6837e1f545"}, + {file = "pyzmq-26.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8b88641384e84a258b740801cd4dbc45c75f148ee674bec3149999adda4a8598"}, + {file = "pyzmq-26.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2fa76ebcebe555cce90f16246edc3ad83ab65bb7b3d4ce408cf6bc67740c4f88"}, + {file = "pyzmq-26.1.0-cp310-cp310-win32.whl", hash = "sha256:fbf558551cf415586e91160d69ca6416f3fce0b86175b64e4293644a7416b81b"}, + {file = "pyzmq-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a7b8aab50e5a288c9724d260feae25eda69582be84e97c012c80e1a5e7e03fb2"}, + {file = "pyzmq-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:08f74904cb066e1178c1ec706dfdb5c6c680cd7a8ed9efebeac923d84c1f13b1"}, + {file = "pyzmq-26.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:46d6800b45015f96b9d92ece229d92f2aef137d82906577d55fadeb9cf5fcb71"}, + {file = "pyzmq-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5bc2431167adc50ba42ea3e5e5f5cd70d93e18ab7b2f95e724dd8e1bd2c38120"}, + {file = "pyzmq-26.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3bb34bebaa1b78e562931a1687ff663d298013f78f972a534f36c523311a84d"}, + {file = "pyzmq-26.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3f6329340cef1c7ba9611bd038f2d523cea79f09f9c8f6b0553caba59ec562"}, + {file = "pyzmq-26.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:471880c4c14e5a056a96cd224f5e71211997d40b4bf5e9fdded55dafab1f98f2"}, + {file = "pyzmq-26.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ce6f2b66799971cbae5d6547acefa7231458289e0ad481d0be0740535da38d8b"}, + {file = "pyzmq-26.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a1f6ea5b1d6cdbb8cfa0536f0d470f12b4b41ad83625012e575f0e3ecfe97f0"}, + {file = "pyzmq-26.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b45e6445ac95ecb7d728604bae6538f40ccf4449b132b5428c09918523abc96d"}, + {file = "pyzmq-26.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:94c4262626424683feea0f3c34951d39d49d354722db2745c42aa6bb50ecd93b"}, + {file = "pyzmq-26.1.0-cp311-cp311-win32.whl", hash = "sha256:a0f0ab9df66eb34d58205913f4540e2ad17a175b05d81b0b7197bc57d000e829"}, + {file = "pyzmq-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:8efb782f5a6c450589dbab4cb0f66f3a9026286333fe8f3a084399149af52f29"}, + {file = "pyzmq-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f133d05aaf623519f45e16ab77526e1e70d4e1308e084c2fb4cedb1a0c764bbb"}, + {file = "pyzmq-26.1.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:3d3146b1c3dcc8a1539e7cc094700b2be1e605a76f7c8f0979b6d3bde5ad4072"}, + {file = "pyzmq-26.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d9270fbf038bf34ffca4855bcda6e082e2c7f906b9eb8d9a8ce82691166060f7"}, + {file = "pyzmq-26.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:995301f6740a421afc863a713fe62c0aaf564708d4aa057dfdf0f0f56525294b"}, + {file = "pyzmq-26.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7eca8b89e56fb8c6c26dd3e09bd41b24789022acf1cf13358e96f1cafd8cae3"}, + {file = "pyzmq-26.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d4feb2e83dfe9ace6374a847e98ee9d1246ebadcc0cb765482e272c34e5820"}, + {file = "pyzmq-26.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d4fafc2eb5d83f4647331267808c7e0c5722c25a729a614dc2b90479cafa78bd"}, + {file = "pyzmq-26.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:58c33dc0e185dd97a9ac0288b3188d1be12b756eda67490e6ed6a75cf9491d79"}, + {file = "pyzmq-26.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:68a0a1d83d33d8367ddddb3e6bb4afbb0f92bd1dac2c72cd5e5ddc86bdafd3eb"}, + {file = "pyzmq-26.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ae7c57e22ad881af78075e0cea10a4c778e67234adc65c404391b417a4dda83"}, + {file = "pyzmq-26.1.0-cp312-cp312-win32.whl", hash = "sha256:347e84fc88cc4cb646597f6d3a7ea0998f887ee8dc31c08587e9c3fd7b5ccef3"}, + {file = "pyzmq-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:9f136a6e964830230912f75b5a116a21fe8e34128dcfd82285aa0ef07cb2c7bd"}, + {file = "pyzmq-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:a4b7a989c8f5a72ab1b2bbfa58105578753ae77b71ba33e7383a31ff75a504c4"}, + {file = "pyzmq-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d416f2088ac8f12daacffbc2e8918ef4d6be8568e9d7155c83b7cebed49d2322"}, + {file = "pyzmq-26.1.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:ecb6c88d7946166d783a635efc89f9a1ff11c33d680a20df9657b6902a1d133b"}, + {file = "pyzmq-26.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:471312a7375571857a089342beccc1a63584315188560c7c0da7e0a23afd8a5c"}, + {file = "pyzmq-26.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e6cea102ffa16b737d11932c426f1dc14b5938cf7bc12e17269559c458ac334"}, + {file = "pyzmq-26.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec7248673ffc7104b54e4957cee38b2f3075a13442348c8d651777bf41aa45ee"}, + {file = "pyzmq-26.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0614aed6f87d550b5cecb03d795f4ddbb1544b78d02a4bd5eecf644ec98a39f6"}, + {file = "pyzmq-26.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:e8746ce968be22a8a1801bf4a23e565f9687088580c3ed07af5846580dd97f76"}, + {file = "pyzmq-26.1.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:7688653574392d2eaeef75ddcd0b2de5b232d8730af29af56c5adf1df9ef8d6f"}, + {file = "pyzmq-26.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8d4dac7d97f15c653a5fedcafa82626bd6cee1450ccdaf84ffed7ea14f2b07a4"}, + {file = "pyzmq-26.1.0-cp313-cp313-win32.whl", hash = "sha256:ccb42ca0a4a46232d716779421bbebbcad23c08d37c980f02cc3a6bd115ad277"}, + {file = "pyzmq-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e1e5d0a25aea8b691a00d6b54b28ac514c8cc0d8646d05f7ca6cb64b97358250"}, + {file = "pyzmq-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:fc82269d24860cfa859b676d18850cbb8e312dcd7eada09e7d5b007e2f3d9eb1"}, + {file = "pyzmq-26.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:416ac51cabd54f587995c2b05421324700b22e98d3d0aa2cfaec985524d16f1d"}, + {file = "pyzmq-26.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:ff832cce719edd11266ca32bc74a626b814fff236824aa1aeaad399b69fe6eae"}, + {file = "pyzmq-26.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:393daac1bcf81b2a23e696b7b638eedc965e9e3d2112961a072b6cd8179ad2eb"}, + {file = "pyzmq-26.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9869fa984c8670c8ab899a719eb7b516860a29bc26300a84d24d8c1b71eae3ec"}, + {file = "pyzmq-26.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b3b8e36fd4c32c0825b4461372949ecd1585d326802b1321f8b6dc1d7e9318c"}, + {file = "pyzmq-26.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3ee647d84b83509b7271457bb428cc347037f437ead4b0b6e43b5eba35fec0aa"}, + {file = "pyzmq-26.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:45cb1a70eb00405ce3893041099655265fabcd9c4e1e50c330026e82257892c1"}, + {file = "pyzmq-26.1.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:5cca7b4adb86d7470e0fc96037771981d740f0b4cb99776d5cb59cd0e6684a73"}, + {file = "pyzmq-26.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:91d1a20bdaf3b25f3173ff44e54b1cfbc05f94c9e8133314eb2962a89e05d6e3"}, + {file = "pyzmq-26.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c0665d85535192098420428c779361b8823d3d7ec4848c6af3abb93bc5c915bf"}, + {file = "pyzmq-26.1.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:96d7c1d35ee4a495df56c50c83df7af1c9688cce2e9e0edffdbf50889c167595"}, + {file = "pyzmq-26.1.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b281b5ff5fcc9dcbfe941ac5c7fcd4b6c065adad12d850f95c9d6f23c2652384"}, + {file = "pyzmq-26.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5384c527a9a004445c5074f1e20db83086c8ff1682a626676229aafd9cf9f7d1"}, + {file = "pyzmq-26.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:754c99a9840839375ee251b38ac5964c0f369306eddb56804a073b6efdc0cd88"}, + {file = "pyzmq-26.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9bdfcb74b469b592972ed881bad57d22e2c0acc89f5e8c146782d0d90fb9f4bf"}, + {file = "pyzmq-26.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bd13f0231f4788db619347b971ca5f319c5b7ebee151afc7c14632068c6261d3"}, + {file = "pyzmq-26.1.0-cp37-cp37m-win32.whl", hash = "sha256:c5668dac86a869349828db5fc928ee3f58d450dce2c85607067d581f745e4fb1"}, + {file = "pyzmq-26.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad875277844cfaeca7fe299ddf8c8d8bfe271c3dc1caf14d454faa5cdbf2fa7a"}, + {file = "pyzmq-26.1.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:65c6e03cc0222eaf6aad57ff4ecc0a070451e23232bb48db4322cc45602cede0"}, + {file = "pyzmq-26.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:038ae4ffb63e3991f386e7fda85a9baab7d6617fe85b74a8f9cab190d73adb2b"}, + {file = "pyzmq-26.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bdeb2c61611293f64ac1073f4bf6723b67d291905308a7de9bb2ca87464e3273"}, + {file = "pyzmq-26.1.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:61dfa5ee9d7df297c859ac82b1226d8fefaf9c5113dc25c2c00ecad6feeeb04f"}, + {file = "pyzmq-26.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3292d384537b9918010769b82ab3e79fca8b23d74f56fc69a679106a3e2c2cf"}, + {file = "pyzmq-26.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f9499c70c19ff0fbe1007043acb5ad15c1dec7d8e84ab429bca8c87138e8f85c"}, + {file = "pyzmq-26.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d3dd5523ed258ad58fed7e364c92a9360d1af8a9371e0822bd0146bdf017ef4c"}, + {file = "pyzmq-26.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baba2fd199b098c5544ef2536b2499d2e2155392973ad32687024bd8572a7d1c"}, + {file = "pyzmq-26.1.0-cp38-cp38-win32.whl", hash = "sha256:ddbb2b386128d8eca92bd9ca74e80f73fe263bcca7aa419f5b4cbc1661e19741"}, + {file = "pyzmq-26.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:79e45a4096ec8388cdeb04a9fa5e9371583bcb826964d55b8b66cbffe7b33c86"}, + {file = "pyzmq-26.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:add52c78a12196bc0fda2de087ba6c876ea677cbda2e3eba63546b26e8bf177b"}, + {file = "pyzmq-26.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c03bd7f3339ff47de7ea9ac94a2b34580a8d4df69b50128bb6669e1191a895"}, + {file = "pyzmq-26.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dcc37d9d708784726fafc9c5e1232de655a009dbf97946f117aefa38d5985a0f"}, + {file = "pyzmq-26.1.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5a6ed52f0b9bf8dcc64cc82cce0607a3dfed1dbb7e8c6f282adfccc7be9781de"}, + {file = "pyzmq-26.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451e16ae8bea3d95649317b463c9f95cd9022641ec884e3d63fc67841ae86dfe"}, + {file = "pyzmq-26.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:906e532c814e1d579138177a00ae835cd6becbf104d45ed9093a3aaf658f6a6a"}, + {file = "pyzmq-26.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05bacc4f94af468cc82808ae3293390278d5f3375bb20fef21e2034bb9a505b6"}, + {file = "pyzmq-26.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:57bb2acba798dc3740e913ffadd56b1fcef96f111e66f09e2a8db3050f1f12c8"}, + {file = "pyzmq-26.1.0-cp39-cp39-win32.whl", hash = "sha256:f774841bb0e8588505002962c02da420bcfb4c5056e87a139c6e45e745c0e2e2"}, + {file = "pyzmq-26.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:359c533bedc62c56415a1f5fcfd8279bc93453afdb0803307375ecf81c962402"}, + {file = "pyzmq-26.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:7907419d150b19962138ecec81a17d4892ea440c184949dc29b358bc730caf69"}, + {file = "pyzmq-26.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b24079a14c9596846bf7516fe75d1e2188d4a528364494859106a33d8b48be38"}, + {file = "pyzmq-26.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59d0acd2976e1064f1b398a00e2c3e77ed0a157529779e23087d4c2fb8aaa416"}, + {file = "pyzmq-26.1.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:911c43a4117915203c4cc8755e0f888e16c4676a82f61caee2f21b0c00e5b894"}, + {file = "pyzmq-26.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10163e586cc609f5f85c9b233195554d77b1e9a0801388907441aaeb22841c5"}, + {file = "pyzmq-26.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:28a8b2abb76042f5fd7bd720f7fea48c0fd3e82e9de0a1bf2c0de3812ce44a42"}, + {file = "pyzmq-26.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bef24d3e4ae2c985034439f449e3f9e06bf579974ce0e53d8a507a1577d5b2ab"}, + {file = "pyzmq-26.1.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2cd0f4d314f4a2518e8970b6f299ae18cff7c44d4a1fc06fc713f791c3a9e3ea"}, + {file = "pyzmq-26.1.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fa25a620eed2a419acc2cf10135b995f8f0ce78ad00534d729aa761e4adcef8a"}, + {file = "pyzmq-26.1.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef3b048822dca6d231d8a8ba21069844ae38f5d83889b9b690bf17d2acc7d099"}, + {file = "pyzmq-26.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:9a6847c92d9851b59b9f33f968c68e9e441f9a0f8fc972c5580c5cd7cbc6ee24"}, + {file = "pyzmq-26.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9b9305004d7e4e6a824f4f19b6d8f32b3578aad6f19fc1122aaf320cbe3dc83"}, + {file = "pyzmq-26.1.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:63c1d3a65acb2f9c92dce03c4e1758cc552f1ae5c78d79a44e3bb88d2fa71f3a"}, + {file = "pyzmq-26.1.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d36b8fffe8b248a1b961c86fbdfa0129dfce878731d169ede7fa2631447331be"}, + {file = "pyzmq-26.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67976d12ebfd61a3bc7d77b71a9589b4d61d0422282596cf58c62c3866916544"}, + {file = "pyzmq-26.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:998444debc8816b5d8d15f966e42751032d0f4c55300c48cc337f2b3e4f17d03"}, + {file = "pyzmq-26.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5c88b2f13bcf55fee78ea83567b9fe079ba1a4bef8b35c376043440040f7edb"}, + {file = "pyzmq-26.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d906d43e1592be4b25a587b7d96527cb67277542a5611e8ea9e996182fae410"}, + {file = "pyzmq-26.1.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80b0c9942430d731c786545da6be96d824a41a51742e3e374fedd9018ea43106"}, + {file = "pyzmq-26.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:314d11564c00b77f6224d12eb3ddebe926c301e86b648a1835c5b28176c83eab"}, + {file = "pyzmq-26.1.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:093a1a3cae2496233f14b57f4b485da01b4ff764582c854c0f42c6dd2be37f3d"}, + {file = "pyzmq-26.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3c397b1b450f749a7e974d74c06d69bd22dd362142f370ef2bd32a684d6b480c"}, + {file = "pyzmq-26.1.0.tar.gz", hash = "sha256:6c5aeea71f018ebd3b9115c7cb13863dd850e98ca6b9258509de1246461a7e7f"}, ] [package.dependencies] @@ -3739,90 +3790,90 @@ rpds-py = ">=0.7.0" [[package]] name = "regex" -version = "2024.5.15" +version = "2024.7.24" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" files = [ - {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f"}, - {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6"}, - {file = "regex-2024.5.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0bd000c6e266927cb7a1bc39d55be95c4b4f65c5be53e659537537e019232b1"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eaa7ddaf517aa095fa8da0b5015c44d03da83f5bd49c87961e3c997daed0de7"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba68168daedb2c0bab7fd7e00ced5ba90aebf91024dea3c88ad5063c2a562cca"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e8d717bca3a6e2064fc3a08df5cbe366369f4b052dcd21b7416e6d71620dca1"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1337b7dbef9b2f71121cdbf1e97e40de33ff114801263b275aafd75303bd62b5"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9ebd0a36102fcad2f03696e8af4ae682793a5d30b46c647eaf280d6cfb32796"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9efa1a32ad3a3ea112224897cdaeb6aa00381627f567179c0314f7b65d354c62"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1595f2d10dff3d805e054ebdc41c124753631b6a471b976963c7b28543cf13b0"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b802512f3e1f480f41ab5f2cfc0e2f761f08a1f41092d6718868082fc0d27143"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a0981022dccabca811e8171f913de05720590c915b033b7e601f35ce4ea7019f"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:19068a6a79cf99a19ccefa44610491e9ca02c2be3305c7760d3831d38a467a6f"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b5269484f6126eee5e687785e83c6b60aad7663dafe842b34691157e5083e53"}, - {file = "regex-2024.5.15-cp310-cp310-win32.whl", hash = "sha256:ada150c5adfa8fbcbf321c30c751dc67d2f12f15bd183ffe4ec7cde351d945b3"}, - {file = "regex-2024.5.15-cp310-cp310-win_amd64.whl", hash = "sha256:ac394ff680fc46b97487941f5e6ae49a9f30ea41c6c6804832063f14b2a5a145"}, - {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f5b1dff3ad008dccf18e652283f5e5339d70bf8ba7c98bf848ac33db10f7bc7a"}, - {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6a2b494a76983df8e3d3feea9b9ffdd558b247e60b92f877f93a1ff43d26656"}, - {file = "regex-2024.5.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a32b96f15c8ab2e7d27655969a23895eb799de3665fa94349f3b2fbfd547236f"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10002e86e6068d9e1c91eae8295ef690f02f913c57db120b58fdd35a6bb1af35"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec54d5afa89c19c6dd8541a133be51ee1017a38b412b1321ccb8d6ddbeb4cf7d"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10e4ce0dca9ae7a66e6089bb29355d4432caed736acae36fef0fdd7879f0b0cb"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e507ff1e74373c4d3038195fdd2af30d297b4f0950eeda6f515ae3d84a1770f"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1f059a4d795e646e1c37665b9d06062c62d0e8cc3c511fe01315973a6542e40"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0721931ad5fe0dda45d07f9820b90b2148ccdd8e45bb9e9b42a146cb4f695649"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:833616ddc75ad595dee848ad984d067f2f31be645d603e4d158bba656bbf516c"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:287eb7f54fc81546346207c533ad3c2c51a8d61075127d7f6d79aaf96cdee890"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:19dfb1c504781a136a80ecd1fff9f16dddf5bb43cec6871778c8a907a085bb3d"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:119af6e56dce35e8dfb5222573b50c89e5508d94d55713c75126b753f834de68"}, - {file = "regex-2024.5.15-cp311-cp311-win32.whl", hash = "sha256:1c1c174d6ec38d6c8a7504087358ce9213d4332f6293a94fbf5249992ba54efa"}, - {file = "regex-2024.5.15-cp311-cp311-win_amd64.whl", hash = "sha256:9e717956dcfd656f5055cc70996ee2cc82ac5149517fc8e1b60261b907740201"}, - {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:632b01153e5248c134007209b5c6348a544ce96c46005d8456de1d552455b014"}, - {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e64198f6b856d48192bf921421fdd8ad8eb35e179086e99e99f711957ffedd6e"}, - {file = "regex-2024.5.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68811ab14087b2f6e0fc0c2bae9ad689ea3584cad6917fc57be6a48bbd012c49"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ec0c2fea1e886a19c3bee0cd19d862b3aa75dcdfb42ebe8ed30708df64687a"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0c0c0003c10f54a591d220997dd27d953cd9ccc1a7294b40a4be5312be8797b"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2431b9e263af1953c55abbd3e2efca67ca80a3de8a0437cb58e2421f8184717a"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a605586358893b483976cffc1723fb0f83e526e8f14c6e6614e75919d9862cf"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391d7f7f1e409d192dba8bcd42d3e4cf9e598f3979cdaed6ab11288da88cb9f2"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9ff11639a8d98969c863d4617595eb5425fd12f7c5ef6621a4b74b71ed8726d5"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4eee78a04e6c67e8391edd4dad3279828dd66ac4b79570ec998e2155d2e59fd5"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8fe45aa3f4aa57faabbc9cb46a93363edd6197cbc43523daea044e9ff2fea83e"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d0a3d8d6acf0c78a1fff0e210d224b821081330b8524e3e2bc5a68ef6ab5803d"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c486b4106066d502495b3025a0a7251bf37ea9540433940a23419461ab9f2a80"}, - {file = "regex-2024.5.15-cp312-cp312-win32.whl", hash = "sha256:c49e15eac7c149f3670b3e27f1f28a2c1ddeccd3a2812cba953e01be2ab9b5fe"}, - {file = "regex-2024.5.15-cp312-cp312-win_amd64.whl", hash = "sha256:673b5a6da4557b975c6c90198588181029c60793835ce02f497ea817ff647cb2"}, - {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87e2a9c29e672fc65523fb47a90d429b70ef72b901b4e4b1bd42387caf0d6835"}, - {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3bea0ba8b73b71b37ac833a7f3fd53825924165da6a924aec78c13032f20850"}, - {file = "regex-2024.5.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfc4f82cabe54f1e7f206fd3d30fda143f84a63fe7d64a81558d6e5f2e5aaba9"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5bb9425fe881d578aeca0b2b4b3d314ec88738706f66f219c194d67179337cb"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64c65783e96e563103d641760664125e91bd85d8e49566ee560ded4da0d3e704"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf2430df4148b08fb4324b848672514b1385ae3807651f3567871f130a728cc3"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5397de3219a8b08ae9540c48f602996aa6b0b65d5a61683e233af8605c42b0f2"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:455705d34b4154a80ead722f4f185b04c4237e8e8e33f265cd0798d0e44825fa"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2b6f1b3bb6f640c1a92be3bbfbcb18657b125b99ecf141fb3310b5282c7d4ed"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3ad070b823ca5890cab606c940522d05d3d22395d432f4aaaf9d5b1653e47ced"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5b5467acbfc153847d5adb21e21e29847bcb5870e65c94c9206d20eb4e99a384"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e6662686aeb633ad65be2a42b4cb00178b3fbf7b91878f9446075c404ada552f"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2b4c884767504c0e2401babe8b5b7aea9148680d2e157fa28f01529d1f7fcf67"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3cd7874d57f13bf70078f1ff02b8b0aa48d5b9ed25fc48547516c6aba36f5741"}, - {file = "regex-2024.5.15-cp38-cp38-win32.whl", hash = "sha256:e4682f5ba31f475d58884045c1a97a860a007d44938c4c0895f41d64481edbc9"}, - {file = "regex-2024.5.15-cp38-cp38-win_amd64.whl", hash = "sha256:d99ceffa25ac45d150e30bd9ed14ec6039f2aad0ffa6bb87a5936f5782fc1569"}, - {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13cdaf31bed30a1e1c2453ef6015aa0983e1366fad2667657dbcac7b02f67133"}, - {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cac27dcaa821ca271855a32188aa61d12decb6fe45ffe3e722401fe61e323cd1"}, - {file = "regex-2024.5.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7dbe2467273b875ea2de38ded4eba86cbcbc9a1a6d0aa11dcf7bd2e67859c435"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f18a9a3513a99c4bef0e3efd4c4a5b11228b48aa80743be822b71e132ae4f5"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d347a741ea871c2e278fde6c48f85136c96b8659b632fb57a7d1ce1872547600"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1878b8301ed011704aea4c806a3cadbd76f84dece1ec09cc9e4dc934cfa5d4da"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4babf07ad476aaf7830d77000874d7611704a7fcf68c9c2ad151f5d94ae4bfc4"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cb514e137cb3488bce23352af3e12fb0dbedd1ee6e60da053c69fb1b29cc6c"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdd09d47c0b2efee9378679f8510ee6955d329424c659ab3c5e3a6edea696294"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:72d7a99cd6b8f958e85fc6ca5b37c4303294954eac1376535b03c2a43eb72629"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a094801d379ab20c2135529948cb84d417a2169b9bdceda2a36f5f10977ebc16"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c0c18345010870e58238790a6779a1219b4d97bd2e77e1140e8ee5d14df071aa"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:16093f563098448ff6b1fa68170e4acbef94e6b6a4e25e10eae8598bb1694b5d"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e38a7d4e8f633a33b4c7350fbd8bad3b70bf81439ac67ac38916c4a86b465456"}, - {file = "regex-2024.5.15-cp39-cp39-win32.whl", hash = "sha256:71a455a3c584a88f654b64feccc1e25876066c4f5ef26cd6dd711308aa538694"}, - {file = "regex-2024.5.15-cp39-cp39-win_amd64.whl", hash = "sha256:cab12877a9bdafde5500206d1020a584355a97884dfd388af3699e9137bf7388"}, - {file = "regex-2024.5.15.tar.gz", hash = "sha256:d3ee02d9e5f482cc8309134a91eeaacbdd2261ba111b0fef3748eeb4913e6a2c"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"}, + {file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"}, + {file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"}, + {file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"}, + {file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"}, + {file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"}, + {file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"}, + {file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"}, + {file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"}, + {file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"}, + {file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"}, + {file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"}, ] [[package]] @@ -3891,137 +3942,141 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rpds-py" -version = "0.19.0" +version = "0.20.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" files = [ - {file = "rpds_py-0.19.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:fb37bd599f031f1a6fb9e58ec62864ccf3ad549cf14bac527dbfa97123edcca4"}, - {file = "rpds_py-0.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3384d278df99ec2c6acf701d067147320b864ef6727405d6470838476e44d9e8"}, - {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54548e0be3ac117595408fd4ca0ac9278fde89829b0b518be92863b17ff67a2"}, - {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8eb488ef928cdbc05a27245e52de73c0d7c72a34240ef4d9893fdf65a8c1a955"}, - {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5da93debdfe27b2bfc69eefb592e1831d957b9535e0943a0ee8b97996de21b5"}, - {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:79e205c70afddd41f6ee79a8656aec738492a550247a7af697d5bd1aee14f766"}, - {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:959179efb3e4a27610e8d54d667c02a9feaa86bbabaf63efa7faa4dfa780d4f1"}, - {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a6e605bb9edcf010f54f8b6a590dd23a4b40a8cb141255eec2a03db249bc915b"}, - {file = "rpds_py-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9133d75dc119a61d1a0ded38fb9ba40a00ef41697cc07adb6ae098c875195a3f"}, - {file = "rpds_py-0.19.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd36b712d35e757e28bf2f40a71e8f8a2d43c8b026d881aa0c617b450d6865c9"}, - {file = "rpds_py-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354f3a91718489912f2e0fc331c24eaaf6a4565c080e00fbedb6015857c00582"}, - {file = "rpds_py-0.19.0-cp310-none-win32.whl", hash = "sha256:ebcbf356bf5c51afc3290e491d3722b26aaf5b6af3c1c7f6a1b757828a46e336"}, - {file = "rpds_py-0.19.0-cp310-none-win_amd64.whl", hash = "sha256:75a6076289b2df6c8ecb9d13ff79ae0cad1d5fb40af377a5021016d58cd691ec"}, - {file = "rpds_py-0.19.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6d45080095e585f8c5097897313def60caa2046da202cdb17a01f147fb263b81"}, - {file = "rpds_py-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5c9581019c96f865483d031691a5ff1cc455feb4d84fc6920a5ffc48a794d8a"}, - {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1540d807364c84516417115c38f0119dfec5ea5c0dd9a25332dea60b1d26fc4d"}, - {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e65489222b410f79711dc3d2d5003d2757e30874096b2008d50329ea4d0f88c"}, - {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da6f400eeb8c36f72ef6646ea530d6d175a4f77ff2ed8dfd6352842274c1d8b"}, - {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37f46bb11858717e0efa7893c0f7055c43b44c103e40e69442db5061cb26ed34"}, - {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:071d4adc734de562bd11d43bd134330fb6249769b2f66b9310dab7460f4bf714"}, - {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9625367c8955e4319049113ea4f8fee0c6c1145192d57946c6ffcd8fe8bf48dd"}, - {file = "rpds_py-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e19509145275d46bc4d1e16af0b57a12d227c8253655a46bbd5ec317e941279d"}, - {file = "rpds_py-0.19.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d438e4c020d8c39961deaf58f6913b1bf8832d9b6f62ec35bd93e97807e9cbc"}, - {file = "rpds_py-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90bf55d9d139e5d127193170f38c584ed3c79e16638890d2e36f23aa1630b952"}, - {file = "rpds_py-0.19.0-cp311-none-win32.whl", hash = "sha256:8d6ad132b1bc13d05ffe5b85e7a01a3998bf3a6302ba594b28d61b8c2cf13aaf"}, - {file = "rpds_py-0.19.0-cp311-none-win_amd64.whl", hash = "sha256:7ec72df7354e6b7f6eb2a17fa6901350018c3a9ad78e48d7b2b54d0412539a67"}, - {file = "rpds_py-0.19.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:5095a7c838a8647c32aa37c3a460d2c48debff7fc26e1136aee60100a8cd8f68"}, - {file = "rpds_py-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f2f78ef14077e08856e788fa482107aa602636c16c25bdf59c22ea525a785e9"}, - {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7cc6cb44f8636fbf4a934ca72f3e786ba3c9f9ba4f4d74611e7da80684e48d2"}, - {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf902878b4af334a09de7a45badbff0389e7cf8dc2e4dcf5f07125d0b7c2656d"}, - {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:688aa6b8aa724db1596514751ffb767766e02e5c4a87486ab36b8e1ebc1aedac"}, - {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57dbc9167d48e355e2569346b5aa4077f29bf86389c924df25c0a8b9124461fb"}, - {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4cf5a9497874822341c2ebe0d5850fed392034caadc0bad134ab6822c0925b"}, - {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a790d235b9d39c70a466200d506bb33a98e2ee374a9b4eec7a8ac64c2c261fa"}, - {file = "rpds_py-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d16089dfa58719c98a1c06f2daceba6d8e3fb9b5d7931af4a990a3c486241cb"}, - {file = "rpds_py-0.19.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bc9128e74fe94650367fe23f37074f121b9f796cabbd2f928f13e9661837296d"}, - {file = "rpds_py-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c8f77e661ffd96ff104bebf7d0f3255b02aa5d5b28326f5408d6284c4a8b3248"}, - {file = "rpds_py-0.19.0-cp312-none-win32.whl", hash = "sha256:5f83689a38e76969327e9b682be5521d87a0c9e5a2e187d2bc6be4765f0d4600"}, - {file = "rpds_py-0.19.0-cp312-none-win_amd64.whl", hash = "sha256:06925c50f86da0596b9c3c64c3837b2481337b83ef3519e5db2701df695453a4"}, - {file = "rpds_py-0.19.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:52e466bea6f8f3a44b1234570244b1cff45150f59a4acae3fcc5fd700c2993ca"}, - {file = "rpds_py-0.19.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e21cc693045fda7f745c790cb687958161ce172ffe3c5719ca1764e752237d16"}, - {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b31f059878eb1f5da8b2fd82480cc18bed8dcd7fb8fe68370e2e6285fa86da6"}, - {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dd46f309e953927dd018567d6a9e2fb84783963650171f6c5fe7e5c41fd5666"}, - {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34a01a4490e170376cd79258b7f755fa13b1a6c3667e872c8e35051ae857a92b"}, - {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcf426a8c38eb57f7bf28932e68425ba86def6e756a5b8cb4731d8e62e4e0223"}, - {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f68eea5df6347d3f1378ce992d86b2af16ad7ff4dcb4a19ccdc23dea901b87fb"}, - {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dab8d921b55a28287733263c0e4c7db11b3ee22aee158a4de09f13c93283c62d"}, - {file = "rpds_py-0.19.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6fe87efd7f47266dfc42fe76dae89060038f1d9cb911f89ae7e5084148d1cc08"}, - {file = "rpds_py-0.19.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:535d4b52524a961d220875688159277f0e9eeeda0ac45e766092bfb54437543f"}, - {file = "rpds_py-0.19.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8b1a94b8afc154fbe36978a511a1f155f9bd97664e4f1f7a374d72e180ceb0ae"}, - {file = "rpds_py-0.19.0-cp38-none-win32.whl", hash = "sha256:7c98298a15d6b90c8f6e3caa6457f4f022423caa5fa1a1ca7a5e9e512bdb77a4"}, - {file = "rpds_py-0.19.0-cp38-none-win_amd64.whl", hash = "sha256:b0da31853ab6e58a11db3205729133ce0df26e6804e93079dee095be3d681dc1"}, - {file = "rpds_py-0.19.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5039e3cef7b3e7a060de468a4a60a60a1f31786da94c6cb054e7a3c75906111c"}, - {file = "rpds_py-0.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab1932ca6cb8c7499a4d87cb21ccc0d3326f172cfb6a64021a889b591bb3045c"}, - {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2afd2164a1e85226fcb6a1da77a5c8896c18bfe08e82e8ceced5181c42d2179"}, - {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1c30841f5040de47a0046c243fc1b44ddc87d1b12435a43b8edff7e7cb1e0d0"}, - {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f757f359f30ec7dcebca662a6bd46d1098f8b9fb1fcd661a9e13f2e8ce343ba1"}, - {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15e65395a59d2e0e96caf8ee5389ffb4604e980479c32742936ddd7ade914b22"}, - {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb0f6eb3a320f24b94d177e62f4074ff438f2ad9d27e75a46221904ef21a7b05"}, - {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b228e693a2559888790936e20f5f88b6e9f8162c681830eda303bad7517b4d5a"}, - {file = "rpds_py-0.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2575efaa5d949c9f4e2cdbe7d805d02122c16065bfb8d95c129372d65a291a0b"}, - {file = "rpds_py-0.19.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5c872814b77a4e84afa293a1bee08c14daed1068b2bb1cc312edbf020bbbca2b"}, - {file = "rpds_py-0.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:850720e1b383df199b8433a20e02b25b72f0fded28bc03c5bd79e2ce7ef050be"}, - {file = "rpds_py-0.19.0-cp39-none-win32.whl", hash = "sha256:ce84a7efa5af9f54c0aa7692c45861c1667080814286cacb9958c07fc50294fb"}, - {file = "rpds_py-0.19.0-cp39-none-win_amd64.whl", hash = "sha256:1c26da90b8d06227d7769f34915913911222d24ce08c0ab2d60b354e2d9c7aff"}, - {file = "rpds_py-0.19.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:75969cf900d7be665ccb1622a9aba225cf386bbc9c3bcfeeab9f62b5048f4a07"}, - {file = "rpds_py-0.19.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8445f23f13339da640d1be8e44e5baf4af97e396882ebbf1692aecd67f67c479"}, - {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5a7c1062ef8aea3eda149f08120f10795835fc1c8bc6ad948fb9652a113ca55"}, - {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:462b0c18fbb48fdbf980914a02ee38c423a25fcc4cf40f66bacc95a2d2d73bc8"}, - {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3208f9aea18991ac7f2b39721e947bbd752a1abbe79ad90d9b6a84a74d44409b"}, - {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3444fe52b82f122d8a99bf66777aed6b858d392b12f4c317da19f8234db4533"}, - {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb4bac7185a9f0168d38c01d7a00addece9822a52870eee26b8d5b61409213"}, - {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6b130bd4163c93798a6b9bb96be64a7c43e1cec81126ffa7ffaa106e1fc5cef5"}, - {file = "rpds_py-0.19.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a707b158b4410aefb6b054715545bbb21aaa5d5d0080217290131c49c2124a6e"}, - {file = "rpds_py-0.19.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dc9ac4659456bde7c567107556ab065801622396b435a3ff213daef27b495388"}, - {file = "rpds_py-0.19.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:81ea573aa46d3b6b3d890cd3c0ad82105985e6058a4baed03cf92518081eec8c"}, - {file = "rpds_py-0.19.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f148c3f47f7f29a79c38cc5d020edcb5ca780020fab94dbc21f9af95c463581"}, - {file = "rpds_py-0.19.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0906357f90784a66e89ae3eadc2654f36c580a7d65cf63e6a616e4aec3a81be"}, - {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f629ecc2db6a4736b5ba95a8347b0089240d69ad14ac364f557d52ad68cf94b0"}, - {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6feacd1d178c30e5bc37184526e56740342fd2aa6371a28367bad7908d454fc"}, - {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b6068ee374fdfab63689be0963333aa83b0815ead5d8648389a8ded593378"}, - {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d57546bad81e0da13263e4c9ce30e96dcbe720dbff5ada08d2600a3502e526"}, - {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b6683a37338818646af718c9ca2a07f89787551057fae57c4ec0446dc6224b"}, - {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e8481b946792415adc07410420d6fc65a352b45d347b78fec45d8f8f0d7496f0"}, - {file = "rpds_py-0.19.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bec35eb20792ea64c3c57891bc3ca0bedb2884fbac2c8249d9b731447ecde4fa"}, - {file = "rpds_py-0.19.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:aa5476c3e3a402c37779e95f7b4048db2cb5b0ed0b9d006983965e93f40fe05a"}, - {file = "rpds_py-0.19.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:19d02c45f2507b489fd4df7b827940f1420480b3e2e471e952af4d44a1ea8e34"}, - {file = "rpds_py-0.19.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a3e2fd14c5d49ee1da322672375963f19f32b3d5953f0615b175ff7b9d38daed"}, - {file = "rpds_py-0.19.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:93a91c2640645303e874eada51f4f33351b84b351a689d470f8108d0e0694210"}, - {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b9fc03bf76a94065299d4a2ecd8dfbae4ae8e2e8098bbfa6ab6413ca267709"}, - {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a4b07cdf3f84310c08c1de2c12ddadbb7a77568bcb16e95489f9c81074322ed"}, - {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba0ed0dc6763d8bd6e5de5cf0d746d28e706a10b615ea382ac0ab17bb7388633"}, - {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:474bc83233abdcf2124ed3f66230a1c8435896046caa4b0b5ab6013c640803cc"}, - {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329c719d31362355a96b435f4653e3b4b061fcc9eba9f91dd40804ca637d914e"}, - {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef9101f3f7b59043a34f1dccbb385ca760467590951952d6701df0da9893ca0c"}, - {file = "rpds_py-0.19.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:0121803b0f424ee2109d6e1f27db45b166ebaa4b32ff47d6aa225642636cd834"}, - {file = "rpds_py-0.19.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:8344127403dea42f5970adccf6c5957a71a47f522171fafaf4c6ddb41b61703a"}, - {file = "rpds_py-0.19.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:443cec402ddd650bb2b885113e1dcedb22b1175c6be223b14246a714b61cd521"}, - {file = "rpds_py-0.19.0.tar.gz", hash = "sha256:4fdc9afadbeb393b4bbbad75481e0ea78e4469f2e1d713a90811700830b553a9"}, + {file = "rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2"}, + {file = "rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94"}, + {file = "rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee"}, + {file = "rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58"}, + {file = "rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0"}, + {file = "rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174"}, + {file = "rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139"}, + {file = "rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57"}, + {file = "rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a"}, + {file = "rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a"}, + {file = "rpds_py-0.20.0-cp38-none-win32.whl", hash = "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5"}, + {file = "rpds_py-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b"}, + {file = "rpds_py-0.20.0-cp39-none-win32.whl", hash = "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7"}, + {file = "rpds_py-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8"}, + {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, ] [[package]] name = "ruff" -version = "0.5.1" +version = "0.5.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.5.1-py3-none-linux_armv6l.whl", hash = "sha256:6ecf968fcf94d942d42b700af18ede94b07521bd188aaf2cd7bc898dd8cb63b6"}, - {file = "ruff-0.5.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:204fb0a472f00f2e6280a7c8c7c066e11e20e23a37557d63045bf27a616ba61c"}, - {file = "ruff-0.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d235968460e8758d1e1297e1de59a38d94102f60cafb4d5382033c324404ee9d"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38beace10b8d5f9b6bdc91619310af6d63dd2019f3fb2d17a2da26360d7962fa"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e478d2f09cf06add143cf8c4540ef77b6599191e0c50ed976582f06e588c994"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0368d765eec8247b8550251c49ebb20554cc4e812f383ff9f5bf0d5d94190b0"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3a9a9a1b582e37669b0138b7c1d9d60b9edac880b80eb2baba6d0e566bdeca4d"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdd9f723e16003623423affabcc0a807a66552ee6a29f90eddad87a40c750b78"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9fd62c1e99539da05fcdc1e90d20f74aec1b7a1613463ed77870057cd6bd96"}, - {file = "ruff-0.5.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e216fc75a80ea1fbd96af94a6233d90190d5b65cc3d5dfacf2bd48c3e067d3e1"}, - {file = "ruff-0.5.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c4c2112e9883a40967827d5c24803525145e7dab315497fae149764979ac7929"}, - {file = "ruff-0.5.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dfaf11c8a116394da3b65cd4b36de30d8552fa45b8119b9ef5ca6638ab964fa3"}, - {file = "ruff-0.5.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d7ceb9b2fe700ee09a0c6b192c5ef03c56eb82a0514218d8ff700f6ade004108"}, - {file = "ruff-0.5.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bac6288e82f6296f82ed5285f597713acb2a6ae26618ffc6b429c597b392535c"}, - {file = "ruff-0.5.1-py3-none-win32.whl", hash = "sha256:5c441d9c24ec09e1cb190a04535c5379b36b73c4bc20aa180c54812c27d1cca4"}, - {file = "ruff-0.5.1-py3-none-win_amd64.whl", hash = "sha256:b1789bf2cd3d1b5a7d38397cac1398ddf3ad7f73f4de01b1e913e2abc7dfc51d"}, - {file = "ruff-0.5.1-py3-none-win_arm64.whl", hash = "sha256:2875b7596a740cbbd492f32d24be73e545a4ce0a3daf51e4f4e609962bfd3cd2"}, - {file = "ruff-0.5.1.tar.gz", hash = "sha256:3164488aebd89b1745b47fd00604fb4358d774465f20d1fcd907f9c0fc1b0655"}, + {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, + {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, + {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, + {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, + {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, + {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, + {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, ] [[package]] @@ -4167,13 +4222,13 @@ win32 = ["pywin32"] [[package]] name = "sentry-sdk" -version = "2.9.0" +version = "2.12.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" files = [ - {file = "sentry_sdk-2.9.0-py2.py3-none-any.whl", hash = "sha256:0bea5fa8b564cc0d09f2e6f55893e8f70286048b0ffb3a341d5b695d1af0e6ee"}, - {file = "sentry_sdk-2.9.0.tar.gz", hash = "sha256:4c85bad74df9767976afb3eeddc33e0e153300e887d637775a753a35ef99bee6"}, + {file = "sentry_sdk-2.12.0-py2.py3-none-any.whl", hash = "sha256:7a8d5163d2ba5c5f4464628c6b68f85e86972f7c636acc78aed45c61b98b7a5e"}, + {file = "sentry_sdk-2.12.0.tar.gz", hash = "sha256:8763840497b817d44c49b3fe3f5f7388d083f2337ffedf008b2cdb63b5c86dc6"}, ] [package.dependencies] @@ -4203,7 +4258,7 @@ langchain = ["langchain (>=0.0.210)"] loguru = ["loguru (>=0.5)"] openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] opentelemetry = ["opentelemetry-distro (>=0.35b0)"] -opentelemetry-experimental = ["opentelemetry-instrumentation-aio-pika (==0.46b0)", "opentelemetry-instrumentation-aiohttp-client (==0.46b0)", "opentelemetry-instrumentation-aiopg (==0.46b0)", "opentelemetry-instrumentation-asgi (==0.46b0)", "opentelemetry-instrumentation-asyncio (==0.46b0)", "opentelemetry-instrumentation-asyncpg (==0.46b0)", "opentelemetry-instrumentation-aws-lambda (==0.46b0)", "opentelemetry-instrumentation-boto (==0.46b0)", "opentelemetry-instrumentation-boto3sqs (==0.46b0)", "opentelemetry-instrumentation-botocore (==0.46b0)", "opentelemetry-instrumentation-cassandra (==0.46b0)", "opentelemetry-instrumentation-celery (==0.46b0)", "opentelemetry-instrumentation-confluent-kafka (==0.46b0)", "opentelemetry-instrumentation-dbapi (==0.46b0)", "opentelemetry-instrumentation-django (==0.46b0)", "opentelemetry-instrumentation-elasticsearch (==0.46b0)", "opentelemetry-instrumentation-falcon (==0.46b0)", "opentelemetry-instrumentation-fastapi (==0.46b0)", "opentelemetry-instrumentation-flask (==0.46b0)", "opentelemetry-instrumentation-grpc (==0.46b0)", "opentelemetry-instrumentation-httpx (==0.46b0)", "opentelemetry-instrumentation-jinja2 (==0.46b0)", "opentelemetry-instrumentation-kafka-python (==0.46b0)", "opentelemetry-instrumentation-logging (==0.46b0)", "opentelemetry-instrumentation-mysql (==0.46b0)", "opentelemetry-instrumentation-mysqlclient (==0.46b0)", "opentelemetry-instrumentation-pika (==0.46b0)", "opentelemetry-instrumentation-psycopg (==0.46b0)", "opentelemetry-instrumentation-psycopg2 (==0.46b0)", "opentelemetry-instrumentation-pymemcache (==0.46b0)", "opentelemetry-instrumentation-pymongo (==0.46b0)", "opentelemetry-instrumentation-pymysql (==0.46b0)", "opentelemetry-instrumentation-pyramid (==0.46b0)", "opentelemetry-instrumentation-redis (==0.46b0)", "opentelemetry-instrumentation-remoulade (==0.46b0)", "opentelemetry-instrumentation-requests (==0.46b0)", "opentelemetry-instrumentation-sklearn (==0.46b0)", "opentelemetry-instrumentation-sqlalchemy (==0.46b0)", "opentelemetry-instrumentation-sqlite3 (==0.46b0)", "opentelemetry-instrumentation-starlette (==0.46b0)", "opentelemetry-instrumentation-system-metrics (==0.46b0)", "opentelemetry-instrumentation-threading (==0.46b0)", "opentelemetry-instrumentation-tornado (==0.46b0)", "opentelemetry-instrumentation-tortoiseorm (==0.46b0)", "opentelemetry-instrumentation-urllib (==0.46b0)", "opentelemetry-instrumentation-urllib3 (==0.46b0)", "opentelemetry-instrumentation-wsgi (==0.46b0)"] +opentelemetry-experimental = ["opentelemetry-distro"] pure-eval = ["asttokens", "executing", "pure-eval"] pymongo = ["pymongo (>=3.1)"] pyspark = ["pyspark (>=2.4.4)"] @@ -4317,18 +4372,19 @@ test = ["pytest"] [[package]] name = "setuptools" -version = "70.3.0" +version = "72.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, - {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, + {file = "setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1"}, + {file = "setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"}, ] [package.extras] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -4435,13 +4491,13 @@ tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version >= \"3.7\" and py [[package]] name = "tenacity" -version = "8.5.0" +version = "9.0.0" description = "Retry code until it succeeds" optional = false python-versions = ">=3.8" files = [ - {file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"}, - {file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"}, + {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, + {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, ] [package.extras] @@ -4483,13 +4539,13 @@ files = [ [[package]] name = "tensorboard-plugin-profile" -version = "2.15.1" +version = "2.17.0" description = "Profile Tensorboard Plugin" optional = false -python-versions = ">= 2.7, != 3.0.*, != 3.1.*" +python-versions = "!=3.0.*,!=3.1.*,>=2.7" files = [ - {file = "tensorboard_plugin_profile-2.15.1-py3-none-any.whl", hash = "sha256:93231c3330d19c0647279eb296b7a1f20ea70dfd366a9fe837b016aa2cc4190c"}, - {file = "tensorboard_plugin_profile-2.15.1.tar.gz", hash = "sha256:84bb33e446eb4a9c0616f669fc6a42cdd40eadd9ae1d74bf756f4f0479993273"}, + {file = "tensorboard_plugin_profile-2.17.0-py3-none-any.whl", hash = "sha256:47f04031c8746869755132c6570fd73b8c4101a1ef7343dd8787b53c9498a2f8"}, + {file = "tensorboard_plugin_profile-2.17.0.tar.gz", hash = "sha256:a7bb4eae9f41ca3606bb2fb43ffe04ab5dbb872fc5fc26a76086ebc608ef58ed"}, ] [package.dependencies] @@ -4666,6 +4722,17 @@ webencodings = ">=0.4" doc = ["sphinx", "sphinx_rtd_theme"] test = ["pytest", "ruff"] +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + [[package]] name = "tomli" version = "2.0.1" @@ -4710,13 +4777,13 @@ files = [ [[package]] name = "tqdm" -version = "4.66.4" +version = "4.66.5" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"}, - {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"}, + {file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"}, + {file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"}, ] [package.dependencies] @@ -4867,43 +4934,46 @@ sweeps = ["sweeps (>=0.2.0)"] [[package]] name = "watchdog" -version = "4.0.1" +version = "4.0.2" description = "Filesystem events monitoring" optional = false python-versions = ">=3.8" files = [ - {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"}, - {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"}, - {file = "watchdog-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b"}, - {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5"}, - {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767"}, - {file = "watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459"}, - {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175"}, - {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7"}, - {file = "watchdog-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28"}, - {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35"}, - {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db"}, - {file = "watchdog-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709"}, - {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba"}, - {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235"}, - {file = "watchdog-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682"}, - {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7"}, - {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5"}, - {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193"}, - {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625"}, - {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd"}, - {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee"}, - {file = "watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253"}, - {file = "watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d"}, - {file = "watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6"}, - {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57"}, - {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e"}, - {file = "watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5"}, - {file = "watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84"}, - {file = "watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429"}, - {file = "watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a"}, - {file = "watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d"}, - {file = "watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44"}, + {file = "watchdog-4.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ede7f010f2239b97cc79e6cb3c249e72962404ae3865860855d5cbe708b0fd22"}, + {file = "watchdog-4.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2cffa171445b0efa0726c561eca9a27d00a1f2b83846dbd5a4f639c4f8ca8e1"}, + {file = "watchdog-4.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c50f148b31b03fbadd6d0b5980e38b558046b127dc483e5e4505fcef250f9503"}, + {file = "watchdog-4.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c7d4bf585ad501c5f6c980e7be9c4f15604c7cc150e942d82083b31a7548930"}, + {file = "watchdog-4.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:914285126ad0b6eb2258bbbcb7b288d9dfd655ae88fa28945be05a7b475a800b"}, + {file = "watchdog-4.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:984306dc4720da5498b16fc037b36ac443816125a3705dfde4fd90652d8028ef"}, + {file = "watchdog-4.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a"}, + {file = "watchdog-4.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29"}, + {file = "watchdog-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a"}, + {file = "watchdog-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b"}, + {file = "watchdog-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d"}, + {file = "watchdog-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7"}, + {file = "watchdog-4.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:aa160781cafff2719b663c8a506156e9289d111d80f3387cf3af49cedee1f040"}, + {file = "watchdog-4.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f6ee8dedd255087bc7fe82adf046f0b75479b989185fb0bdf9a98b612170eac7"}, + {file = "watchdog-4.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b4359067d30d5b864e09c8597b112fe0a0a59321a0f331498b013fb097406b4"}, + {file = "watchdog-4.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:770eef5372f146997638d737c9a3c597a3b41037cfbc5c41538fc27c09c3a3f9"}, + {file = "watchdog-4.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eeea812f38536a0aa859972d50c76e37f4456474b02bd93674d1947cf1e39578"}, + {file = "watchdog-4.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b2c45f6e1e57ebb4687690c05bc3a2c1fb6ab260550c4290b8abb1335e0fd08b"}, + {file = "watchdog-4.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10b6683df70d340ac3279eff0b2766813f00f35a1d37515d2c99959ada8f05fa"}, + {file = "watchdog-4.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7c739888c20f99824f7aa9d31ac8a97353e22d0c0e54703a547a218f6637eb3"}, + {file = "watchdog-4.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c100d09ac72a8a08ddbf0629ddfa0b8ee41740f9051429baa8e31bb903ad7508"}, + {file = "watchdog-4.0.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f5315a8c8dd6dd9425b974515081fc0aadca1d1d61e078d2246509fd756141ee"}, + {file = "watchdog-4.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2d468028a77b42cc685ed694a7a550a8d1771bb05193ba7b24006b8241a571a1"}, + {file = "watchdog-4.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f15edcae3830ff20e55d1f4e743e92970c847bcddc8b7509bcd172aa04de506e"}, + {file = "watchdog-4.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83"}, + {file = "watchdog-4.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c"}, + {file = "watchdog-4.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a"}, + {file = "watchdog-4.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73"}, + {file = "watchdog-4.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc"}, + {file = "watchdog-4.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757"}, + {file = "watchdog-4.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c0b14488bd336c5b1845cee83d3e631a1f8b4e9c5091ec539406e4a324f882d8"}, + {file = "watchdog-4.0.2-py3-none-win32.whl", hash = "sha256:0d8a7e523ef03757a5aa29f591437d64d0d894635f8a50f370fe37f913ce4e19"}, + {file = "watchdog-4.0.2-py3-none-win_amd64.whl", hash = "sha256:c344453ef3bf875a535b0488e3ad28e341adbd5a9ffb0f7d62cefacc8824ef2b"}, + {file = "watchdog-4.0.2-py3-none-win_ia64.whl", hash = "sha256:baececaa8edff42cd16558a639a9b0ddf425f93d892e8392a56bf904f5eff22c"}, + {file = "watchdog-4.0.2.tar.gz", hash = "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270"}, ] [package.extras] @@ -4922,13 +4992,13 @@ files = [ [[package]] name = "webcolors" -version = "24.6.0" +version = "24.8.0" description = "A library for working with the color formats defined by HTML and CSS." optional = false python-versions = ">=3.8" files = [ - {file = "webcolors-24.6.0-py3-none-any.whl", hash = "sha256:8cf5bc7e28defd1d48b9e83d5fc30741328305a8195c29a8e668fa45586568a1"}, - {file = "webcolors-24.6.0.tar.gz", hash = "sha256:1d160d1de46b3e81e58d0a280d0c78b467dc80f47294b91b1ad8029d2cedb55b"}, + {file = "webcolors-24.8.0-py3-none-any.whl", hash = "sha256:fc4c3b59358ada164552084a8ebee637c221e4059267d0f8325b3b560f6c7f0a"}, + {file = "webcolors-24.8.0.tar.gz", hash = "sha256:08b07af286a01bcd30d583a7acadf629583d1f79bfef27dd2c2c5c263817277d"}, ] [package.extras] @@ -5003,13 +5073,13 @@ dev = ["Sphinx (>=4.5.0)", "black (>=22.3.0)", "pylint (>=2.13.7)", "pytest (>=7 [[package]] name = "wheel" -version = "0.43.0" +version = "0.44.0" description = "A built-package format for Python" optional = false python-versions = ">=3.8" files = [ - {file = "wheel-0.43.0-py3-none-any.whl", hash = "sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81"}, - {file = "wheel-0.43.0.tar.gz", hash = "sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85"}, + {file = "wheel-0.44.0-py3-none-any.whl", hash = "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f"}, + {file = "wheel-0.44.0.tar.gz", hash = "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49"}, ] [package.extras] @@ -5097,4 +5167,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "38cf29eb34bbf08bcdc545de3a8d8febbe64a52a94127d871820b8e3ecbf33ec" +content-hash = "1a3b60841bfb644cf9426a21237665cca6872a7a0963d7b2f8dcd755dc0ae3ed" diff --git a/pyproject.toml b/pyproject.toml index bcd13bb0..b26f8618 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ orjson = "^3.9.13" physiokit = "^0.8.1" requests = "^2.31.0" argdantic = {extras = ["all"], version = "^1.0.0"} -neuralspot-edge = "^0.1.3" +neuralspot-edge = {git = "https://github.com/AmbiqAI/neuralspot-edge.git"} [tool.poetry.group.dev.dependencies] ipython = "^8.21.0"